Unified TypeScript SDK for the Upgraide platform APIs.
All services are accessible via the diskd factory, which provides a consistent
diskd.auth.* + namespaced service pattern across diskd.os.*, diskd.platform.*,
and diskd.utils.*:
import { diskd } from '@diskd-ai/sdk';
const auth = diskd.auth.apiKey({ workspaceId: '...' });
const drive = diskd.os.drive({ version: 'v1', auth });
const sessions = diskd.platform.sessions({
auth,
scope: { scopeType: 'project', projectId: 'proj-1' },
});
const crontab = diskd.platform.crontab({
auth,
scope: { scopeType: 'project', projectId: 'proj-1' },
});
const db = diskd.os.database({ auth, dbName: '...', schema: { ... } });
const ds = diskd.os.datasource({ auth, dbName: '...', entities: [...] });
const llm = diskd.os.llm({ auth });
const agents = diskd.os.agents({ auth, workspaceId: '...' }); // deprecated, unrouted
const mcp = diskd.os.mcp({ auth, workspaceId: '...' });
const messages = diskd.os.messagesStore({ auth });
const routines = diskd.platform.routines({ auth });
const operatives = diskd.platform.operatives({ auth });
const calendar = diskd.platform.calendar({ auth });
const tg = diskd.utils.tgUserBot({ auth, workspaceId: '...' });
const webNav = diskd.utils.webNavigator({ auth, workspaceId: '...' });-
Configure
.npmrcin your project (or~/.npmrc):@diskd:registry=https://gitlab.iosya.com/api/v4/projects/80/packages/npm/ //gitlab.iosya.com/api/v4/projects/80/packages/npm/:_authToken=${NPM_TOKEN}
-
Set
NPM_TOKENto a GitLab personal access token withread_apiscope:export NPM_TOKEN=glpat-xxxxxxxxxxxx -
Install:
bun add @diskd-ai/sdk
cd mono/platform-api
bun install
bun run buildThe SDK supports two authentication modes via the AuthModule interface.
Use diskd.auth.credentials() for OAuth2 service-account or PKCE browser flows:
import { diskd } from '@diskd-ai/sdk';
const auth = await diskd.auth.credentials({
scopes: [
'openid',
'drive:read',
'drive:write',
'sessions:read',
'sessions:write',
'crontab:read',
'crontab:write',
],
keyfilePath: 'credentials.json',
});
const drive = diskd.os.drive({ version: 'v1', auth });
const sessions = diskd.platform.sessions({
auth,
scope: { scopeType: 'project', projectId: 'proj-1' },
});
const crontab = diskd.platform.crontab({
auth,
scope: { scopeType: 'project', projectId: 'proj-1' },
});The SDK forwards scopes unchanged to the token request. Request the scopes
that the public APIS gateway route of every client you call declares
(required_scopes in apis-service config/routes.yaml). The gateway compares
the token against every scope the route declares, so request the full set for
each route:
| SDK client | Gateway route | Scopes |
|---|---|---|
diskd.os.drive, diskd.os.messagesStore |
/v1/os/drive |
drive:read drive:write |
diskd.os.database |
/v1/os/database |
database:read database:write |
diskd.os.email |
/v1/os/email |
email:read email:write |
diskd.os.llm |
/v1/os/llm |
llm:invoke llm:models:read |
diskd.os.mcp, diskd.os.mcpTools |
/v1/os/mcp |
mcp:read mcp:execute |
diskd.platform.sessions |
/v1/platform/sessions |
sessions:read sessions:write |
diskd.platform.crontab |
/v1/platform/crontab |
crontab:read crontab:write |
diskd.platform.operatives |
/v1/platform/operatives |
operatives:read operatives:write |
diskd.platform.projects, diskd.platform.notes |
/v1/platform/projects |
projects:read projects:write |
diskd.platform.routines, diskd.platform.routineRuns |
/v1/platform/routines |
routines:read routines:write |
diskd.platform.events |
/v1/platform/events |
events:write |
diskd.platform.calendar |
/v1/platform/calendar |
calendar:read calendar:write |
diskd.platform.contacts |
/v1/platform/contacts |
contacts:read contacts:write |
diskd.platform.inbox |
/v1/os/drive, /v1/os/email |
drive:read drive:write email:read email:write |
diskd.utils.tgUserBot |
/v1/utils/tg-userbot |
tg-userbot:read tg-userbot:write |
diskd.utils.webNavigator |
/v1/utils/web-navigator |
web-navigator:read web-navigator:write |
Keep openid when you need an OIDC sign-in (browser PKCE flow); it is harmless
for client-credentials keyfiles.
Rollout status (2026-10-05): the gateway does not enforce scopes yet. It logs
each OAuth2 request's token_scopes and whether they satisfy the route
(scopes_satisfied). OAuth clients registered before the iam-service scope
rollout only allow openid, so Hydra answers invalid_scope when they request
route scopes. SDK/CLI credentials pick up the route scopes the next time
credentials are fetched through the app's credentials flow (the workspace keeps
the same client id and secret, so an existing credentials.json keeps working
afterwards); self-service OAuth apps are reconciled by iam-service on startup.
Use diskd.auth.apiKey() for service-to-service communication within the cluster:
import { diskd } from '@diskd-ai/sdk';
const auth = diskd.auth.apiKey({ workspaceId: process.env.WORKSPACE_ID! });
const drive = diskd.os.drive({ version: 'v1', auth });diskd.auth.apiKey() reads APIS_API_KEY from the environment and fails fast when
either APIS_API_KEY or APIS_BASE_URL is missing.
Both auth modes produce identical client instances.
All resource APIs resolve from the centralized gateway base URL:
| Env Variable | Default |
|---|---|
APIS_BASE_URL |
https://apis.diskd.local:8080 |
APIS_API_KEY |
none |
The gateway is the single resource entrypoint. Public gateway URLs follow the
versioned convention https://apis.example/v1/{namespace}/{module}. The SDK
derives API paths from the same namespace structure as the public SDK surface
and lets the gateway handle API orchestration and auth strategy.
Derived default paths:
/v1/os/drive/v1/os/database/v1/os/llm/v1/os/agents(deprecated Agent Hub client; apis-service has no such route)/v1/os/mcp/v1/platform/sessions/v1/platform/crontab/v1/platform/operatives/v1/platform/projects/v1/platform/routines/v1/platform/events/v1/platform/calendar/v1/utils/tg-userbot/v1/utils/web-navigator
You can still override a client with an explicit url, but the default mode is
the centralized gateway.
This SDK does not treat resource APIs as independently-discovered hosts.
The canonical model is one centralized apis gateway behind APIS_BASE_URL.
That means:
- no per-service default env vars such as
LLM_ROUTER_BASE_URL,AGENT_HUB_BASE_URL, orMCP_HUB_BASE_URL - resource clients derive their route from
APIS_BASE_URLplus a namespace-derived path prefix - the gateway is responsible for request routing, API orchestration, and auth-strategy handling
Per-client url remains available only as an explicit override.
await drive.init();
const entries = await drive.list({ path: '/' });
const dir = await drive.create({ dirName: 'my-folder' });
await drive.rename({ inode: dir.inode, newName: 'renamed-folder' });
await drive.delete({ inodes: [dir.inode], recursive: true });
const resolved = await drive.resolve({ inodes: ['inode1', 'inode2'] });
await drive.updateMetadata({ inode: 'abc', metadata: { key: 'value' } });
await drive.updateAttributes({ inode: 'abc', attributes: ['pinned'] });// Buffer upload
const result = await drive.upload.file({
name: 'hello.txt',
data: new TextEncoder().encode('Hello, world!'),
mimeType: 'text/plain',
onProgress: (uploaded, total) => console.log(`${uploaded}/${total}`),
});
// Stream upload (large files)
const result = await drive.upload.file({
name: 'large.bin',
stream: readableStream,
size: 1_000_000_000,
sha256Root: 'precomputed-hex',
});const file = await drive.download.file({
inode: 'abc123',
onProgress: (downloaded, total) => console.log(`${downloaded}/${total}`),
});
await file.stream.pipeTo(writableStream);const meta = await drive.files.metadata({ inode: 'abc' });
const usage = await drive.diskUsage();
const ls = await drive.tools.ls({ path: '/', recursive: true });
const grep = await drive.tools.grep({ pattern: 'TODO' });The SDK exposes these session methods on diskd.platform.sessions({ auth, scope }):
startopensavelistdeletemessage
const session = await sessions.start({
title: 'Deployment help',
});
await session.append([
sessions.message({ role: 'user', content: 'How do I deploy to production?' }),
]);
const sessionList = await sessions.list();The SDK exposes these scheduler methods on diskd.platform.crontab({ auth, scope, timezone? }).
If timezone is omitted, the SDK uses the caller runtime timezone by default.
savegetgetStatuslistJobsrunJobcreateJob
await crontab.createJob({
job: {
jobId: '01JABCD2FGH3JK4MNP5QRST6VW',
enabled: true,
schedule: {
minute: '*/5',
hour: '*',
dayOfMonth: '*',
month: '*',
dayOfWeek: '*',
},
request: {
method: 'POST',
url: 'https://example.internal/hooks/sync',
payload: {
kind: 'json',
value: { source: 'sdk-example' },
},
},
},
});
const status = await crontab.getStatus();See examples/node/drive-upload-download.ts, examples/node/drive-session-external.ts, and examples/node/drive-crontab.ts.
diskd.os.email({ auth }) uses /v1/os/email to submit durable provider commands
and wait for workspace-scoped results. It supports account listing, five-minute
sync enrollment/status, body and attachment hydration, flags, deletion, and
independent SMTP/IMAP connection probes. Mail content remains in Drive; send mail
through the existing Drive Exchange Outbox contract.
diskd.platform.inbox({ auth }) uses these workers automatically. For a custom
worker gateway, replace the removed mcpUrl option with emailUrl. Existing
Inbox methods and message locators are unchanged. Internal consumers can supply
emailAuth when workers and Drive require different service credentials. The
worker credential is then sent only to the email endpoint. A client deadline does not
cancel an already accepted command.
Channel-agnostic message storage on top of Drive. Each workspace mailbox is one
SQLite file under /Mailboxes/<mailboxId>.mailbox; attachment bytes live in
Drive under /Mailboxes/<mailboxId>/<per-message-folder>/. The wire is the
messages_store/* JSON-RPC namespace served by drive.
Use it to persist email (IMAP / JMAP), Telegram, WhatsApp, or any other
channel where messages live in folders inside per-account mailboxes. Message
payload is opaque JSON; the store never inspects it.
For email read/unread changes, use diskd.platform.inbox({ auth }).markRead(...).
It resolves the persisted provider identity, queues a shared email worker command,
and verifies the provider flags and a fresh Drive mirror
read before returning. Provider flags take precedence over legacy isRead and
isFlagged booleans. A client configured with contentMode: 'stored-only' rejects
mark-read because it has no provider mutation boundary. The public method and
argument shape are unchanged; no stored-data migration is required.
The client exposes four boundaries -- mailboxes, folders, messages, attachments
-- via a functional scoping pattern: each level returns a client that
captures its identifiers in a closure, so callers never repeat
(mailboxId, folderId, externalId) on every call.
const messagesStore = diskd.os.messagesStore({ auth });
const mailbox = messagesStore.mailbox({ mailboxId: 'gmail-acme' });
const folder = mailbox.folder({ folderId: 'INBOX' });
const message = folder.message({ externalId: 'imap-uid-1001' });// Allocate the mailbox SQLite file under /Mailboxes/.
// metadata is opaque JSON, stashed on the underlying drive_databases record.
const created = await messagesStore.createMailbox({
mailboxId: 'gmail-acme',
displayName: 'acme@gmail.com',
metadata: { protocol: 'imap', host: 'imap.gmail.com' },
});
// → { mailboxId, dbInode, drivePath: '/Mailboxes/gmail-acme.mailbox' }
// Bind a mailbox-scoped client; subsequent calls don't repeat mailboxId.
const mailbox = messagesStore.mailbox({ mailboxId: 'gmail-acme' });
// Idempotent SQLite-schema bootstrap. Required before any folder/message ops.
await mailbox.init();
// Workspace-scoped enumeration.
const all = await messagesStore.listMailboxes();
// → readonly { mailboxId, displayName, dbInode, recordCount,
// sizeBytes, updatedAt }[]
// Cascade-delete (mailbox file + per-mailbox attachment subtree).
await mailbox.delete();folderId is opaque (caller-chosen) -- IMAP folder name, JMAP id, Telegram
chat_id, etc. Folder metadata is the natural place for protocol-specific
sync state (UIDVALIDITY/UIDNEXT/HIGHESTMODSEQ for IMAP, JMAP state,
Telegram pts, etc.). The store never reads it.
// Idempotent upsert. created=true on first call, false thereafter.
await mailbox.upsertFolder({
folderId: 'INBOX',
displayName: 'Inbox',
metadata: { uidvalidity: 12345, uidnext: 1101 },
});
// List folders in this mailbox.
const folders = await mailbox.listFolders();
// Bind a folder-scoped client.
const folder = mailbox.folder({ folderId: 'INBOX' });
// Update display name / sync metadata via the scoped client (folderId implicit).
await folder.upsert({
displayName: 'Inbox',
metadata: { uidvalidity: 12345, uidnext: 1200 },
});
// Read one folder.
const summary = await folder.get();
// → { folderId, displayName, metadata, messageCount, updatedAt }
// Cascade-delete folder + messages + per-message attachments.
const result = await folder.delete();
// → { folderId, deleted, deletedMessageCount }externalId is the caller's idempotency key within the folder (IMAP UID
stringified, Telegram message_id, JMAP id, ...). payload is opaque JSON
the store never inspects.
A successful upsertBatch response means the batch is durable in S3 -- the
SQLite head is committed before the call returns.
// Bulk insert-or-update by externalId.
const ub = await folder.upsertBatch({
items: [
{
externalId: 'imap-uid-1001',
payload: {
subject: 'Welcome to upgraide',
from: 'noreply@upgraide.dev',
receivedAt: '2026-04-28T13:21:32Z',
labels: ['inbox', 'unread'],
},
},
{ externalId: 'imap-uid-1002', payload: { subject: 'Your weekly digest' } },
],
});
// → { inserted: 2, updated: 0 }
// Cursor-paginated read.
let cursor: string | null = null;
do {
const page = await folder.listMessages({ limit: 100, cursor: cursor ?? undefined });
for (const m of page.items) {
// m.externalId, m.payload, m.createdAt, m.updatedAt
}
cursor = page.nextCursor;
} while (cursor);
// Single-message lookup.
const msg = await folder.getMessage({ externalId: 'imap-uid-1001' });
// Bulk delete; missing ids are silently skipped.
const del = await folder.deleteBatch({
externalIds: ['imap-uid-1001', 'imap-uid-1002'],
});
// → { deleted: 2 }Attachments follow Drive's upload-intent contract (start → PUT bytes →
commit) but are scoped to a single message. The per-message Drive folder is
created lazily on the first uploadStart call.
const message = folder.message({ externalId: 'imap-uid-1001' });
// 1. Begin upload -- get an intent + presigned URL.
const intent = await message.attachments.uploadStart({
attachmentId: 'att-1',
filename: 'invoice.pdf',
contentType: 'application/pdf',
sizeBytes: 12345,
});
// 2. PUT the bytes to intent.uploadUrl with header X-Upload-Intent-Id.
// (See Drive upload examples for streaming/buffer modes.)
const putRes = await fetch(intent.uploadUrl, {
method: 'PUT',
headers: { 'X-Upload-Intent-Id': intent.intentId, 'Content-Type': 'application/pdf' },
body: pdfBytes,
});
const { etag } = (await putRes.json()) as { etag: string };
// 3. Commit -- registers the attachment row.
await message.attachments.uploadCommit({
attachmentId: 'att-1',
intentId: intent.intentId,
etag,
});
// Enumerate this message's attachments.
const list = await message.attachments.list();
// Presigned download URL (with explicit expiresAt).
const dl = await message.attachments.downloadUrl({ attachmentId: 'att-1' });
// Delete the attachment row + its Drive file.
await message.attachments.delete({ attachmentId: 'att-1' });Same as the rest of the SDK -- both work:
// API key (internal services / Tilt / port-forward).
const auth = diskd.auth.apiKey({ workspaceId: 'ws-...' });
// OAuth2 client-credentials (external clients); Messages Store calls /v1/os/drive.
const auth = await diskd.auth.credentials({
scopes: ['openid', 'drive:read', 'drive:write'],
keyfilePath: '.agents/credentials.json',
});
const messagesStore = diskd.os.messagesStore({ auth });Workspace identity is always auth-derived (X-Workspace-Id header from API
key, ext.workspace_id claim from OAuth JWT) -- never sent on the wire as a
parameter. See examples/node/messages-store-example.ts for a full
end-to-end walk-through.
REST client for managing routines (automated workflows) scoped to profile or project:
const routines = diskd.platform.routines({ auth });
// List routines in a scope
const all = await routines.list({ scope: 'workspace' });
const projectRoutines = await routines.list({ scope: 'project', projectName: 'my-project' });
// Get by slug
const routine = await routines.get({ slug: 'daily-summary', scope: 'workspace' });
// Create
const created = await routines.create({
name: 'Daily Summary',
scope: 'workspace',
operativeSlug: 'research-agent',
triggerType: 'rhythm',
trigger: { cron: '0 9 * * *' },
steps: [{ id: 'step-1', name: 'Summarize', action: 'summarize', order: 0 }],
});
// Update
const updated = await routines.update(
'daily-summary',
{ status: 'paused' },
{ scopeType: 'workspace' },
);
// Delete
await routines.delete({ slug: 'daily-summary', scope: 'workspace' });REST client for managing operatives (AI agents) with attached files, skills, and MCP tools:
const ops = diskd.platform.operatives({ auth });
// List operatives in a project
const list = await ops.list({ projectId: 'proj-1' });
// Get by id or slug
const operative = await ops.get('op-01');
const bySlug = await ops.getBySlug({ projectId: 'proj-1', slug: 'research-agent' });
// Create
const created = await ops.create({
projectId: 'proj-1',
name: 'Research Agent',
engine: 'deep',
engineProvider: 'anthropic',
engineModel: 'claude-4',
});
// Update
await ops.update('op-01', {
orders: 'You are a research assistant focused on academic papers.',
fileAccess: 'selected',
});
// Delete
await ops.delete('op-01');Attach Drive files from the operative's project chroot as knowledge sources:
// Attach files (paths relative to project chroot)
await ops.files.add('op-01', { paths: ['/docs/knowledge-base', '/docs/readme.md'] });
// List attached files
const files = await ops.files.list('op-01');
// Detach a file
await ops.files.remove('op-01', files[0].id);// Attach skills
await ops.skills.add('op-01', { refIds: ['web-search', 'code-review'] });
// List attached skills
const skills = await ops.skills.list('op-01');
// Detach a skill
await ops.skills.remove('op-01', skills[0].id);// Attach MCP tools
await ops.tools.add('op-01', { selectors: ['github/search_repos', 'slack/send_message'] });
// List attached tools
const tools = await ops.tools.list('op-01');
// Detach a tool
await ops.tools.remove('op-01', tools[0].id);REST client for workspace calendar management -- events, attendees, note links, attachments, and settings:
const calendar = diskd.platform.calendar({ auth });
// Accounts and events
const accounts = await calendar.listAccounts();
const events = await calendar.listEvents({
startAfter: '2026-03-01T00:00:00Z',
startBefore: '2026-03-31T23:59:59Z',
});
// Event CRUD
const event = await calendar.createEvent({
calendarId: accounts[0].calendars[0].id,
title: 'Sprint Planning',
startAt: '2026-03-25T10:00:00Z',
endAt: '2026-03-25T11:00:00Z',
});
await calendar.updateEvent(event.id, {
title: 'Sprint Planning (updated)',
metadata: { timeBlockCategory: 'meeting' },
});
await calendar.deleteEvent(event.id);const attendee = await calendar.attendees.add(event.id, {
email: 'alice@example.com',
role: 'required',
});
await calendar.attendees.updateRsvp(event.id, attendee.id, 'yes');
await calendar.attendees.remove(event.id, attendee.id);const link = await calendar.noteLinks.add(event.id, {
noteDiskPath: '/Projects/sprint/notes/planning.md',
title: 'Planning Notes',
linkType: 'context',
});
await calendar.noteLinks.remove(event.id, link.id);const attachment = await calendar.attachments.add(event.id, {
type: 'url',
title: 'Meeting Recording',
url: 'https://meet.example.com/recording/123',
});
await calendar.attachments.remove(event.id, attachment.id);Events support an extensible metadata JSONB field for cross-domain data:
await calendar.updateEvent(event.id, {
metadata: {
timeBlockCategory: 'focus',
linkedNotes: [
{ noteDiskPath: '/docs/spec.md', title: 'Spec', linkType: 'context' },
],
},
});const settings = await calendar.getSettings();
await calendar.updateSettings({
weekStartDay: 0,
defaultView: 'month',
timezone: 'America/New_York',
});JSON-RPC client for Drive's SQLite-backed database operations:
const drive = diskd.os.drive({ version: 'v1', auth });
// Create a database with schema
const db = await drive.db.create({
name: 'myapp.workspace-123.main',
schema: {
users: {
id: { type: 'TEXT', primaryKey: true },
name: { type: 'TEXT', notNull: true },
email: { type: 'TEXT', notNull: true },
},
},
});
// Insert rows
await drive.db.insert({
name: 'myapp.workspace-123.main',
table: 'users',
rows: [{ id: '1', name: 'Alice', email: 'alice@example.com' }],
});
// Query with parameters
const result = await drive.db.query({
name: 'myapp.workspace-123.main',
sql: 'SELECT * FROM users WHERE id = ?',
parameters: ['1'],
});
// Commit, metadata, drop, resolve
await drive.db.commit({ name: 'myapp.workspace-123.main' });
const meta = await drive.db.metadata({ name: 'myapp.workspace-123.main' });
const resolved = await drive.db.resolveByInode({ dbInode: db.dbInode });Higher-level database + table-scoped repository pattern -- ideal for services that use Drive DB as their persistence layer:
// Create database with schema
const db = diskd.os.database({
auth,
dbName: 'shop.workspace-123.main',
dbType: 'database',
schema: {
users: { id: { type: 'TEXT', primaryKey: true }, name: { type: 'TEXT', notNull: true } },
orders: { id: { type: 'TEXT', primaryKey: true }, user_id: { type: 'TEXT' }, total: { type: 'INTEGER' } },
},
});
await db.ensureCreated();
// Get table-scoped repositories
const users = db.repository('users');
const orders = db.repository('orders');
// Insert
await users.insert([{ id: 'u1', name: 'Alice' }, { id: 'u2', name: 'Bob' }]);
// Find with where, orderBy, limit, offset
const results = await users.find({
where: { name: 'Alice' },
orderBy: { column: 'name', direction: 'ASC' },
limit: 10,
});
// Find one (returns null if not found)
const alice = await users.findOne({ id: 'u1' });
// Count
const total = await orders.count();
const pending = await orders.count({ status: 'pending' });
// Update
await orders.update({ where: { id: 'o1' }, set: { status: 'shipped' } });
// Delete
await orders.deleteRows({ status: 'cancelled' });
// Raw SQL at database level for joins and complex queries
const summary = await db.query(`
SELECT u.name, SUM(o.total) AS revenue
FROM users u JOIN orders o ON o.user_id = u.id
GROUP BY u.id ORDER BY revenue DESC
`);
// Commit and metadata (database-level operations)
await db.commit();
const meta = await db.metadata();See examples/node/drive-db-repository-example.ts.
Use TypeORM entities, relations, and repositories against Drive DB. SQL is routed
through Drive DB JSON-RPC, and TypeORM's transaction lifecycle maps to Drive DB's
commit/rollback semantics. Requires typeorm as a peer dependency.
npm install @diskd-ai/sdk typeormimport { diskd } from '@diskd-ai/sdk';
import { Entity, PrimaryColumn, Column } from 'typeorm';
// Define entities
@Entity({ name: 'users' })
class User {
@PrimaryColumn({ type: 'varchar', length: 26 })
id!: string;
@Column({ type: 'varchar' })
name!: string;
@Column({ type: 'varchar' })
email!: string;
}
// Create DataSource backed by Drive DB
process.env.APIS_BASE_URL ??= 'https://apis.diskd.local:8080';
const auth = diskd.auth.apiKey({ workspaceId: 'workspace-123' });
const ds = diskd.os.datasource({
auth,
url: `${process.env.APIS_BASE_URL}/v1/os/database/api/v1`,
dbName: 'shop.workspace-123.main',
entities: [User],
synchronize: true,
});
await ds.initialize();
// Standard TypeORM repository operations
const userRepo = ds.getRepository(User);
await userRepo.save({ id: 'u1', name: 'Alice', email: 'alice@example.com' });
const alice = await userRepo.findOneBy({ id: 'u1' });
const users = await userRepo.find({ order: { name: 'ASC' } });
// Persist to S3 (flush WAL)
await ds.driver.commit();
// Rollback discards uncommitted changes (revert to last commit)
await ds.driver.driveRollback();| TypeORM operation | Drive DB action |
|---|---|
BEGIN TRANSACTION |
No-op (writes auto-accumulate in WAL) |
COMMIT |
drive.db.commit() -- flush WAL to S3 |
ROLLBACK |
drive.db.rollback() -- discard WAL |
- No nested transactions / savepoints (deferred to v2)
- Affected row count returns 0 (each JSON-RPC call is a separate SQLite connection; works fine for ULID-based entities)
- Schema introspection from live database is limited;
synchronize: truegenerates DDL directly
See examples/node/typeorm-drive-example.ts and docs/typeorm-driver-design.md.
JSON-RPC 2.0 + NDJSON streaming for multi-provider LLM completions:
const llm = diskd.os.llm({ auth });
// Non-streaming completion
const result = await llm.completions.create({
provider: 'openai', model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Hello' }],
maxTokens: 64,
});
// Streaming
for await (const chunk of llm.completions.stream(params)) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
}
// Models, embeddings, OCR, audio transcription
const models = await llm.models.listAll();
const embeddings = await llm.embeddings.create({ provider: 'openai', model: 'text-embedding-3-small', input: ['text'] });See examples/node/llm-router-example.ts.
Deprecated and unrouted. Agent Hub is deprecated, and apis-service declares
no /v1/os/agents route, so diskd.os.agents (and createAgentHubClient)
cannot reach it through the gateway. The client stays exported only to avoid a
breaking change. Agent turns run on pi-agent-service, reached through the
app-service sessions API (/api/sessions); this SDK has no client for that API
yet. diskd.platform.sessions is the Drive session store and does not invoke
agents.
SSE streaming with StreamProtocolHandler + StreamProtocolFetcher for agent invocation:
import { diskd, StreamProtocolHandler } from '@diskd-ai/sdk';
const agents = diskd.os.agents({ auth, workspaceId: '...' });
// List agents and models
const agentList = await agents.agents.list();
const models = await agents.agents.getSupportedModels('assistant');
const billing = await agents.billing.getAliases();
// Invoke with fluent stream handling
const handler = new StreamProtocolHandler()
.on('response.output_text.delta', (e) => process.stdout.write(e.delta))
.on('response.completed', (e) => console.log('done', e.response.usage))
.on('response.failed', (e) => console.error(e.response.error.message))
.on('error', (e) => console.error(e.message));
const stream = await agents.invoke({
agentName: 'assistant',
query: 'Hello',
agentOptions: { maxTokens: 256 },
});
stream.map((event) => handler.handle(event))
.stop(() => console.log('stream closed'))
.catch((err) => console.error(err));Stream protocol events include text deltas, function calls/results, content parts (images, files, audio), web/file search lifecycle, external sources, plan updates, notifications, and error/completion signals.
See examples/node/agent-hub-example.ts.
REST client for MCP server catalog, registry, and integrations:
const mcp = diskd.os.mcp({ auth, workspaceId: '...' });
// Catalog
const catalog = await mcp.catalog.list({ search: 'github' });
const details = await mcp.catalog.getServerDetails(serverId);
// Registry (installed servers)
const registry = await mcp.registry.list();
const added = await mcp.registry.addServer({ catalogServerId: '...' });
await mcp.registry.toggleTool(serverId, toolId, false);
const logs = await mcp.registry.getServerLogs(serverId, { limit: 10 });
await mcp.registry.deleteServer(serverId);
// Env vars, connection settings, remote servers
await mcp.registry.upsertEnvVar(serverId, { key: 'TOKEN', value: '...' });
await mcp.registry.addRemoteServer({ name: 'My MCP', url: '...', authType: 'pat' });See examples/node/mcp-hub-example.ts.
REST client for Telegram channel resolution, importing, and message retrieval:
const tg = diskd.utils.tgUserBot({ auth, workspaceId: '...' });
// Resolve channel (public, no auth required)
const resolved = await tg.channels.resolve('durov');
// Channel operations
const channels = await tg.channels.list();
await tg.channels.add({ channelIdentifier: '@mychannel', limit: 1000 });
await tg.channels.sync({ telegramId: -1001234567890 });
// Messages and stats
const messages = await tg.channels.getMessages(channelId, { limit: 50, searchText: 'keyword' });
const stats = await tg.channels.getStats(channelId);
const status = await tg.channels.getStatus(channelId);
// Tasks
const tasks = await tg.tasks.list();
await tg.tasks.cancel(taskUuid);See examples/node/tg-userbot-example.ts.
REST client for URL resolution and web scraping jobs:
const webNav = diskd.utils.webNavigator({ auth, workspaceId: '...' });
// Resolve URL metadata
const meta = await webNav.resolve({ url: 'https://example.com' });
// Submit scrape job
const job = await webNav.scrape.submit({ url: 'https://example.com', depth: 1, maxPages: 10 });
const status = await webNav.scrape.getStatus(job.jobId);
const result = await webNav.scrape.getResult(job.jobId);
await webNav.scrape.cancel(job.jobId);See examples/node/web-navigator-example.ts.
Use @diskd-ai/sdk/browser and a standard OAuth2 Authorization Code + PKCE redirect.
Runnable example: examples/web/ (see examples/README.md).
-
Bump the version in
package.json:npm version patch # 0.3.0 -> 0.3.1 npm version minor # 0.3.0 -> 0.4.0 npm version major # 0.3.0 -> 1.0.0
-
Push the commit and tag:
git push gitlab main --tags
-
The GitLab CI pipeline triggers on
v*.*.*tags and automatically:- Builds the project
- Runs unit tests and typecheck
- Publishes to the GitLab Package Registry
-
Verify at:
https://gitlab.iosya.com/upgraide-v2/platform-api/-/packages
- Quickstart:
docs/sdk-quickstart.md - Examples:
examples/README.md - Design:
docs/drive-session-sdk-design.md