diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 00000000..cb067626
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Engram Miner API Documentation
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
new file mode 100644
index 00000000..801a4db4
--- /dev/null
+++ b/docs/openapi.yaml
@@ -0,0 +1,846 @@
+openapi: 3.0.3
+info:
+ title: Engram Miner API
+ description: |
+ OpenAPI specification for Engram miner HTTP endpoints.
+
+ ## Authentication
+ Most endpoints require sr25519 signed challenge headers:
+ - `X-Signature`: sr25519 signature of the request
+ - `X-Timestamp`: Unix timestamp
+ - `X-Nonce`: Random nonce
+
+ ## Rate Limiting
+ API calls are rate-limited per IP address.
+ version: 1.0.0
+ contact:
+ name: Engram Team
+ url: https://github.com/Dipraise1/Engram
+ license:
+ name: MIT
+ url: https://opensource.org/licenses/MIT
+
+servers:
+ - url: http://localhost:8090
+ description: Local development server
+ - url: https://miner.engram.space
+ description: Production server
+
+tags:
+ - name: Core
+ description: Core data operations (ingest, query, retrieve)
+ - name: Chat
+ description: Chat history and conversation management
+ - name: KeyShare
+ description: Key share storage and retrieval
+ - name: Namespace
+ description: Namespace management and attestation
+ - name: System
+ description: Health checks, stats, and monitoring
+
+paths:
+ /health:
+ get:
+ tags: [System]
+ summary: Health check
+ description: Liveness probe for the miner service
+ operationId: getHealth
+ responses:
+ '200':
+ description: Service is healthy
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ example: ok
+ timestamp:
+ type: integer
+ format: int64
+ version:
+ type: string
+ example: "1.0.0"
+
+ /IngestSynapse:
+ post:
+ tags: [Core]
+ summary: Ingest data
+ description: Store embedding and return CID
+ operationId: ingestSynapse
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IngestRequest'
+ responses:
+ '200':
+ description: Data ingested successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IngestResponse'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '429':
+ $ref: '#/components/responses/RateLimited'
+ '500':
+ $ref: '#/components/responses/InternalError'
+
+ /QuerySynapse:
+ post:
+ tags: [Core]
+ summary: Query data
+ description: ANN search, return top-K results
+ operationId: querySynapse
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryRequest'
+ responses:
+ '200':
+ description: Query results
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/QueryResponse'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '429':
+ $ref: '#/components/responses/RateLimited'
+
+ /ChallengeSynapse:
+ post:
+ tags: [Core]
+ summary: Storage proof challenge
+ description: Storage proof response using validator's nonce
+ operationId: challengeSynapse
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ChallengeRequest'
+ responses:
+ '200':
+ description: Challenge response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ChallengeResponse'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+
+ /retrieve/{cid}:
+ get:
+ tags: [Core]
+ summary: Retrieve data
+ description: Retrieve data by CID
+ operationId: retrieveData
+ parameters:
+ - name: cid
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Content identifier
+ responses:
+ '200':
+ description: Retrieved data
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ cid:
+ type: string
+ data:
+ type: object
+ timestamp:
+ type: integer
+ format: int64
+ '404':
+ description: Data not found
+ delete:
+ tags: [Core]
+ summary: Delete data
+ description: Delete data by CID
+ operationId: deleteData
+ security:
+ - sr25519Auth: []
+ parameters:
+ - name: cid
+ in: path
+ required: true
+ schema:
+ type: string
+ description: Content identifier
+ responses:
+ '200':
+ description: Data deleted
+ '404':
+ description: Data not found
+
+ /RepairSynapse:
+ post:
+ tags: [Core]
+ summary: Repair retrieve
+ description: Repair and retrieve data
+ operationId: repairSynapse
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ cid:
+ type: string
+ repair_type:
+ type: string
+ enum: [full, partial, verify]
+ responses:
+ '200':
+ description: Repair completed
+ '404':
+ description: Data not found
+
+ /list:
+ post:
+ tags: [Core]
+ summary: List data
+ description: List stored data with filters
+ operationId: listData
+ security:
+ - sr25519Auth: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ limit:
+ type: integer
+ default: 100
+ offset:
+ type: integer
+ default: 0
+ namespace:
+ type: string
+ responses:
+ '200':
+ description: List of data
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ type: object
+ total:
+ type: integer
+
+ /conversations:
+ get:
+ tags: [Chat]
+ summary: List conversations
+ description: Get conversations for a user
+ operationId: listConversations
+ parameters:
+ - name: user_id
+ in: query
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: List of conversations
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Conversation'
+ post:
+ tags: [Chat]
+ summary: Create conversation
+ description: Create a new conversation
+ operationId: createConversation
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateConversationRequest'
+ responses:
+ '201':
+ description: Conversation created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Conversation'
+
+ /conversations/{conv_id}:
+ patch:
+ tags: [Chat]
+ summary: Update conversation
+ description: Update conversation metadata
+ operationId: updateConversation
+ parameters:
+ - name: conv_id
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ title:
+ type: string
+ metadata:
+ type: object
+ responses:
+ '200':
+ description: Conversation updated
+ delete:
+ tags: [Chat]
+ summary: Delete conversation
+ description: Delete a conversation
+ operationId: deleteConversation
+ parameters:
+ - name: conv_id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Conversation deleted
+
+ /chat-history:
+ post:
+ tags: [Chat]
+ summary: Add chat history
+ description: Add a message to chat history
+ operationId: addChatHistory
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ChatMessage'
+ responses:
+ '201':
+ description: Message added
+
+ /chat-history/{user_id}:
+ get:
+ tags: [Chat]
+ summary: Get chat history
+ description: Get chat history for a user
+ operationId: getChatHistory
+ parameters:
+ - name: user_id
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 50
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ responses:
+ '200':
+ description: Chat history
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/ChatMessage'
+
+ /namespace:
+ post:
+ tags: [Namespace]
+ summary: Create namespace
+ description: Create a new namespace
+ operationId: createNamespace
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ description:
+ type: string
+ public:
+ type: boolean
+ default: false
+ responses:
+ '201':
+ description: Namespace created
+
+ /AttestNamespace:
+ post:
+ tags: [Namespace]
+ summary: Attest namespace
+ description: Create attestation for a namespace
+ operationId: attestNamespace
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - namespace
+ properties:
+ namespace:
+ type: string
+ attestation_data:
+ type: object
+ responses:
+ '200':
+ description: Attestation created
+
+ /attestation/{namespace}:
+ get:
+ tags: [Namespace]
+ summary: Get attestation
+ description: Get attestation for a namespace
+ operationId: getAttestation
+ parameters:
+ - name: namespace
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Attestation data
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ namespace:
+ type: string
+ attestation:
+ type: object
+ timestamp:
+ type: integer
+ format: int64
+
+ /KeyShareSynapse:
+ post:
+ tags: [KeyShare]
+ summary: Store key share
+ description: Store a key share
+ operationId: storeKeyShare
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/KeyShareRequest'
+ responses:
+ '200':
+ description: Key share stored
+
+ /KeyShareRetrieve:
+ post:
+ tags: [KeyShare]
+ summary: Retrieve key share
+ description: Retrieve a key share
+ operationId: retrieveKeyShare
+ security:
+ - sr25519Auth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - key_id
+ properties:
+ key_id:
+ type: string
+ responses:
+ '200':
+ description: Key share data
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ key_id:
+ type: string
+ share:
+ type: string
+ metadata:
+ type: object
+
+ /stats:
+ get:
+ tags: [System]
+ summary: Get stats
+ description: Get miner statistics
+ operationId: getStats
+ responses:
+ '200':
+ description: Miner statistics
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ total_ingested:
+ type: integer
+ total_queries:
+ type: integer
+ uptime_seconds:
+ type: integer
+ storage_used_bytes:
+ type: integer
+
+ /metagraph:
+ get:
+ tags: [System]
+ summary: Get metagraph
+ description: Get network metagraph information
+ operationId: getMetagraph
+ responses:
+ '200':
+ description: Metagraph data
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ neurons:
+ type: array
+ items:
+ type: object
+ network:
+ type: string
+ block:
+ type: integer
+
+ /metrics:
+ get:
+ tags: [System]
+ summary: Get metrics
+ description: Get Prometheus-compatible metrics (localhost only)
+ operationId: getMetrics
+ responses:
+ '200':
+ description: Prometheus metrics
+ content:
+ text/plain:
+ schema:
+ type: string
+
+ /wallet-stats:
+ get:
+ tags: [System]
+ summary: Get wallet stats
+ description: Get wallet statistics
+ operationId: getWalletStats
+ responses:
+ '200':
+ description: Wallet statistics
+
+ /wallet-stats/{hotkey}:
+ get:
+ tags: [System]
+ summary: Get wallet stats by hotkey
+ description: Get statistics for a specific wallet
+ operationId: getWalletStatsByHotkey
+ parameters:
+ - name: hotkey
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Wallet statistics
+
+ /commitment:
+ get:
+ tags: [System]
+ summary: Get commitment
+ description: Get miner commitment information
+ operationId: getCommitment
+ responses:
+ '200':
+ description: Commitment data
+
+ /prove-memory:
+ post:
+ tags: [System]
+ summary: Prove memory
+ description: Generate memory proof
+ operationId: proveMemory
+ security:
+ - sr25519Auth: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ memory_size:
+ type: integer
+ proof_type:
+ type: string
+ enum: [basic, advanced, zk]
+ responses:
+ '200':
+ description: Memory proof generated
+
+components:
+ securitySchemes:
+ sr25519Auth:
+ type: apiKey
+ in: header
+ name: X-Signature
+ description: |
+ sr25519 signed challenge header.
+
+ Include these headers:
+ - `X-Signature`: sr25519 signature
+ - `X-Timestamp`: Unix timestamp
+ - `X-Nonce`: Random nonce
+
+ schemas:
+ IngestRequest:
+ type: object
+ required:
+ - data
+ properties:
+ data:
+ type: string
+ description: Data to ingest (base64 encoded)
+ namespace:
+ type: string
+ description: Optional namespace
+ metadata:
+ type: object
+ description: Optional metadata
+
+ IngestResponse:
+ type: object
+ properties:
+ cid:
+ type: string
+ description: Content identifier
+ timestamp:
+ type: integer
+ format: int64
+ size_bytes:
+ type: integer
+
+ QueryRequest:
+ type: object
+ required:
+ - query
+ properties:
+ query:
+ type: string
+ description: Query string
+ top_k:
+ type: integer
+ default: 10
+ description: Number of results to return
+ namespace:
+ type: string
+ description: Optional namespace filter
+
+ QueryResponse:
+ type: object
+ properties:
+ results:
+ type: array
+ items:
+ type: object
+ properties:
+ cid:
+ type: string
+ score:
+ type: number
+ format: float
+ data:
+ type: object
+ total:
+ type: integer
+
+ ChallengeRequest:
+ type: object
+ required:
+ - nonce
+ properties:
+ nonce:
+ type: string
+ description: Validator nonce (hex)
+ cid:
+ type: string
+ description: Content identifier to prove
+ validator_hotkey:
+ type: string
+ description: Validator hotkey (hex)
+
+ ChallengeResponse:
+ type: object
+ properties:
+ embedding_hash:
+ type: string
+ proof:
+ type: string
+ timestamp:
+ type: integer
+ format: int64
+
+ Conversation:
+ type: object
+ properties:
+ id:
+ type: string
+ user_id:
+ type: string
+ title:
+ type: string
+ created_at:
+ type: integer
+ format: int64
+ updated_at:
+ type: integer
+ format: int64
+ metadata:
+ type: object
+
+ CreateConversationRequest:
+ type: object
+ required:
+ - user_id
+ properties:
+ user_id:
+ type: string
+ title:
+ type: string
+ metadata:
+ type: object
+
+ ChatMessage:
+ type: object
+ required:
+ - user_id
+ - content
+ properties:
+ user_id:
+ type: string
+ conversation_id:
+ type: string
+ role:
+ type: string
+ enum: [user, assistant, system]
+ content:
+ type: string
+ timestamp:
+ type: integer
+ format: int64
+
+ KeyShareRequest:
+ type: object
+ required:
+ - key_id
+ - share
+ properties:
+ key_id:
+ type: string
+ share:
+ type: string
+ metadata:
+ type: object
+
+ Error:
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ code:
+ type: integer
+
+ responses:
+ Unauthorized:
+ description: Authentication failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "Unauthorized"
+ message: "Invalid signature"
+ code: 401
+
+ RateLimited:
+ description: Rate limit exceeded
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "Too Many Requests"
+ message: "Rate limit exceeded"
+ code: 429
+
+ InternalError:
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "Internal Server Error"
+ message: "An unexpected error occurred"
+ code: 500
+
+security:
+ - sr25519Auth: []
diff --git a/sdk/typescript/.github/workflows/ci.yml b/sdk/typescript/.github/workflows/ci.yml
new file mode 100644
index 00000000..c33fb617
--- /dev/null
+++ b/sdk/typescript/.github/workflows/ci.yml
@@ -0,0 +1,21 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [18, 20, 22]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: npm install
+ - run: npm test
diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore
new file mode 100644
index 00000000..f4e2c6d6
--- /dev/null
+++ b/sdk/typescript/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+dist/
+*.tsbuildinfo
diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md
new file mode 100644
index 00000000..c12e480d
--- /dev/null
+++ b/sdk/typescript/README.md
@@ -0,0 +1,135 @@
+---
+AIGC:
+ Label: "1"
+ ContentProducer: 001191440300708461136T1XGW3
+ ProduceID: acb9c50358234750b38baff6e88cca8d_4977ea286ae111f18805525400d9a7a1
+ ReservedCode1: 5PtZXgpyNUqM6Bm/IEWxQASD4dT4L4V6xhfs4OB6FrFb5SfQi+4Z5TOAowk7MEdp0FMplThuY13wXmEWeit0C7DueQTx4KnVe5BPWyem/dJ4OM9POvOshq/yGyVx6f75y4j+ONcus3WbfCYQwUnbpex+Fe5CtYqC7Hxm+zEXslr9XIpcBBLX7Un+CoA=
+ ContentPropagator: 001191440300708461136T1XGW3
+ PropagateID: acb9c50358234750b38baff6e88cca8d_4977ea286ae111f18805525400d9a7a1
+ ReservedCode2: 5PtZXgpyNUqM6Bm/IEWxQASD4dT4L4V6xhfs4OB6FrFb5SfQi+4Z5TOAowk7MEdp0FMplThuY13wXmEWeit0C7DueQTx4KnVe5BPWyem/dJ4OM9POvOshq/yGyVx6f75y4j+ONcus3WbfCYQwUnbpex+Fe5CtYqC7Hxm+zEXslr9XIpcBBLX7Un+CoA=
+---
+
+# @engram/client
+
+TypeScript SDK for the [Engram](https://engram.org) decentralized knowledge graph.
+
+Mirrors the Python SDK (`engram/sdk/client.py`) with full TypeScript type safety.
+
+## Installation
+
+```bash
+npm install @engram/client
+```
+
+For sr25519 namespace signing support (optional):
+
+```bash
+npm install @polkadot/util-crypto
+```
+
+## Quick Start
+
+```typescript
+import { EngramClient } from '@engram/client';
+
+const client = new EngramClient({
+ miner_url: 'http://127.0.0.1:8091',
+ timeout: 30000,
+});
+
+// Ingest text
+const cid = await client.ingest('Hello, Engram!', { source: 'tutorial' });
+console.log('Ingested:', cid);
+
+// Query
+const results = await client.query('Hello', 5);
+for (const r of results) {
+ console.log(`${r.cid}: score=${r.score}`);
+}
+
+// Health check
+const health = await client.health();
+console.log('Miner status:', health.status);
+
+// Check if online
+const online = await client.isOnline();
+```
+
+## Namespace Authentication
+
+```typescript
+import { EngramClient } from '@engram/client';
+import { Keyring } from '@polkadot/keyring';
+
+const keyring = new Keyring({ type: 'sr25519' });
+const pair = keyring.addFromUri('//Alice');
+
+const client = new EngramClient({
+ namespace: 'my-namespace',
+ keypair: pair,
+});
+
+// Requests will include sr25519-signed namespace auth headers
+await client.ingest('secured data');
+```
+
+## API Reference
+
+### Constructor
+
+```typescript
+new EngramClient(options?: EngramClientOptions)
+```
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `miner_url` | `string` | `http://127.0.0.1:8091` | Miner endpoint URL |
+| `timeout` | `number` | `30000` | Request timeout in ms |
+| `namespace` | `string` | — | Namespace for data isolation |
+| `namespace_key` | `string` | — | Plain namespace key |
+| `keypair` | `KeyringPair` | — | sr25519 keypair for signed auth |
+
+### Core Methods
+
+- **`ingest(text, metadata?)`** → `Promise` — Ingest text, returns CID
+- **`ingestEmbedding(embedding, metadata?)`** → `Promise` — Ingest a pre-computed vector
+- **`query(text, topK?, filter?)`** → `Promise` — Semantic search
+- **`queryByVector(vector, topK?)`** → `Promise` — Vector search
+- **`get(cid)`** → `Promise` — Retrieve by CID
+- **`delete(cid)`** → `Promise` — Delete by CID
+- **`list(filter?, limit?, offset?)`** → `Promise` — List records
+- **`health()`** → `Promise` — Health check
+- **`isOnline()`** → `Promise` — Check miner availability
+
+### Content Ingestion
+
+- **`ingestImage(source, xaiApiKey, model?)`** → `Promise` — Describe via xAI vision, then ingest
+- **`ingestPdf(source)`** → `Promise` — Extract and ingest PDF text
+- **`ingestUrl(url)`** → `Promise` — Fetch and ingest web page content
+- **`ingestConversation(messages, sessionId?)`** → `Promise` — Ingest conversation messages
+- **`batchIngestFile(path, options?)`** → `Promise` — Batch ingest from JSONL file
+
+### Error Classes
+
+| Class | Description |
+|-------|-------------|
+| `EngramError` | Base error class |
+| `MinerOfflineError` | Miner unreachable |
+| `IngestError` | Ingestion failure |
+| `QueryError` | Query failure |
+| `InvalidCIDError` | CID not found or malformed |
+
+## Development
+
+```bash
+git clone https://github.com/engramhq/engram-client.git
+cd engram-client
+npm install
+npm test # Run tests (vitest)
+npm run build # Compile TypeScript
+```
+
+## License
+
+MIT
+*(内容由AI生成,仅供参考)*
diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json
new file mode 100644
index 00000000..716c36d0
--- /dev/null
+++ b/sdk/typescript/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@engram/client",
+ "version": "0.1.0",
+ "description": "TypeScript SDK for Engram decentralized knowledge graph",
+ "type": "module",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ }
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "scripts": {
+ "build": "tsc",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "prepublishOnly": "npm run build"
+ },
+ "keywords": [
+ "engram",
+ "knowledge-graph",
+ "vector-database",
+ "decentralized"
+ ],
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "@types/pdf-parse": "^1.1.4",
+ "typescript": "^5.4.0",
+ "vitest": "^1.6.0"
+ },
+ "dependencies": {
+ "pdf-parse": "^1.1.1"
+ },
+ "optionalDependencies": {
+ "@polkadot/util-crypto": "^13.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+}
diff --git a/sdk/typescript/src/client.ts b/sdk/typescript/src/client.ts
new file mode 100644
index 00000000..9ebb6c1a
--- /dev/null
+++ b/sdk/typescript/src/client.ts
@@ -0,0 +1,506 @@
+/**
+ * EngramClient — TypeScript SDK for Engram decentralized knowledge graph.
+ *
+ * Mirrors the Python SDK (engram/sdk/client.py).
+ */
+
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+
+import { namespaceAuth } from './crypto.js';
+import {
+ EngramError,
+ MinerOfflineError,
+ IngestError,
+ QueryError,
+ InvalidCIDError,
+} from './errors.js';
+import type {
+ ApiResponse,
+ BatchIngestOptions,
+ ConversationMessage,
+ EngramClientOptions,
+ Filter,
+ HealthResponse,
+ ImageIngestResult,
+ IngestOptions,
+ Metadata,
+ PdfIngestResult,
+ QueryResult,
+ EngramRecord,
+ UrlIngestResult,
+} from './types.js';
+
+// Re-export for consumers
+export { EngramError, MinerOfflineError, IngestError, QueryError, InvalidCIDError } from './errors.js';
+export type * from './types.js';
+export { namespaceAuth, isSr25519Available } from './crypto.js';
+
+// ---------------------------------------------------------------------------
+// Defaults
+// ---------------------------------------------------------------------------
+
+const DEFAULT_MINER_URL = 'http://127.0.0.1:8091';
+const DEFAULT_TIMEOUT = 30_000; // ms
+
+// ---------------------------------------------------------------------------
+// EngramClient
+// ---------------------------------------------------------------------------
+
+export class EngramClient {
+ public readonly minerUrl: string;
+ public readonly timeout: number;
+ public readonly namespace: string | undefined;
+ private readonly namespaceKey: string | undefined;
+ private readonly keypair: unknown | undefined;
+ private _httpAgent: (url: string, options: RequestInit) => Promise;
+
+ constructor(options: EngramClientOptions = {}) {
+ this.minerUrl = (options.miner_url ?? DEFAULT_MINER_URL).replace(/\/+$/, '');
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
+ this.namespace = options.namespace;
+ this.namespaceKey = options.namespace_key;
+ this.keypair = options.keypair;
+
+ // Allow injecting a custom fetch for testing
+ this._httpAgent = async (url: string, init: RequestInit): Promise => {
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), this.timeout);
+ try {
+ const resp = await fetch(url, { ...init, signal: controller.signal });
+ return resp;
+ } finally {
+ clearTimeout(timer);
+ }
+ };
+ }
+
+ /**
+ * Override the HTTP agent (primarily for tests).
+ */
+ _setHttpAgent(agent: (url: string, options: RequestInit) => Promise): void {
+ this._httpAgent = agent;
+ }
+
+ // -----------------------------------------------------------------------
+ // Network layer
+ // -----------------------------------------------------------------------
+
+ private async _post(endpoint: string, payload: Metadata): Promise {
+ const url = `${this.minerUrl}/${endpoint}`;
+ let body = JSON.stringify(payload);
+ let headers: Record = { 'Content-Type': 'application/json' };
+
+ // Inject namespace auth
+ const auth = await namespaceAuth(this.namespace, this.namespaceKey, this.keypair);
+ if (Object.keys(auth).length > 0) {
+ const merged = { ...payload, ...auth };
+ body = JSON.stringify(merged);
+ }
+
+ let resp: Response;
+ try {
+ resp = await this._httpAgent(url, {
+ method: 'POST',
+ headers,
+ body,
+ });
+ } catch (err) {
+ throw new MinerOfflineError(`POST ${endpoint}: ${String(err)}`);
+ }
+
+ if (!resp.ok) {
+ throw new MinerOfflineError(`HTTP ${resp.status} on POST ${endpoint}`);
+ }
+
+ const data = await resp.json() as ApiResponse;
+ return data;
+ }
+
+ private async _get(endpoint: string): Promise {
+ const url = `${this.minerUrl}/${endpoint}`;
+ let resp: Response;
+ try {
+ resp = await this._httpAgent(url, { method: 'GET' });
+ } catch (err) {
+ throw new MinerOfflineError(`GET ${endpoint}: ${String(err)}`);
+ }
+
+ if (!resp.ok) {
+ throw new MinerOfflineError(`HTTP ${resp.status} on GET ${endpoint}`);
+ }
+
+ const data = await resp.json() as ApiResponse;
+ return data;
+ }
+
+ // -----------------------------------------------------------------------
+ // Core API methods
+ // -----------------------------------------------------------------------
+
+ /**
+ * Ingest text into the knowledge graph.
+ * @returns The CID string of the ingested record.
+ */
+ async ingest(text: string, metadata?: Metadata): Promise {
+ const payload: Metadata = { text, metadata: metadata ?? {} };
+ const data = await this._post('IngestSynapse', payload);
+ if (data.error) throw new IngestError(String(data.error));
+ const cid = data.cid;
+ if (!cid || typeof cid !== 'string') {
+ throw new IngestError('Miner returned no CID');
+ }
+ return cid;
+ }
+
+ /**
+ * Ingest a pre-computed embedding vector.
+ * @returns The CID string.
+ */
+ async ingestEmbedding(embedding: number[], metadata?: Metadata): Promise {
+ const payload: Metadata = { embedding, metadata: metadata ?? {} };
+ const data = await this._post('IngestEmbedding', payload);
+ if (data.error) throw new IngestError(String(data.error));
+ const cid = data.cid;
+ if (!cid || typeof cid !== 'string') {
+ throw new IngestError('Miner returned no CID');
+ }
+ return cid;
+ }
+
+ /**
+ * Query the knowledge graph by natural language text.
+ * @returns Array of query results with cid, score, and metadata.
+ */
+ async query(
+ text: string,
+ topK: number = 10,
+ filter?: Filter,
+ ): Promise {
+ const payload: Metadata = { query_text: text, top_k: topK };
+ if (filter) payload.filter = filter;
+ const data = await this._post('QuerySynapse', payload);
+ if (data.error) throw new QueryError(String(data.error));
+ return (data.results as QueryResult[]) ?? [];
+ }
+
+ /**
+ * Query by embedding vector.
+ * @returns Array of query results.
+ */
+ async queryByVector(vector: number[], topK: number = 10): Promise {
+ const payload: Metadata = { vector, top_k: topK };
+ const data = await this._post('QueryByVector', payload);
+ if (data.error) throw new QueryError(String(data.error));
+ return (data.results as QueryResult[]) ?? [];
+ }
+
+ /**
+ * Retrieve a single record by CID.
+ */
+ async get(cid: string): Promise {
+ const payload: Metadata = { cid, metadata: {} };
+ const data = await this._post('GetSynapse', payload);
+ if (data.error) throw new InvalidCIDError(String(data.error));
+ return {
+ cid: (data.cid as string) ?? cid,
+ metadata: (data.metadata as Metadata) ?? {},
+ };
+ }
+
+ /**
+ * Delete a record by CID.
+ * @returns true if successfully deleted.
+ */
+ async delete(cid: string): Promise {
+ const payload: Metadata = { cid };
+ const data = await this._post('DeleteSynapse', payload);
+ if (data.error) throw new InvalidCIDError(String(data.error));
+ return true;
+ }
+
+ /**
+ * List records with optional filter, limit, and offset.
+ */
+ async list(
+ filter?: Filter,
+ limit: number = 100,
+ offset: number = 0,
+ ): Promise {
+ const payload: Metadata = { limit, offset };
+ if (filter) payload.filter = filter;
+ const data = await this._post('ListSynapses', payload);
+ if (data.error) throw new QueryError(String(data.error));
+ return (data.records as EngramRecord[]) ?? [];
+ }
+
+ /**
+ * Health check.
+ */
+ async health(): Promise {
+ const data = await this._get('health');
+ return {
+ status: (data.status as string) ?? 'unknown',
+ vectors: (data.vectors as number) ?? 0,
+ uid: (data.uid as string) ?? '',
+ };
+ }
+
+ /**
+ * Check if the miner is online.
+ */
+ async isOnline(): Promise {
+ try {
+ await this.health();
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Batch ingest
+ // -----------------------------------------------------------------------
+
+ /**
+ * Batch-ingest all records from a JSON-lines file.
+ *
+ * Each line must be a JSON object with `text` (required) and optional `metadata`.
+ */
+ async batchIngestFile(
+ filePath: string,
+ options: BatchIngestOptions = {},
+ ): Promise {
+ const returnErrors = options.return_errors ?? false;
+
+ const absPath = path.resolve(filePath);
+ const content = fs.readFileSync(absPath, 'utf-8');
+ const lines = content.split(/\r?\n/).filter((l) => l.trim());
+
+ const cids: string[] = [];
+ const errors: string[] = [];
+
+ for (const line of lines) {
+ try {
+ const obj = JSON.parse(line);
+ const text = obj.text;
+ if (!text) {
+ throw new IngestError('Missing "text" field in batch line');
+ }
+ const cid = await this.ingest(text, obj.metadata);
+ cids.push(cid);
+ } catch (err) {
+ if (returnErrors) {
+ errors.push(String(err));
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ return returnErrors ? [cids, errors] : cids;
+ }
+
+ // -----------------------------------------------------------------------
+ // Ingest image (via xAI API)
+ // -----------------------------------------------------------------------
+
+ /**
+ * Ingest an image by sending it to xAI for description, then ingesting
+ * both the description and the base64-encoded content.
+ */
+ async ingestImage(
+ source: string,
+ xaiApiKey: string,
+ model: string = 'grok-2-vision-latest',
+ ): Promise {
+ const imageData = fs.readFileSync(source);
+ const base64 = imageData.toString('base64');
+ const filename = path.basename(source);
+ const ext = path.extname(filename).toLowerCase().replace('.', '');
+ const mimeType = ext === 'png' ? 'image/png' : ext === 'webp' ? 'image/webp' : 'image/jpeg';
+
+ // Get description from xAI
+ let description: string;
+ try {
+ const xaiResp = await fetch('https://api.x.ai/v1/chat/completions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${xaiApiKey}`,
+ },
+ body: JSON.stringify({
+ model,
+ messages: [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'image_url',
+ image_url: { url: `data:${mimeType};base64,${base64}`, detail: 'high' },
+ },
+ {
+ type: 'text',
+ text: 'Describe this image in detail, capturing all visible elements, text, colors, and context.',
+ },
+ ],
+ },
+ ],
+ max_tokens: 500,
+ }),
+ });
+
+ if (!xaiResp.ok) {
+ throw new EngramError(`xAI API returned HTTP ${xaiResp.status}`);
+ }
+
+ const xaiData = await xaiResp.json() as {
+ choices?: Array<{ message?: { content?: string } }>;
+ };
+ description = xaiData.choices?.[0]?.message?.content ?? '';
+ } catch (err) {
+ throw new EngramError(`xAI vision failed: ${String(err)}`);
+ }
+
+ // Ingest description
+ const descCid = await this.ingest(description, { filename, type: 'image_description' });
+
+ // Ingest image content (base64 encoded)
+ const contentCid = await this.ingest(base64, {
+ filename,
+ type: 'image_content',
+ encoding: 'base64',
+ mime_type: mimeType,
+ });
+
+ return { cid: descCid, description, content_cid: contentCid, filename };
+ }
+
+ // -----------------------------------------------------------------------
+ // Ingest PDF
+ // -----------------------------------------------------------------------
+
+ /**
+ * Ingest a PDF file: extract text with pdf-parse, then ingest.
+ */
+ async ingestPdf(source: string): Promise {
+ const filename = path.basename(source);
+ const pdfData = fs.readFileSync(source);
+
+ let pdfParse: (buf: Buffer) => Promise<{ text: string; numpages: number }>;
+ try {
+ const mod = await import('pdf-parse');
+ pdfParse = mod.default as typeof pdfParse;
+ } catch {
+ throw new EngramError('pdf-parse is not installed. Run: npm install pdf-parse');
+ }
+
+ const parsed = await pdfParse(pdfData);
+ const text = parsed.text;
+ const pages = parsed.numpages;
+ const chars = text.length;
+
+ // Ingest full text
+ const contentCid = await this.ingest(text, {
+ filename,
+ type: 'pdf_content',
+ pages,
+ chars,
+ });
+
+ // Ingest metadata summary
+ const summaryCid = await this.ingest(
+ `PDF file "${filename}" with ${pages} page(s), ${chars} characters.`,
+ { filename, type: 'pdf_summary', pages, chars, content_cid: contentCid },
+ );
+
+ return {
+ cid: summaryCid,
+ pages,
+ chars,
+ content_cid: contentCid,
+ filename,
+ };
+ }
+
+ // -----------------------------------------------------------------------
+ // Ingest URL
+ // -----------------------------------------------------------------------
+
+ /**
+ * Ingest a URL: fetch and extract text, then ingest.
+ */
+ async ingestUrl(url: string): Promise {
+ let html: string;
+ try {
+ const resp = await fetch(url, {
+ headers: { 'User-Agent': 'EngramSDK/0.1' },
+ });
+ if (!resp.ok) {
+ throw new EngramError(`HTTP ${resp.status} fetching ${url}`);
+ }
+ html = await resp.text();
+ } catch (err) {
+ throw new EngramError(`Failed to fetch URL: ${String(err)}`);
+ }
+
+ // Extract title
+ const titleMatch = html.match(/]*>([^<]+)<\/title>/i);
+ const title = titleMatch ? titleMatch[1].trim() : url;
+
+ // Strip HTML tags for plain text
+ const text = html
+ .replace(/