@factory/droid-sdk 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +239 -47
  2. package/dist/index.cjs +1869 -1296
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +6734 -4444
  5. package/dist/index.d.ts +6734 -4444
  6. package/dist/index.js +1815 -1298
  7. package/dist/index.js.map +1 -1
  8. package/docs/examples/init-metadata.md +79 -0
  9. package/docs/examples/list-sessions.md +64 -0
  10. package/docs/examples/multi-turn-session.md +77 -0
  11. package/docs/examples/one-shot-query.md +73 -0
  12. package/docs/examples/permission-handler.md +102 -0
  13. package/docs/examples/run.md +52 -0
  14. package/docs/examples/spec-mode.md +101 -0
  15. package/docs/examples/tool-controls.md +90 -0
  16. package/docs/typescript-sdk-reference.md +372 -0
  17. package/examples/.gitkeep +1 -0
  18. package/examples/abort-session-stream.ts +67 -0
  19. package/examples/droid-dev-structured-output.ts +44 -0
  20. package/examples/fork-session.ts +42 -0
  21. package/examples/init-metadata.ts +76 -0
  22. package/examples/interrupt-session.ts +67 -0
  23. package/examples/list-sessions.ts +65 -0
  24. package/examples/multi-turn-session.ts +67 -0
  25. package/examples/permission-handler.ts +98 -0
  26. package/examples/result-metadata.ts +62 -0
  27. package/examples/run.ts +38 -0
  28. package/examples/sdk-mcp-tool.ts +38 -0
  29. package/examples/simple-query.ts +53 -0
  30. package/examples/spec-mode-new-session.ts +121 -0
  31. package/examples/spec-mode-same-session.ts +133 -0
  32. package/examples/structured-output.ts +53 -0
  33. package/examples/test-compact.ts +60 -0
  34. package/examples/test-rewind.ts +155 -0
  35. package/examples/tool-controls.ts +69 -0
  36. package/package.json +26 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @factory/droid-sdk
2
2
 
3
- TypeScript SDK for the [Factory](https://factory.ai) Droid CLI. Provides a high-level API for interacting with Droid as a subprocess, with streaming message support, multi-turn sessions, and tool permission handling.
3
+ TypeScript SDK for the [Factory](https://factory.ai) Droid CLI. Provides a high-level API for interacting with Droid as a subprocess, with streaming message support, multi-turn sessions, spec mode, tool controls, initialization metadata, session forking, session discovery, and tool permission handling.
4
4
 
5
5
  ## Requirements
6
6
 
@@ -18,19 +18,19 @@ npm install @factory/droid-sdk
18
18
  Send a one-shot prompt and stream the response:
19
19
 
20
20
  ```ts
21
- import { query } from "@factory/droid-sdk";
21
+ import { DroidMessageType, query } from '@factory/droid-sdk';
22
22
 
23
23
  const stream = query({
24
- prompt: "What files are in the current directory?",
25
- cwd: "/my/project",
24
+ prompt: 'What files are in the current directory?',
25
+ cwd: '/my/project',
26
26
  });
27
27
 
28
28
  for await (const msg of stream) {
29
- if (msg.type === "assistant_text_delta") {
29
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
30
30
  process.stdout.write(msg.text);
31
31
  }
32
- if (msg.type === "turn_complete") {
33
- console.log("\nDone!");
32
+ if (msg.type === DroidMessageType.TurnComplete) {
33
+ console.log('\nDone!');
34
34
  }
35
35
  }
36
36
  ```
@@ -40,19 +40,19 @@ for await (const msg of stream) {
40
40
  Use `createSession()` for persistent conversations with multiple turns:
41
41
 
42
42
  ```ts
43
- import { createSession } from "@factory/droid-sdk";
43
+ import { createSession, DroidMessageType } from '@factory/droid-sdk';
44
44
 
45
- const session = await createSession({ cwd: "/my/project" });
45
+ const session = await createSession({ cwd: '/my/project' });
46
46
 
47
47
  // Streaming turn
48
- for await (const msg of session.stream("List all TypeScript files")) {
49
- if (msg.type === "assistant_text_delta") {
48
+ for await (const msg of session.stream('List all TypeScript files')) {
49
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
50
50
  process.stdout.write(msg.text);
51
51
  }
52
52
  }
53
53
 
54
54
  // Non-streaming turn
55
- const result = await session.send("Summarize the project");
55
+ const result = await session.send('Summarize the project');
56
56
  console.log(result.text);
57
57
 
58
58
  await session.close();
@@ -61,32 +61,186 @@ await session.close();
61
61
  Resume an existing session by ID:
62
62
 
63
63
  ```ts
64
- import { resumeSession } from "@factory/droid-sdk";
64
+ import { resumeSession } from '@factory/droid-sdk';
65
65
 
66
- const session = await resumeSession("session-id-here");
67
- const result = await session.send("Continue where we left off");
66
+ const session = await resumeSession('session-id-here');
67
+ const result = await session.send('Continue where we left off');
68
68
  console.log(result.text);
69
69
  await session.close();
70
70
  ```
71
71
 
72
+ The returned `DroidSession` also exposes `session.initResult`, which contains the raw `initialize_session` or `load_session` result returned by the JSON-RPC server.
73
+
74
+ ## Initialization Metadata
75
+
76
+ Inspect the raw initialization metadata from `query()`, `createSession()`, and `resumeSession()`:
77
+
78
+ ```ts
79
+ import { createSession, query, resumeSession } from '@factory/droid-sdk';
80
+
81
+ const stream = query({
82
+ prompt: 'Reply with "ready" and nothing else.',
83
+ cwd: '/my/project',
84
+ });
85
+
86
+ console.log(stream.sessionId); // null before initialization
87
+ console.log(stream.initResult); // null before initialization
88
+
89
+ const initialized = await stream.initialized;
90
+ console.log(initialized.sessionId);
91
+ console.log(initialized.settings.modelId);
92
+ stream.abort();
93
+
94
+ const session = await createSession({ cwd: '/my/project' });
95
+ console.log(session.initResult.settings.modelId);
96
+
97
+ const resumed = await resumeSession(session.sessionId, { cwd: '/my/project' });
98
+ console.log(resumed.initResult.cwd);
99
+
100
+ await resumed.close();
101
+ await session.close();
102
+ ```
103
+
104
+ ## Spec Mode
105
+
106
+ Start a session directly in spec mode, or enter spec mode later on an existing session:
107
+
108
+ ```ts
109
+ import {
110
+ createSession,
111
+ DroidInteractionMode,
112
+ ReasoningEffort,
113
+ } from '@factory/droid-sdk';
114
+
115
+ const session = await createSession({
116
+ cwd: '/my/project',
117
+ interactionMode: DroidInteractionMode.Spec,
118
+ specModeReasoningEffort: ReasoningEffort.High,
119
+ specModeModelId: 'claude-sonnet-4-20250514',
120
+ });
121
+
122
+ const plan = await session.send('Draft a plan for adding integration tests');
123
+ console.log(plan.text);
124
+
125
+ await session.enterSpecMode({
126
+ specModeReasoningEffort: ReasoningEffort.High,
127
+ });
128
+
129
+ await session.close();
130
+ ```
131
+
132
+ When handling spec-mode approval, you can approve implementation in the same session with `ToolConfirmationOutcome.ProceedOnce`, or hand off to a fresh session with `ToolConfirmationOutcome.ProceedNewSessionHigh`.
133
+
134
+ ## Tool Controls
135
+
136
+ Control which exec tools are available at session start, inspect the current tool catalog, and update tool overrides later:
137
+
138
+ ```ts
139
+ import { createSession } from '@factory/droid-sdk';
140
+
141
+ const session = await createSession({
142
+ cwd: '/my/project',
143
+ enabledToolIds: ['Read'],
144
+ disabledToolIds: ['Execute'],
145
+ });
146
+
147
+ const { tools } = await session.listTools();
148
+ console.log(
149
+ tools.map((tool) => ({
150
+ id: tool.llmId,
151
+ allowed: tool.currentlyAllowed,
152
+ }))
153
+ );
154
+
155
+ await session.updateSettings({
156
+ disabledToolIds: ['Read', 'Execute'],
157
+ });
158
+
159
+ await session.close();
160
+ ```
161
+
162
+ ## Forking Sessions
163
+
164
+ Fork the current server-side session and continue from the new session ID:
165
+
166
+ ```ts
167
+ import { createSession, resumeSession } from '@factory/droid-sdk';
168
+
169
+ const session = await createSession({ cwd: '/my/project' });
170
+
171
+ await session.send('Remember this phrase: mango sunrise');
172
+
173
+ const { newSessionId } = await session.forkSession();
174
+ const fork = await resumeSession(newSessionId, { cwd: '/my/project' });
175
+
176
+ const result = await fork.send('What phrase did I ask you to remember?');
177
+ console.log(result.text);
178
+
179
+ await fork.close();
180
+ await session.close();
181
+ ```
182
+
183
+ ## Listing Sessions
184
+
185
+ Discover droid sessions saved on disk (mirrors the CLI's `/sessions` command). Reads `~/.factory/sessions/` directly — no droid process is spawned, so this works even when no session is running:
186
+
187
+ ```ts
188
+ import { listSessions } from '@factory/droid-sdk';
189
+
190
+ // Sessions for the current project (cwd defaults to process.cwd())
191
+ const current = await listSessions();
192
+
193
+ // 10 most recent sessions in the current project
194
+ const recent = await listSessions({ numSessions: 10 });
195
+
196
+ // Every session on disk, most recent first
197
+ const all = await listSessions({ fetchOutsideCWD: true });
198
+
199
+ // 10 most recent sessions across all projects
200
+ const recentAcrossProjects = await listSessions({
201
+ fetchOutsideCWD: true,
202
+ numSessions: 10,
203
+ });
204
+
205
+ // Sessions for a specific other project
206
+ const other = await listSessions({ cwd: '/Users/me/other-repo' });
207
+
208
+ for (const s of current) {
209
+ console.log(`[${s.id}] ${s.title} (${s.messageCount} msgs)`);
210
+ }
211
+ ```
212
+
213
+ Each `SessionMetadata` record includes `id`, `title`, `sessionTitle`, `owner`, `messageCount`, `modifiedTime`, `createdTime`, `isFavorite`, `cwd`, `decompSessionType`, and `decompMissionId`. Archived sessions (those with an `archivedAt` in their settings file) are excluded automatically. Results are sorted by `modifiedTime` descending.
214
+
215
+ `ListSessionsOptions`:
216
+
217
+ - **`cwd`** — working directory to scope the listing to (default `process.cwd()`). Ignored when `fetchOutsideCWD` is `true`.
218
+ - **`fetchOutsideCWD`** — return sessions from every working directory on disk (default `false`)
219
+ - **`numSessions`** — cap on total sessions returned
220
+ - **`sessionsDir`** — override the sessions root (default `~/.factory/sessions/`)
221
+
72
222
  ## Permission Handling
73
223
 
74
224
  Handle tool confirmation requests with a custom permission handler:
75
225
 
76
226
  ```ts
77
- import { query, ToolConfirmationOutcome } from "@factory/droid-sdk";
227
+ import {
228
+ DroidMessageType,
229
+ query,
230
+ ToolConfirmationOutcome,
231
+ } from '@factory/droid-sdk';
78
232
 
79
233
  const stream = query({
80
- prompt: "Create a hello.txt file",
81
- cwd: "/my/project",
234
+ prompt: 'Create a hello.txt file',
235
+ cwd: '/my/project',
82
236
  permissionHandler(params) {
83
- console.log("Tool permission requested:", params);
237
+ console.log('Tool permission requested:', params);
84
238
  return ToolConfirmationOutcome.ProceedOnce;
85
239
  },
86
240
  });
87
241
 
88
242
  for await (const msg of stream) {
89
- if (msg.type === "assistant_text_delta") {
243
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
90
244
  process.stdout.write(msg.text);
91
245
  }
92
246
  }
@@ -96,11 +250,12 @@ for await (const msg of stream) {
96
250
 
97
251
  ### Top-Level Functions
98
252
 
99
- | Function | Description |
100
- |---|---|
101
- | `query(options)` | One-shot prompt → async generator of `DroidMessage` events |
102
- | `createSession(options?)` | Create a new multi-turn session → `DroidSession` |
103
- | `resumeSession(id, options?)` | Resume an existing session → `DroidSession` |
253
+ | Function | Description |
254
+ | ----------------------------- | ---------------------------------------------------------------- |
255
+ | `query(options)` | One-shot prompt → async generator of `DroidMessage` events |
256
+ | `createSession(options?)` | Create a new multi-turn session → `DroidSession` |
257
+ | `resumeSession(id, options?)` | Resume an existing session → `DroidSession` |
258
+ | `listSessions(options?)` | List droid sessions saved on disk → `Promise<SessionMetadata[]>` |
104
259
 
105
260
  ### `query(options): DroidQuery`
106
261
 
@@ -109,6 +264,10 @@ Returns an async generator that yields `DroidMessage` events. The returned `Droi
109
264
  - **`interrupt()`** — gracefully interrupt the agent's current turn
110
265
  - **`abort()`** — forcefully kill the subprocess
111
266
  - **`sessionId`** — the session ID (available after initialization)
267
+ - **`initResult`** — cached `initialize_session` result, or `null` before initialization
268
+ - **`initialized`** — promise that resolves with the `initialize_session` result
269
+
270
+ `query(options)` also accepts an `abortSignal` for external cancellation.
112
271
 
113
272
  ### `DroidSession`
114
273
 
@@ -119,8 +278,14 @@ Returned by `createSession()` and `resumeSession()`. Key methods:
119
278
  - **`interrupt()`** — interrupt the current turn
120
279
  - **`close()`** — close the session and release resources
121
280
  - **`updateSettings(params)`** — update model, autonomy level, etc.
122
- - **`addMcpServer(params)`** / **`removeMcpServer(params)`** — manage MCP servers
281
+ - **`enterSpecMode(params?)`** — switch the current session into spec mode
282
+ - **`forkSession()`** — create a forked server-side session and return its new session ID
283
+ - **`addMcpServer(params)`** / **`removeMcpServer(params)`** / **`toggleMcpServer(params)`** — manage MCP servers
284
+ - **`listMcpServers()`** / **`listMcpTools()`** / **`authenticateMcpServer(params)`** — inspect and authenticate MCP servers
285
+ - **`listTools(params?)`** — inspect the exec tool catalog and current allow/deny state
286
+ - **`renameSession(params)`** — rename the current session
123
287
  - **`sessionId`** — the session ID
288
+ - **`initResult`** — cached `initialize_session` or `load_session` result
124
289
 
125
290
  ### `DroidResult`
126
291
 
@@ -134,18 +299,30 @@ Returned by `session.send()`:
134
299
 
135
300
  All messages have a discriminated `type` field:
136
301
 
137
- | Type | Description |
138
- |---|---|
139
- | `assistant_text_delta` | Streaming text token from the assistant |
140
- | `thinking_text_delta` | Streaming reasoning/thinking token |
141
- | `tool_use` | Tool invocation by the assistant |
142
- | `tool_result` | Result from a tool execution |
143
- | `tool_progress` | Progress update during tool execution |
144
- | `working_state_changed` | Agent working state transition |
145
- | `token_usage_update` | Updated token usage counters |
146
- | `create_message` | Full assistant message created |
147
- | `turn_complete` | Sentinel: agent turn finished |
148
- | `error` | Error event from the process |
302
+ ```ts
303
+ import { DroidMessageType } from '@factory/droid-sdk';
304
+
305
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
306
+ process.stdout.write(msg.text);
307
+ }
308
+ ```
309
+
310
+ | Type | Description |
311
+ | ----------------------- | --------------------------------------- |
312
+ | `assistant_text_delta` | Streaming text token from the assistant |
313
+ | `thinking_text_delta` | Streaming reasoning/thinking token |
314
+ | `tool_use` | Tool invocation by the assistant |
315
+ | `tool_result` | Result from a tool execution |
316
+ | `tool_progress` | Progress update during tool execution |
317
+ | `working_state_changed` | Agent working state transition |
318
+ | `token_usage_update` | Updated token usage counters |
319
+ | `create_message` | Full assistant message created |
320
+ | `turn_complete` | Sentinel: agent turn finished |
321
+ | `session_title_updated` | Session title changed |
322
+ | `mcp_status_changed` | MCP server status changed |
323
+ | `mcp_auth_required` | MCP authentication required |
324
+ | `mcp_auth_completed` | MCP authentication completed |
325
+ | `error` | Error event from the process |
149
326
 
150
327
  ### Options
151
328
 
@@ -155,26 +332,33 @@ All messages have a discriminated `type` field:
155
332
  - **`cwd`** — working directory for the session
156
333
  - **`modelId`** — LLM model identifier
157
334
  - **`autonomyLevel`** — `AutonomyLevel` enum value
335
+ - **`interactionMode`** — `DroidInteractionMode` enum value
158
336
  - **`reasoningEffort`** — `ReasoningEffort` enum value
337
+ - **`specModeModelId`** — override model used in spec mode
338
+ - **`specModeReasoningEffort`** — override reasoning level used in spec mode
339
+ - **`mcpServers`** — initial MCP server configurations, including SDK-backed MCP servers from `createSdkMcpServer()`
340
+ - **`enabledToolIds`** — explicit exec tool allowlist
341
+ - **`disabledToolIds`** — explicit exec tool denylist
159
342
  - **`permissionHandler`** — callback for tool confirmations
160
343
  - **`askUserHandler`** — callback for interactive questions
344
+ - **`abortSignal`** — standard `AbortSignal` for cancellation
161
345
  - **`execPath`** — path to `droid` executable (default: `"droid"`)
162
346
  - **`transport`** — provide a custom transport instead of spawning a process
163
347
 
164
348
  ### `DroidClient`
165
349
 
166
- Low-level JSON-RPC client for advanced use. Provides typed methods for all 19 protocol operations. Most users should prefer `query()` and `createSession()`.
350
+ Low-level JSON-RPC client for advanced use. Provides typed methods for the underlying protocol operations, including `listTools()` and `renameSession()`. Most users should prefer `query()` and `createSession()`.
167
351
 
168
352
  ### Error Types
169
353
 
170
- | Error | Description |
171
- |---|---|
172
- | `ConnectionError` | Failed to connect to the droid process |
173
- | `ProtocolError` | JSON-RPC protocol error |
174
- | `SessionError` | Base session error |
175
- | `SessionNotFoundError` | Session ID not found |
176
- | `TimeoutError` | Request timed out |
177
- | `ProcessExitError` | Droid subprocess exited unexpectedly |
354
+ | Error | Description |
355
+ | ---------------------- | -------------------------------------- |
356
+ | `ConnectionError` | Failed to connect to the droid process |
357
+ | `ProtocolError` | JSON-RPC protocol error |
358
+ | `SessionError` | Base session error |
359
+ | `SessionNotFoundError` | Session ID not found |
360
+ | `TimeoutError` | Request timed out |
361
+ | `ProcessExitError` | Droid subprocess exited unexpectedly |
178
362
 
179
363
  ## Examples
180
364
 
@@ -182,7 +366,15 @@ See the [`examples/`](./examples) directory for runnable examples:
182
366
 
183
367
  - **[`simple-query.ts`](./examples/simple-query.ts)** — one-shot query with streaming output
184
368
  - **[`multi-turn-session.ts`](./examples/multi-turn-session.ts)** — multi-turn session lifecycle
369
+ - **[`abort-session-stream.ts`](./examples/abort-session-stream.ts)** — cancel an in-flight streaming session turn with `AbortSignal`
370
+ - **[`init-metadata.ts`](./examples/init-metadata.ts)** — read initialization and load metadata from query/session APIs
185
371
  - **[`permission-handler.ts`](./examples/permission-handler.ts)** — custom permission handling
372
+ - **[`spec-mode-same-session.ts`](./examples/spec-mode-same-session.ts)** — approve a spec and continue in the same session
373
+ - **[`spec-mode-new-session.ts`](./examples/spec-mode-new-session.ts)** — approve a spec and hand off implementation to a new session
374
+ - **[`tool-controls.ts`](./examples/tool-controls.ts)** — configure allow/deny lists and inspect tool availability
375
+ - **[`sdk-mcp-tool.ts`](./examples/sdk-mcp-tool.ts)** — expose SDK-defined tools to Droid through MCP
376
+ - **[`fork-session.ts`](./examples/fork-session.ts)** — fork a session and continue from the new session ID
377
+ - **[`list-sessions.ts`](./examples/list-sessions.ts)** — discover droid sessions saved on disk
186
378
 
187
379
  ## License
188
380