@factory/droid-sdk 0.6.0 → 0.7.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 (80) hide show
  1. package/README.md +35 -482
  2. package/dist/chunk-5UXINOXG.mjs +24483 -0
  3. package/dist/chunk-5UXINOXG.mjs.map +1 -0
  4. package/dist/chunk-C3KHERVH.js +24483 -0
  5. package/dist/chunk-C3KHERVH.js.map +1 -0
  6. package/dist/{index-CwXX5IUH.d.cts → index-D_SzTnFR.d.mts} +83826 -81547
  7. package/dist/{index-CwXX5IUH.d.ts → index-D_SzTnFR.d.ts} +83826 -81547
  8. package/dist/index.d.mts +4 -0
  9. package/dist/index.d.ts +4 -93103
  10. package/dist/index.js +821 -11745
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +822 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/node.d.mts +736 -0
  15. package/dist/node.d.ts +736 -0
  16. package/dist/node.js +4601 -0
  17. package/dist/node.js.map +1 -0
  18. package/dist/node.mjs +4601 -0
  19. package/dist/node.mjs.map +1 -0
  20. package/docs/typescript-sdk-reference.md +1260 -0
  21. package/examples/browser/daemon-lifecycle.ts +95 -0
  22. package/examples/browser/daemon-multi-session.ts +66 -0
  23. package/examples/browser/daemon-session.ts +73 -0
  24. package/examples/browser/index.html +68 -0
  25. package/examples/browser/list-sessions.ts +52 -0
  26. package/examples/{abort-session-stream.ts → node/abort-session-stream.ts} +8 -10
  27. package/examples/node/ask-user-elicitation.ts +35 -0
  28. package/examples/node/compact-session.ts +48 -0
  29. package/examples/node/enter-spec-mode.ts +66 -0
  30. package/examples/node/fork-session.ts +45 -0
  31. package/examples/node/hook-execution.ts +94 -0
  32. package/examples/node/image-attachment.ts +35 -0
  33. package/examples/node/init-metadata.ts +24 -0
  34. package/examples/{interrupt-session.ts → node/interrupt-session.ts} +7 -6
  35. package/examples/node/list-sessions.ts +20 -0
  36. package/examples/node/multi-turn-session.ts +33 -0
  37. package/examples/node/observability.ts +88 -0
  38. package/examples/node/permission-handler.ts +55 -0
  39. package/examples/node/rename-session.ts +29 -0
  40. package/examples/node/result-metadata.ts +34 -0
  41. package/examples/node/rewind-session.ts +69 -0
  42. package/examples/node/run.ts +25 -0
  43. package/examples/{sdk-mcp-tool.ts → node/sdk-mcp-tool.ts} +22 -12
  44. package/examples/node/session-settings.ts +35 -0
  45. package/examples/node/session-stream.ts +44 -0
  46. package/examples/node/spec-mode-new-session.ts +92 -0
  47. package/examples/node/spec-mode-same-session.ts +85 -0
  48. package/examples/node/structured-output.ts +73 -0
  49. package/examples/node/tool-controls.ts +26 -0
  50. package/examples/package.json +3 -0
  51. package/package.json +20 -42
  52. package/dist/index.cjs +0 -12135
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.d.cts +0 -93103
  55. package/dist/protocol/index.cjs +0 -6466
  56. package/dist/protocol/index.cjs.map +0 -1
  57. package/dist/protocol/index.d.cts +0 -2
  58. package/dist/protocol/index.d.ts +0 -2
  59. package/dist/protocol/index.js +0 -6078
  60. package/dist/protocol/index.js.map +0 -1
  61. package/docs/daemon-usage-guide.md +0 -544
  62. package/docs/sdk-usage-guide.md +0 -748
  63. package/examples/daemon-multi-session.ts +0 -67
  64. package/examples/droid-dev-structured-output.ts +0 -44
  65. package/examples/fork-session.ts +0 -68
  66. package/examples/hook-execution.ts +0 -68
  67. package/examples/init-metadata.ts +0 -24
  68. package/examples/list-sessions.ts +0 -19
  69. package/examples/multi-turn-session.ts +0 -40
  70. package/examples/permission-handler.ts +0 -41
  71. package/examples/readme-structured-output.ts +0 -26
  72. package/examples/result-metadata.ts +0 -63
  73. package/examples/run.ts +0 -39
  74. package/examples/session-stream.ts +0 -57
  75. package/examples/spec-mode-new-session.ts +0 -61
  76. package/examples/spec-mode-same-session.ts +0 -72
  77. package/examples/structured-output.ts +0 -60
  78. package/examples/test-compact.ts +0 -65
  79. package/examples/test-rewind.ts +0 -87
  80. package/examples/tool-controls.ts +0 -37
@@ -0,0 +1,1260 @@
1
+ # Factory TypeScript SDK
2
+
3
+ The Factory TypeScript SDK lets applications run Droid, continue conversations,
4
+ stream work, control tools, and connect to a running Droid daemon.
5
+
6
+ The public npm package is `@factory/droid-sdk`.
7
+
8
+ This page is the single user-facing SDK guide. The package's exported
9
+ TypeScript declarations remain authoritative for its compile-time API surface.
10
+
11
+ ## Overview
12
+
13
+ The SDK has two main runtimes.
14
+
15
+ | Runtime | Import | Use it for |
16
+ | ------- | ------------------------- | --------------------------------------------------------------- |
17
+ | Node | `@factory/droid-sdk/node` | Scripts, CI, local tools, and apps that can start the Droid CLI |
18
+ | Daemon | `@factory/droid-sdk` | Browser or Node apps that connect to an existing Droid daemon |
19
+
20
+ The Node runtime starts a Droid subprocess for you. The daemon runtime connects
21
+ to a daemon over WebSocket and can manage several sessions through one
22
+ connection.
23
+
24
+ The root import is browser-safe. The `/node` import adds subprocess,
25
+ filesystem, local session discovery, and SDK MCP tool APIs.
26
+
27
+ ## Requirements and installation
28
+
29
+ Install the package:
30
+
31
+ ```bash
32
+ npm install @factory/droid-sdk
33
+ ```
34
+
35
+ The SDK requires Node.js 18 or later.
36
+
37
+ By default, Node session APIs start the `droid` CLI from `PATH`. You can provide
38
+ a different executable with `execPath`. Supplying a custom `transport` bypasses
39
+ subprocess creation and this requirement.
40
+
41
+ Set an API key:
42
+
43
+ ```bash
44
+ export FACTORY_API_KEY="your-key"
45
+ ```
46
+
47
+ You can also pass `apiKey` directly when creating a Node session.
48
+
49
+ ### Browser security
50
+
51
+ The daemon client currently authenticates with a Factory API key. Use browser
52
+ examples only in trusted local tools. Do not ship a Factory API key in a public
53
+ website, commit it to source control, or put it in a URL.
54
+
55
+ An HTTPS page must connect with `wss://`. Local pages served over HTTP can use
56
+ `ws://`.
57
+
58
+ ## Quick start
59
+
60
+ ### Run one prompt
61
+
62
+ **Runtime: Node**
63
+
64
+ ```ts
65
+ import { run } from '@factory/droid-sdk/node';
66
+
67
+ const result = await run('Summarize this repository.');
68
+
69
+ if (!result.success) {
70
+ throw new Error(result.error?.message ?? 'The run did not complete.');
71
+ }
72
+
73
+ console.log(result.text);
74
+ ```
75
+
76
+ `run()` creates a session, waits for its final result, and closes the session.
77
+ Use it for one-shot work.
78
+
79
+ ### Continue a conversation
80
+
81
+ **Runtime: Node**
82
+
83
+ ```ts
84
+ import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
85
+
86
+ const session = await createSession();
87
+
88
+ try {
89
+ for await (const message of session.stream('What does this project do?')) {
90
+ if (message.type === DroidMessageType.Assistant) {
91
+ console.log(message.text);
92
+ }
93
+ }
94
+
95
+ for await (const message of session.stream('What should I test first?')) {
96
+ if (message.type === DroidMessageType.Assistant) {
97
+ console.log(message.text);
98
+ }
99
+ }
100
+ } finally {
101
+ await session.close();
102
+ }
103
+ ```
104
+
105
+ Use a session when later prompts need context from earlier turns.
106
+
107
+ ### Connect to a daemon
108
+
109
+ **Runtime: Node or trusted local browser**
110
+
111
+ ```ts
112
+ import { connectToDaemon, DroidMessageType } from '@factory/droid-sdk';
113
+
114
+ async function runWithDaemon(apiKey: string, cwd: string) {
115
+ const droid = await connectToDaemon({
116
+ url: 'ws://127.0.0.1:37643',
117
+ auth: { apiKey },
118
+ });
119
+
120
+ const session = await droid.sessions.create({ cwd });
121
+
122
+ try {
123
+ for await (const message of session.stream('Summarize this repository.')) {
124
+ if (message.type === DroidMessageType.Assistant) {
125
+ console.log(message.text);
126
+ }
127
+ }
128
+ } finally {
129
+ await Promise.allSettled([session.close()]);
130
+ droid.disconnect();
131
+ }
132
+ }
133
+ ```
134
+
135
+ In browser code, obtain both arguments from trusted local configuration. This
136
+ does not make an API key secret from browser JavaScript, so keep this pattern
137
+ restricted to trusted local pages.
138
+
139
+ ## Core concepts
140
+
141
+ ### Session
142
+
143
+ A session holds conversation history, settings, and a working directory.
144
+
145
+ Node sessions are created with `createSession()` or loaded with
146
+ `resumeSession()`. Daemon sessions are created or resumed through
147
+ `droid.sessions`.
148
+
149
+ ### Turn
150
+
151
+ A turn starts when you send one prompt with `session.stream()`. A session can
152
+ run one turn at a time. Different daemon sessions can run turns concurrently.
153
+
154
+ ### Stream message
155
+
156
+ `session.stream()` yields a discriminated message union. The default stream
157
+ contains complete user, assistant, tool, hook, error, and result messages.
158
+
159
+ ### Result
160
+
161
+ Every normally completed stream ends with a result message. A result reports
162
+ success, interruption, or failure and includes final text, messages, duration,
163
+ turn count, and token usage when available.
164
+
165
+ ### Node and daemon lifecycle behavior
166
+
167
+ Node and daemon replacement operations intentionally behave differently.
168
+
169
+ | Behavior | Node session | Daemon session |
170
+ | ------------------------------- | ---------------------------------------------------------- | ------------------------------ |
171
+ | Successor response | `fork`: handle; `compact`/`rewind`: outcome with `session` | Outcome with `newSessionId` |
172
+ | Source handle after replacement | Retired | Still usable |
173
+ | Load successor | Automatic | Call `droid.sessions.resume()` |
174
+
175
+ See [Session lifecycle](#session-lifecycle) for examples.
176
+
177
+ ## One-shot runs
178
+
179
+ **Runtime: Node**
180
+
181
+ ```ts
182
+ function run(prompt: string, options?: RunOptions): Promise<DroidResult>;
183
+ ```
184
+
185
+ `run()` supports the same creation and message options as a Node session,
186
+ except partial stream events.
187
+
188
+ ```ts
189
+ const result = await run('Review the authentication middleware.', {
190
+ cwd: '/path/to/repository',
191
+ modelId: 'model-id',
192
+ reasoningEffort: ReasoningEffort.High,
193
+ disabledToolIds: ['Execute'],
194
+ });
195
+ ```
196
+
197
+ Common options:
198
+
199
+ | Option | Purpose |
200
+ | ------------------------------ | ---------------------------------------------- |
201
+ | `cwd` | Working directory. Defaults to `process.cwd()` |
202
+ | `apiKey` | Factory API key. Defaults to `FACTORY_API_KEY` |
203
+ | `modelId` | Model selection |
204
+ | `reasoningEffort` | Model reasoning level |
205
+ | `autonomyLevel` | Permission policy level |
206
+ | `interactionMode` | Auto, spec, or mission mode. AGI is deprecated |
207
+ | `specModeModelId` | Model used in spec mode |
208
+ | `specModeReasoningEffort` | Reasoning level used in spec mode |
209
+ | `disabledToolIds` | Tools that Droid cannot use |
210
+ | `autoRejectPermissionRequests` | Reject all permission requests automatically |
211
+ | `disableBuiltinSkills` | Hide Factory-provided built-in skills |
212
+ | `permissionHandler` | Handles permission requests |
213
+ | `askUserHandler` | Answers AskUser requests |
214
+ | `tags` | Session attribution tags |
215
+ | `sessionSource` | Session source attribution |
216
+ | `images` | Base64 image attachments |
217
+ | `files` | Text or PDF attachments |
218
+ | `outputFormat` | JSON schema for structured output |
219
+ | `abortSignal` | Cancels the run |
220
+ | `mcpServers` | MCP servers available to the session |
221
+ | `observability` | Logging, metrics, and trace integration |
222
+ | `execPath` | Droid executable path |
223
+ | `execArgs` | Additional Droid process arguments |
224
+ | `env` | Additional Droid process environment variables |
225
+ | `transport` | Custom string-framed transport |
226
+
227
+ ## Sessions
228
+
229
+ ### Create a Node session
230
+
231
+ ```ts
232
+ import { createSession } from '@factory/droid-sdk/node';
233
+
234
+ const session = await createSession({
235
+ cwd: process.cwd(),
236
+ modelId: 'model-id',
237
+ });
238
+ ```
239
+
240
+ When omitted, `cwd` defaults to `process.cwd()`. Model and reasoning defaults
241
+ come from Droid settings.
242
+
243
+ ### Resume a Node session
244
+
245
+ ```ts
246
+ import { resumeSession } from '@factory/droid-sdk/node';
247
+
248
+ const session = await resumeSession('session-id');
249
+ ```
250
+
251
+ The saved session owns its working directory and settings. Resume options can
252
+ change handlers, transports, disabled tools, MCP servers, and observability,
253
+ but cannot replace saved model or working-directory settings.
254
+
255
+ ### Session state
256
+
257
+ ```ts
258
+ console.log(session.id);
259
+ console.log(session.cwd);
260
+ console.log(session.settings.modelId);
261
+ ```
262
+
263
+ `settings` and `cwd` update when the running session reports changes.
264
+
265
+ ### Update settings
266
+
267
+ ```ts
268
+ await session.updateSettings({
269
+ modelId: 'model-id',
270
+ reasoningEffort: ReasoningEffort.High,
271
+ autonomyLevel: AutonomyLevel.Low,
272
+ disabledToolIds: ['Execute'],
273
+ });
274
+ ```
275
+
276
+ Public settings include model, reasoning, interaction mode, autonomy, spec
277
+ mode settings, tags, mission settings, compaction settings, and disabled tools.
278
+
279
+ ### Rename a session
280
+
281
+ ```ts
282
+ await session.rename({ title: 'Authentication review' });
283
+ ```
284
+
285
+ ### Discover saved Node sessions
286
+
287
+ ```ts
288
+ import { listSessions } from '@factory/droid-sdk/node';
289
+
290
+ const sessions = await listSessions({
291
+ cwd: process.cwd(),
292
+ limit: 10,
293
+ });
294
+ ```
295
+
296
+ `listSessions()` reads local session storage. It does not call the Factory REST
297
+ API or a daemon.
298
+
299
+ ### Close a session
300
+
301
+ Call `close()` in `finally` when your code owns a Node session.
302
+
303
+ ```ts
304
+ const session = await createSession();
305
+
306
+ try {
307
+ // Use the session.
308
+ } finally {
309
+ await session.close();
310
+ }
311
+ ```
312
+
313
+ ## Streaming
314
+
315
+ ### Complete messages
316
+
317
+ Complete messages are the default.
318
+
319
+ ```ts
320
+ for await (const message of session.stream('Find the failing test.')) {
321
+ switch (message.type) {
322
+ case DroidMessageType.Assistant:
323
+ console.log(message.text);
324
+ break;
325
+ case DroidMessageType.ToolCall:
326
+ console.log(`Tool: ${message.name}`);
327
+ break;
328
+ case DroidMessageType.Result:
329
+ console.log(message.subtype);
330
+ break;
331
+ }
332
+ }
333
+ ```
334
+
335
+ Default message types:
336
+
337
+ | Type | Purpose |
338
+ | ------------- | ------------------------------------ |
339
+ | `assistant` | Complete assistant message |
340
+ | `user` | User message recorded by the session |
341
+ | `tool_call` | Complete tool request |
342
+ | `tool_result` | Complete tool result |
343
+ | `hook` | Hook execution |
344
+ | `error` | Runtime error event |
345
+ | `result` | Final turn result |
346
+
347
+ ### Partial events
348
+
349
+ Use partial events only when the application needs live text, thinking, tool
350
+ progress, usage, or state updates.
351
+
352
+ ```ts
353
+ for await (const event of session.stream('Explain the test failure.', {
354
+ includePartialMessages: true,
355
+ })) {
356
+ if (event.type === DroidMessageType.AssistantTextDelta) {
357
+ process.stdout.write(event.text);
358
+ }
359
+ }
360
+ ```
361
+
362
+ Partial streams can also include thinking deltas, tool-call deltas, tool
363
+ progress, token updates, permission results, settings changes, working-state
364
+ changes, MCP status, and mission events.
365
+
366
+ ### Stream concurrency
367
+
368
+ One session handle can have one active stream. Starting another active stream
369
+ throws `ConcurrentStreamError`.
370
+
371
+ Daemon sessions on the same connection may stream concurrently.
372
+
373
+ ### Stop consuming a stream
374
+
375
+ Breaking out of a stream early interrupts the active server turn.
376
+
377
+ ## Inputs and outputs
378
+
379
+ ### Images
380
+
381
+ ```ts
382
+ const result = await run('Describe this image.', {
383
+ images: [
384
+ {
385
+ type: 'base64',
386
+ data: base64Png,
387
+ mediaType: 'image/png',
388
+ },
389
+ ],
390
+ });
391
+ ```
392
+
393
+ Supported image types are JPEG, PNG, GIF, and WebP.
394
+
395
+ ### Documents
396
+
397
+ ```ts
398
+ for await (const _message of session.stream('Summarize this document.', {
399
+ files: [
400
+ {
401
+ type: 'text',
402
+ mediaType: 'text/plain',
403
+ data: report,
404
+ name: 'report.txt',
405
+ },
406
+ ],
407
+ })) {
408
+ // Consume messages.
409
+ }
410
+ ```
411
+
412
+ Document inputs support plain text and base64 PDF data.
413
+
414
+ ### Structured output
415
+
416
+ Use structured output when another system needs a predictable result.
417
+
418
+ ```ts
419
+ import { OutputFormatType, run } from '@factory/droid-sdk/node';
420
+
421
+ const result = await run('Review this function for correctness issues.', {
422
+ outputFormat: {
423
+ type: OutputFormatType.JsonSchema,
424
+ schema: {
425
+ type: 'object',
426
+ properties: {
427
+ summary: { type: 'string' },
428
+ findings: {
429
+ type: 'array',
430
+ items: {
431
+ type: 'object',
432
+ properties: {
433
+ line: { type: 'number' },
434
+ severity: {
435
+ type: 'string',
436
+ enum: ['low', 'medium', 'high'],
437
+ },
438
+ message: { type: 'string' },
439
+ },
440
+ required: ['line', 'severity', 'message'],
441
+ },
442
+ },
443
+ },
444
+ required: ['summary', 'findings'],
445
+ },
446
+ },
447
+ });
448
+
449
+ if (!result.success) {
450
+ throw new Error(
451
+ result.structuredOutputError?.message ??
452
+ result.error?.message ??
453
+ 'Structured output failed.'
454
+ );
455
+ }
456
+
457
+ console.log(result.structuredOutput);
458
+ ```
459
+
460
+ `structuredOutput` is typed as `unknown`. Validate or narrow it before passing
461
+ it to another system. A successful result can omit structured output.
462
+
463
+ ### Result metadata
464
+
465
+ ```ts
466
+ console.log(result.sessionId);
467
+ console.log(result.durationMs);
468
+ console.log(result.turnCount);
469
+ console.log(result.tokenUsage);
470
+ console.log(result.messages);
471
+ ```
472
+
473
+ `tokenUsage` is `null` when usage is unavailable.
474
+
475
+ ## Cancellation and interruption
476
+
477
+ ### Abort one turn
478
+
479
+ ```ts
480
+ const controller = new AbortController();
481
+
482
+ setTimeout(() => controller.abort(), 5_000);
483
+
484
+ for await (const message of session.stream('Perform a long review.', {
485
+ abortSignal: controller.signal,
486
+ })) {
487
+ // Handle messages.
488
+ }
489
+ ```
490
+
491
+ The stream throws the abort reason after interrupting the active turn.
492
+
493
+ ### Interrupt and keep the session
494
+
495
+ ```ts
496
+ await session.interrupt();
497
+ ```
498
+
499
+ `interrupt()` stops the active turn on the server. The stream continues to its
500
+ terminal result, normally with subtype `interrupted`. The session remains
501
+ usable for later prompts.
502
+
503
+ A session-level abort signal passed to `createSession()` closes the whole
504
+ session when aborted.
505
+
506
+ ## Permissions and user input
507
+
508
+ ### Autonomy
509
+
510
+ ```ts
511
+ import { AutonomyLevel } from '@factory/droid-sdk/node';
512
+
513
+ const session = await createSession({
514
+ autonomyLevel: AutonomyLevel.Off,
515
+ });
516
+ ```
517
+
518
+ Levels are `Off`, `Low`, `Medium`, and `High`. When omitted, Droid settings
519
+ choose the default.
520
+
521
+ ### Permission handler
522
+
523
+ ```ts
524
+ import {
525
+ ToolConfirmationOutcome,
526
+ ToolConfirmationType,
527
+ } from '@factory/droid-sdk/node';
528
+
529
+ const session = await createSession({
530
+ autonomyLevel: AutonomyLevel.Off,
531
+ permissionHandler(request) {
532
+ const safe =
533
+ request.toolUses.length > 0 &&
534
+ request.toolUses.every(
535
+ (use) => use.details.type === ToolConfirmationType.Create
536
+ );
537
+
538
+ return safe
539
+ ? ToolConfirmationOutcome.ProceedOnce
540
+ : ToolConfirmationOutcome.Cancel;
541
+ },
542
+ });
543
+ ```
544
+
545
+ The handler must return an outcome supplied by the request. Invalid results and
546
+ handler errors cancel the request.
547
+
548
+ Without a handler, permission requests are cancelled.
549
+
550
+ ### AskUser handler
551
+
552
+ ```ts
553
+ const result = await run('Ask me which environment to deploy.', {
554
+ askUserHandler(request) {
555
+ return {
556
+ answers: request.questions.map((question) => ({
557
+ index: question.index,
558
+ question: question.question,
559
+ answer: question.options[0] ?? 'none',
560
+ })),
561
+ };
562
+ },
563
+ });
564
+ ```
565
+
566
+ To decline:
567
+
568
+ ```ts
569
+ return { cancelled: true, answers: [] };
570
+ ```
571
+
572
+ Without a handler, AskUser requests are declined.
573
+
574
+ ### Disable tools
575
+
576
+ ```ts
577
+ const session = await createSession({
578
+ disabledToolIds: ['Execute'],
579
+ });
580
+ ```
581
+
582
+ Tool IDs are strings. Use `listTools()` to inspect the tools available to the
583
+ current session.
584
+
585
+ ```ts
586
+ const tools = await session.listTools();
587
+
588
+ for (const tool of tools) {
589
+ console.log(tool.id, tool.allowed);
590
+ }
591
+ ```
592
+
593
+ The public SDK provides subtractive `disabledToolIds`. It does not expose a
594
+ restrictive allowlist.
595
+
596
+ ## Tools and extensions
597
+
598
+ ### Skills
599
+
600
+ ```ts
601
+ const { skills } = await session.listSkills();
602
+
603
+ await session.setSkillDisabled({
604
+ skillName: 'skill-name',
605
+ disabled: true,
606
+ settingsLevel: 'project',
607
+ });
608
+ ```
609
+
610
+ Skills may come from project, personal, built-in, or automation settings.
611
+
612
+ ### SDK MCP tools
613
+
614
+ **Runtime: Node**
615
+
616
+ Use an SDK MCP server to define an in-process tool with a typed Zod input.
617
+
618
+ ```ts
619
+ import { z } from 'zod';
620
+ import {
621
+ createSdkMcpServer,
622
+ createSession,
623
+ tool,
624
+ } from '@factory/droid-sdk/node';
625
+
626
+ const tools = createSdkMcpServer({
627
+ name: 'review-tools',
628
+ tools: [
629
+ tool(
630
+ 'lookup_owner',
631
+ 'Returns the owner of a file',
632
+ { path: z.string() },
633
+ ({ path }) => `Owner for ${path}: platform-team`
634
+ ),
635
+ ],
636
+ });
637
+
638
+ const session = await createSession({
639
+ mcpServers: [tools],
640
+ });
641
+ ```
642
+
643
+ The SDK starts an authenticated loopback MCP server and closes it with the
644
+ session.
645
+
646
+ ### External MCP servers
647
+
648
+ Node sessions can add, remove, toggle, list, and authenticate MCP servers.
649
+
650
+ ```ts
651
+ await session.addMcpServer({
652
+ name: 'docs',
653
+ type: 'http',
654
+ url: 'https://example.com/mcp',
655
+ });
656
+
657
+ const servers = await session.listMcpServers();
658
+ const tools = await session.listMcpTools();
659
+ ```
660
+
661
+ The daemon client exposes the complete MCP management API through `droid.mcp`.
662
+
663
+ ### Hooks
664
+
665
+ Hooks are configured in `.factory/hooks.json`. They can run before or after
666
+ tools, when prompts are submitted, when notifications arrive, during
667
+ compaction, and when sessions or subagents stop.
668
+
669
+ Hook events are available in the stream:
670
+
671
+ ```ts
672
+ for await (const message of session.stream('Run the tests.')) {
673
+ if (message.type === DroidMessageType.Hook) {
674
+ console.log(message.status, message.command);
675
+ }
676
+ }
677
+ ```
678
+
679
+ The SDK exposes hook schemas and types, but does not provide a programmatic
680
+ hook-registration API.
681
+
682
+ ### Raw notifications
683
+
684
+ **Runtime: Node**
685
+
686
+ ```ts
687
+ const unsubscribe = session.onNotification(
688
+ (notification) => console.log(notification),
689
+ { type: 'settings_updated' }
690
+ );
691
+
692
+ unsubscribe();
693
+ ```
694
+
695
+ Use raw notifications only when the typed stream does not expose the event you
696
+ need.
697
+
698
+ ## Session lifecycle
699
+
700
+ ### Node replacement handles
701
+
702
+ Node `fork()` returns a ready successor handle. `compact()` and `rewind()`
703
+ return outcome objects containing a ready successor in `session`. Each
704
+ operation retires its source wrapper.
705
+
706
+ ```ts
707
+ const forked = await session.fork();
708
+
709
+ const { session: compacted, removedCount } = await forked.compact();
710
+
711
+ const {
712
+ session: rewound,
713
+ restoredCount,
714
+ deletedCount,
715
+ } = await compacted.rewind({
716
+ messageId,
717
+ filesToRestore,
718
+ filesToDelete,
719
+ forkTitle: 'Before the failed change',
720
+ });
721
+ ```
722
+
723
+ After a successful replacement, active operations on the source wrapper throw
724
+ `SessionReplacedError`. Its `id`, `settings`, and `cwd` remain readable, and
725
+ `close()` is a no-op. The persisted source session can still be loaded later
726
+ with `resumeSession(sourceId)`.
727
+
728
+ Do not replace a session while it has an active stream.
729
+
730
+ ### Daemon replacement IDs
731
+
732
+ Daemon operations return a new session ID. The source handle remains usable.
733
+
734
+ ```ts
735
+ const { newSessionId } = await source.compact();
736
+ const compacted = await droid.sessions.resume(newSessionId);
737
+ ```
738
+
739
+ The same rule applies to `fork()` and `rewind()`.
740
+
741
+ ```ts
742
+ const forkResult = await source.fork();
743
+ const fork = await droid.sessions.resume(forkResult.newSessionId);
744
+
745
+ const rewindResult = await source.rewind({
746
+ messageId,
747
+ filesToRestore,
748
+ filesToDelete,
749
+ forkTitle: 'Before the failed change',
750
+ });
751
+ const rewound = await droid.sessions.resume(rewindResult.newSessionId);
752
+ ```
753
+
754
+ Daemon replacement locking is local to one client handle. Avoid replacing or
755
+ streaming the same session through several daemon clients at once.
756
+
757
+ ## Spec mode
758
+
759
+ Create a session in spec mode:
760
+
761
+ ```ts
762
+ import {
763
+ createSession,
764
+ DroidInteractionMode,
765
+ ReasoningEffort,
766
+ } from '@factory/droid-sdk/node';
767
+
768
+ const session = await createSession({
769
+ interactionMode: DroidInteractionMode.Spec,
770
+ specModeReasoningEffort: ReasoningEffort.High,
771
+ });
772
+ ```
773
+
774
+ Enter spec mode later:
775
+
776
+ ```ts
777
+ await session.enterSpecMode({
778
+ specModeReasoningEffort: ReasoningEffort.High,
779
+ });
780
+ ```
781
+
782
+ When Droid asks to leave spec mode, a permission handler can approve
783
+ implementation in the same session or hand it to a new session.
784
+
785
+ | Outcome | Behavior |
786
+ | ----------------------- | --------------------------------------------------- |
787
+ | `ProceedOnce` | Continue in the same session |
788
+ | `ProceedNewSessionHigh` | Start implementation in a new high-autonomy session |
789
+
790
+ New-session handoff IDs currently require inspecting raw notifications.
791
+
792
+ ## Daemon and browser applications
793
+
794
+ ### Connect
795
+
796
+ ```ts
797
+ const droid = await connectToDaemon({
798
+ url: 'ws://127.0.0.1:37643',
799
+ auth: { apiKey },
800
+ permissionHandler,
801
+ askUserHandler,
802
+ onError(error) {
803
+ console.error(error);
804
+ },
805
+ });
806
+ ```
807
+
808
+ Connection-level handlers become defaults for created and resumed sessions.
809
+ Per-session handlers override them.
810
+
811
+ ### Create and resume
812
+
813
+ ```ts
814
+ const created = await droid.sessions.create({
815
+ cwd: '/path/to/repository',
816
+ });
817
+
818
+ const resumed = await droid.sessions.resume('session-id');
819
+ ```
820
+
821
+ One `ConnectedDroid` can have one attached handle for each session ID.
822
+
823
+ ### List and inspect history
824
+
825
+ ```ts
826
+ const sessions = await droid.sessions.list({ limit: 20 });
827
+
828
+ const messages = await droid.sessions.getMessages(sessions[0].id, {
829
+ limit: 50,
830
+ });
831
+
832
+ const matches = await droid.sessions.search({
833
+ query: 'authentication failure',
834
+ });
835
+ ```
836
+
837
+ The daemon session resource also supports opened-session listing,
838
+ archive/unarchive, rename, settings, context information, and queued-message
839
+ operations.
840
+
841
+ ### Concurrent sessions
842
+
843
+ Different daemon sessions can stream over one connection at the same time.
844
+
845
+ ```ts
846
+ await Promise.all([
847
+ consume(first.stream('Review the API.')),
848
+ consume(second.stream('Review the tests.')),
849
+ ]);
850
+ ```
851
+
852
+ ### Cleanup
853
+
854
+ | Operation | Daemon session | Local handle | Connection |
855
+ | -------------------- | -------------- | ---------------------- | ---------- |
856
+ | `session.detach()` | Keeps running | Detached | Retained |
857
+ | `session.close()` | Ends | Detached after success | Retained |
858
+ | `droid.disconnect()` | Keeps running | All detached | Destroyed |
859
+
860
+ Detached handles cannot be reused. Resume the session to obtain a new handle.
861
+
862
+ ### Stable daemon resources
863
+
864
+ | Namespace | Main methods |
865
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
866
+ | `sessions` | `list`, `listOpened`, `create`, `resume`, `getMessages`, `search`, `archive`, `unarchive`, `rename`, `updateSettings`, `resolveQueuedMessage`, `killWorker`, `getRewindInfo`, `rewind`, `compact`, `fork`, `getContextBreakdown` |
867
+ | `workspace` | `changeDirectory`, `validateDirectory`, `checkTrust`, `trust`, `listFiles`, `searchFiles`, `getFileContent` |
868
+ | `settings` | `getDefaults`, `updateDefaults` |
869
+ | `customModels` | `list`, `upsert`, `delete` |
870
+ | `ssh` | `installKey` |
871
+ | `updates` | `trigger` |
872
+ | `relay` | `start`, `stop`, `status` |
873
+ | `terminals` | `create`, `write`, `resize`, `close`, `list` |
874
+ | `mcp` | `getConfig`, `updateConfig`, `addServer`, `removeServer`, `toggleServer`, authentication, registry, and tool methods |
875
+ | `skills` | `list`, `setDisabled` |
876
+ | `commands` | `list` |
877
+ | `plugins` | `listAvailable`, `listInstalled`, `install`, `uninstall`, `setEnabled`, `update` |
878
+ | `marketplaces` | `list`, `add`, `remove`, `update` |
879
+ | `automations` | `list`, `run`, `pause`, `resume`, history, visual, create, update, rename, delete, fork, and config methods |
880
+ | `git` | `getDiff`, `listBranches`, `checkoutBranch`, `push`, `commit`, `createPullRequest`, `resolvePullRequestStatuses` |
881
+ | `feedback` | `submitBugReport` |
882
+
883
+ ### Facade return shapes
884
+
885
+ The namespaced daemon facade normalizes common collection results:
886
+
887
+ - `sessions.list()` returns session summaries with `id`, `messageCount`, and
888
+ `modifiedTime`.
889
+ - `sessions.getMessages()` returns the message array directly.
890
+ - Collection methods such as `customModels.list()`, `terminals.list()`,
891
+ `mcp.listRegistry()`, `mcp.listTools()`, `commands.list()`, and the plugin,
892
+ marketplace, automation, and Software Factory list methods return arrays.
893
+ - Methods whose protocol result includes additional metadata retain the result
894
+ object. This includes `sessions.search()`, `mcp.listServers()`, and
895
+ `skills.list()`.
896
+
897
+ The low-level `DaemonClient` returns canonical protocol response types instead
898
+ of these normalized facade shapes.
899
+
900
+ ## Observability
901
+
902
+ **Runtime: Node**
903
+
904
+ ```ts
905
+ const session = await createSession({
906
+ observability: {
907
+ logger: {
908
+ log(event) {
909
+ console.log(event.level, event.message);
910
+ },
911
+ },
912
+ metrics: {
913
+ record(event) {
914
+ console.log(event.name, event.value);
915
+ },
916
+ },
917
+ tracing: {
918
+ inject(carrier) {
919
+ carrier.traceparent = currentTraceparent;
920
+ },
921
+ },
922
+ },
923
+ });
924
+ ```
925
+
926
+ Observability sinks must not throw into SDK operations. The SDK avoids sending
927
+ prompt text, message content, tool inputs, raw output, and stack traces through
928
+ its observability contracts.
929
+
930
+ ## Advanced resources
931
+
932
+ ### Low-level daemon integration
933
+
934
+ Most applications should use `connectToDaemon()`. The root entrypoint also
935
+ exports `DaemonClient`, `DaemonSessionController`, `MultiSessionStateManager`,
936
+ `MultiMissionStateManager`, `WebSocketDaemonTransport`, daemon events, and
937
+ canonical protocol types for hosts that need custom connection or state
938
+ ownership.
939
+
940
+ Node-only IPC and in-process transports come from the `/node` entrypoint:
941
+
942
+ ```ts
943
+ import { DaemonClient, MachineType } from '@factory/droid-sdk';
944
+ import {
945
+ InProcessDaemonClientTransport,
946
+ type InProcessDaemonClientTransportOptions,
947
+ type InProcessMessageHandler,
948
+ } from '@factory/droid-sdk/node';
949
+
950
+ function createInProcessClient(
951
+ sendMessage: InProcessMessageHandler,
952
+ onMessage: NonNullable<InProcessDaemonClientTransportOptions['onMessage']>
953
+ ) {
954
+ const transport = new InProcessDaemonClientTransport({
955
+ sendMessage,
956
+ onMessage,
957
+ });
958
+
959
+ return new DaemonClient({
960
+ machineType: MachineType.Local,
961
+ transport,
962
+ });
963
+ }
964
+ ```
965
+
966
+ The root also exports `WebSocketDaemonTransport`; the `/node` entrypoint exports
967
+ `IpcDaemonClientTransport`.
968
+
969
+ Hosts can inject daemon-core logging, metrics, tracing, and domain helpers once
970
+ at startup:
971
+
972
+ ```ts
973
+ import { configureDaemonCoreDeps } from '@factory/droid-sdk';
974
+
975
+ configureDaemonCoreDeps({
976
+ logger: {
977
+ info: (message, metadata) => logger.info(message, metadata),
978
+ error: (message, metadata) => logger.error(message, metadata),
979
+ },
980
+ });
981
+ ```
982
+
983
+ Unspecified dependencies keep their standalone defaults.
984
+
985
+ ### Low-level Node integration
986
+
987
+ Most Node applications should use `run()` or `DroidSession`. The `/node`
988
+ entrypoint also exports `ProcessTransport` and `DroidClient` for direct
989
+ JSON-RPC integrations.
990
+
991
+ Low-level RPC methods return complete response envelopes. Read successful
992
+ payloads from `response.result`. Direct permission and AskUser handlers receive
993
+ complete request events, while high-level session handlers receive request
994
+ parameters.
995
+
996
+ `ProcessTransport` uses `droidExecPath` and `droidExecExtraArgs` for executable
997
+ configuration. A custom `StringFramedDroidClientTransport` exchanges
998
+ newline-delimited JSON-RPC strings and can replace subprocess transport
999
+ entirely.
1000
+
1001
+ ### Context and token usage
1002
+
1003
+ Read the current context-window estimate:
1004
+
1005
+ ```ts
1006
+ const stats = await session.getContextStats();
1007
+
1008
+ console.log(stats.used, stats.remaining, stats.limit, stats.accuracy);
1009
+ ```
1010
+
1011
+ Enable partial messages to receive live token updates:
1012
+
1013
+ ```ts
1014
+ for await (const message of session.stream('Summarize this repository.', {
1015
+ includePartialMessages: true,
1016
+ })) {
1017
+ if (message.type === DroidMessageType.TokenUsageUpdate) {
1018
+ console.log(message.inputTokens, message.outputTokens);
1019
+ }
1020
+ }
1021
+ ```
1022
+
1023
+ The final result's `tokenUsage` contains the last observed totals or `null`.
1024
+
1025
+ ### Factory REST API
1026
+
1027
+ The browser-safe root exports REST helpers for Factory computers, machine
1028
+ templates, metrics, dependency installation, and remote-session listing.
1029
+
1030
+ ```ts
1031
+ import { listComputers, listRemoteSessions } from '@factory/droid-sdk';
1032
+
1033
+ const options = { apiKey: process.env.FACTORY_API_KEY! };
1034
+
1035
+ const computers = await listComputers(options);
1036
+ const sessions = await listRemoteSessions(options);
1037
+ ```
1038
+
1039
+ REST calls use `https://api.factory.ai` by default. Pass `baseUrl` to target a
1040
+ different Factory API host.
1041
+
1042
+ | Resource | Functions |
1043
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
1044
+ | Machine templates | `listMachineTemplates`, `getMachineTemplate` |
1045
+ | Computers | `listComputers`, `getComputer`, `getComputerByName`, `createComputer`, `updateComputer`, `deleteComputer`, `restartComputer`, `refreshComputer` |
1046
+ | Computer operations | `getComputerMetrics`, `retryInstallDeps` |
1047
+ | Remote sessions | `listRemoteSessions` |
1048
+
1049
+ ### Unstable daemon resources
1050
+
1051
+ APIs under `droid.unstable` may change between SDK releases.
1052
+
1053
+ | Namespace | Main methods or resources |
1054
+ | ----------------- | ------------------------------------------------------------------------- |
1055
+ | `crons` | `list`, `create`, `update`, `delete`, `holdSession`, `resumeSession` |
1056
+ | `missions` | `inspectReadiness`, `acknowledgeReadinessWarning` |
1057
+ | `semanticDiff` | `getCache`, `saveCache`, `generate` |
1058
+ | `fileTransfers` | `push`, `pull` |
1059
+ | `proxy` | `getToken` |
1060
+ | `softwareFactory` | `workstreams`, `signals`, `changes`, `activities`, and `events` resources |
1061
+
1062
+ Keep unstable API use behind an application-owned adapter.
1063
+
1064
+ For example, create and manage a scheduled prompt through the unstable cron
1065
+ resource:
1066
+
1067
+ ```ts
1068
+ const cron = await droid.unstable.crons.create({
1069
+ kind: 'root_prompt',
1070
+ scope: { type: 'root' },
1071
+ schedule: { expression: '0 * * * *', recurring: true },
1072
+ payload: {
1073
+ type: 'prompt',
1074
+ prompt: 'Check repository health',
1075
+ target: { type: 'new_session', cwd: '/workspace' },
1076
+ },
1077
+ });
1078
+
1079
+ await droid.unstable.crons.update(cron.id, { status: 'paused' });
1080
+ await droid.unstable.crons.delete(cron.id);
1081
+ ```
1082
+
1083
+ ## API reference
1084
+
1085
+ ### Node functions
1086
+
1087
+ | API | Returns |
1088
+ | ------------------------------------ | ---------------------------- |
1089
+ | `run(prompt, options?)` | `Promise<DroidResult>` |
1090
+ | `createSession(options?)` | `Promise<DroidSession>` |
1091
+ | `resumeSession(sessionId, options?)` | `Promise<DroidSession>` |
1092
+ | `listSessions(options?)` | `Promise<SessionMetadata[]>` |
1093
+ | `createSdkMcpServer(options)` | `SdkMcpServer` |
1094
+ | `tool(...)` | `DroidTool` |
1095
+
1096
+ ### `DroidSession`
1097
+
1098
+ | Member | Purpose |
1099
+ | ------------------------- | ------------------------------------- |
1100
+ | `id` | Session ID |
1101
+ | `cwd` | Live working directory |
1102
+ | `settings` | Live read-only settings |
1103
+ | `stream()` | Run one turn |
1104
+ | `interrupt()` | Stop the active turn |
1105
+ | `updateSettings()` | Update session settings |
1106
+ | `enterSpecMode()` | Change to spec mode |
1107
+ | `listTools()` | List normalized tools |
1108
+ | `listSkills()` | List skills |
1109
+ | `setSkillDisabled()` | Enable or disable a skill |
1110
+ | `addMcpServer()` | Add an MCP server |
1111
+ | `removeMcpServer()` | Remove an MCP server |
1112
+ | `toggleMcpServer()` | Enable or disable an MCP server |
1113
+ | `listMcpServers()` | List MCP servers |
1114
+ | `listMcpTools()` | List MCP tools |
1115
+ | `authenticateMcpServer()` | Start MCP authentication |
1116
+ | `getContextStats()` | Read context usage |
1117
+ | `getRewindInfo()` | Inspect rewindable file changes |
1118
+ | `rewind()` | Create a rewound successor |
1119
+ | `compact()` | Create a compacted successor |
1120
+ | `fork()` | Create a forked successor |
1121
+ | `rename()` | Change the session title |
1122
+ | `onNotification()` | Subscribe to raw notifications |
1123
+ | `close()` | Close the session and owned resources |
1124
+
1125
+ ### Result states
1126
+
1127
+ | Subtype | `success` | `interrupted` | Meaning |
1128
+ | ------------------------- | --------- | ------------- | ------------------------- |
1129
+ | `success` | `true` | `false` | Turn completed |
1130
+ | `interrupted` | `false` | `true` | Turn was stopped |
1131
+ | `error_during_execution` | `false` | `false` | Runtime failure |
1132
+ | `error_structured_output` | `false` | `false` | Structured-output failure |
1133
+
1134
+ Common result fields:
1135
+
1136
+ ```ts
1137
+ {
1138
+ type: 'result';
1139
+ subtype:
1140
+ | 'success'
1141
+ | 'interrupted'
1142
+ | 'error_during_execution'
1143
+ | 'error_structured_output';
1144
+ sessionId: string;
1145
+ durationMs: number;
1146
+ tokenUsage: TokenUsage | null;
1147
+ messages: DroidStreamEvent[];
1148
+ text: string;
1149
+ turnCount: number;
1150
+ success: boolean;
1151
+ interrupted: boolean;
1152
+ structuredOutput?: unknown;
1153
+ structuredOutputError?: StructuredOutputError | null;
1154
+ error: ErrorEvent | null;
1155
+ }
1156
+ ```
1157
+
1158
+ ### Main enums
1159
+
1160
+ | Enum | Values |
1161
+ | ---------------------- | -------------------------------------------------------------------------------- |
1162
+ | `AutonomyLevel` | `Off`, `Low`, `Medium`, `High` |
1163
+ | `DroidInteractionMode` | `Auto`, `Spec`, `Mission`; `AGI` is deprecated |
1164
+ | `ReasoningEffort` | `None`, `Dynamic`, `Off`, `Minimal`, `Low`, `Medium`, `High`, `ExtraHigh`, `Max` |
1165
+ | `OutputFormatType` | `JsonSchema` |
1166
+
1167
+ Model IDs and tool IDs are strings. Discover available models and tools at
1168
+ runtime instead of treating them as closed enums.
1169
+
1170
+ ### Main errors
1171
+
1172
+ Universal errors:
1173
+
1174
+ | Error | Meaning |
1175
+ | -------------------------- | --------------------------------------------- |
1176
+ | `ConcurrentStreamError` | A session handle already has an active stream |
1177
+ | `SessionReplacedError` | A Node source wrapper was replaced |
1178
+ | `SessionReplacementError` | Successor loading or rollback failed |
1179
+ | `AbortError` | A daemon operation was aborted |
1180
+ | `ConnectionClosedError` | Daemon connection closed |
1181
+ | `JsonRpcRequestError` | Daemon JSON-RPC request failed |
1182
+ | `RelayConnectionError` | Relay connection failed |
1183
+ | `WebSocketConnectionError` | WebSocket connection failed |
1184
+
1185
+ Node-only errors:
1186
+
1187
+ | Error | Meaning |
1188
+ | ------------------------ | -------------------------------------------- |
1189
+ | `DroidClientError` | Base Node client error |
1190
+ | `ConnectionError` | Droid process or transport connection failed |
1191
+ | `TimeoutError` | A request timed out |
1192
+ | `ProtocolError` | Protocol or API response failed |
1193
+ | `SessionError` | Base session error |
1194
+ | `SessionNotFoundError` | Saved session was not found |
1195
+ | `InvalidSessionCwdError` | Saved working directory is invalid |
1196
+ | `ProcessExitError` | Droid subprocess exited unexpectedly |
1197
+
1198
+ ### Custom transports
1199
+
1200
+ **Runtime: Node, advanced**
1201
+
1202
+ ```ts
1203
+ interface StringFramedDroidClientTransport {
1204
+ isConnected: boolean;
1205
+ send(message: string): Promise<void>;
1206
+ onMessage(handler: (message: string) => void): void;
1207
+ onError(handler: (error: Error) => void): void;
1208
+ close(): Promise<void>;
1209
+ }
1210
+ ```
1211
+
1212
+ Pass a ready transport through `run()`, `createSession()`, or
1213
+ `resumeSession()`. A custom transport bypasses subprocess creation and the
1214
+ Node API-key requirement.
1215
+
1216
+ ## Errors and limitations
1217
+
1218
+ ### Handle results explicitly
1219
+
1220
+ ```ts
1221
+ const result = await run('Run the test suite.');
1222
+
1223
+ switch (result.subtype) {
1224
+ case 'success':
1225
+ console.log(result.text);
1226
+ break;
1227
+ case 'interrupted':
1228
+ console.log('The run was interrupted.');
1229
+ break;
1230
+ case 'error_during_execution':
1231
+ case 'error_structured_output':
1232
+ console.error(
1233
+ result.structuredOutputError?.message ??
1234
+ result.error?.message ??
1235
+ 'Unknown error'
1236
+ );
1237
+ break;
1238
+ }
1239
+ ```
1240
+
1241
+ ### Known limitations
1242
+
1243
+ - Daemon replacement locking does not coordinate separate clients.
1244
+ - Browser daemon authentication currently requires an API key.
1245
+ - New-session spec handoff IDs require raw notification inspection.
1246
+ - REST remote-session support currently provides listing only.
1247
+
1248
+ ## Runnable examples
1249
+
1250
+ The package includes runnable examples for one-shot runs, streaming, sessions,
1251
+ permissions, AskUser, attachments, structured output, MCP tools, hooks,
1252
+ observability, spec mode, replacement lifecycle, daemon sessions, concurrent
1253
+ browser sessions, and daemon lifecycle operations.
1254
+
1255
+ All examples are under:
1256
+
1257
+ ```text
1258
+ examples/node/
1259
+ examples/browser/
1260
+ ```