@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,95 @@
1
+ /**
2
+ * Forks, compacts, and rewinds a daemon session.
3
+ *
4
+ * Daemon operations return a new session ID. The source session stays usable,
5
+ * and the successor must be resumed explicitly.
6
+ *
7
+ * Usage:
8
+ * npm run build
9
+ * FACTORY_API_KEY=... DROID_SDK_EXAMPLE_CWD=/repo npm run serve:browser-example
10
+ * open http://127.0.0.1:8420/?example=daemon-lifecycle
11
+ */
12
+
13
+ import {
14
+ connectToDaemon,
15
+ DroidMessageType,
16
+ type ConnectedDroidSession,
17
+ } from '@factory/droid-sdk';
18
+
19
+ const { daemonUrl, apiKey, cwd } = (
20
+ globalThis as typeof globalThis & {
21
+ __DROID_SDK_EXAMPLE_CONFIG__: {
22
+ daemonUrl: string;
23
+ apiKey: string;
24
+ cwd: string;
25
+ };
26
+ }
27
+ ).__DROID_SDK_EXAMPLE_CONFIG__;
28
+ if (!apiKey || !cwd) {
29
+ throw new Error('Set FACTORY_API_KEY and DROID_SDK_EXAMPLE_CWD.');
30
+ }
31
+
32
+ const droid = await connectToDaemon({
33
+ url: daemonUrl,
34
+ auth: { apiKey },
35
+ });
36
+
37
+ async function send(
38
+ session: ConnectedDroidSession,
39
+ prompt: string
40
+ ): Promise<string> {
41
+ let messageId: string | undefined;
42
+
43
+ for await (const message of session.stream(prompt)) {
44
+ if (
45
+ message.type === DroidMessageType.User &&
46
+ message.message.visibility !== 'llm_only'
47
+ ) {
48
+ messageId ??= message.message.id;
49
+ }
50
+ if (message.type === DroidMessageType.Assistant) {
51
+ console.log(message.text);
52
+ }
53
+ }
54
+
55
+ if (!messageId) throw new Error('The turn produced no user message ID.');
56
+ return messageId;
57
+ }
58
+
59
+ const sessions: ConnectedDroidSession[] = [];
60
+
61
+ try {
62
+ const source = await droid.sessions.create({ cwd });
63
+ sessions.push(source);
64
+
65
+ await send(source, 'Remember this phrase exactly: mango sunrise');
66
+ const secondTurn = await send(
67
+ source,
68
+ 'Now forget it and remember: cobalt harbor'
69
+ );
70
+
71
+ const { newSessionId: forkedId } = await source.fork();
72
+ const forked = await droid.sessions.resume(forkedId);
73
+ sessions.push(forked);
74
+ console.log(`Forked session: ${forked.id}`);
75
+
76
+ const { newSessionId: compactedId } = await source.compact();
77
+ const compacted = await droid.sessions.resume(compactedId);
78
+ sessions.push(compacted);
79
+ console.log(`Compacted session: ${compacted.id}`);
80
+
81
+ const { newSessionId: rewoundId } = await source.rewind({
82
+ messageId: secondTurn,
83
+ filesToRestore: [],
84
+ filesToDelete: [],
85
+ forkTitle: 'Before the second phrase',
86
+ });
87
+ const rewound = await droid.sessions.resume(rewoundId);
88
+ sessions.push(rewound);
89
+ console.log(`Rewound session: ${rewound.id}`);
90
+
91
+ await send(source, 'Say "source still works" and nothing else.');
92
+ } finally {
93
+ await Promise.allSettled(sessions.map((session) => session.close()));
94
+ droid.disconnect();
95
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Creates two sessions concurrently over one daemon connection.
3
+ *
4
+ * Runs in a real browser (no `process`, no Node builtins). From a source
5
+ * checkout of this repository:
6
+ * npm run build
7
+ * FACTORY_API_KEY=... DROID_SDK_EXAMPLE_CWD=/repo npm run serve:browser-example
8
+ * open http://127.0.0.1:8420/?example=daemon-multi-session
9
+ *
10
+ * Never commit a real API key or deploy it in browser code.
11
+ */
12
+
13
+ import {
14
+ connectToDaemon,
15
+ DroidMessageType,
16
+ type ConnectedDroidSession,
17
+ } from '@factory/droid-sdk';
18
+
19
+ const { daemonUrl, apiKey, cwd } = (
20
+ globalThis as typeof globalThis & {
21
+ __DROID_SDK_EXAMPLE_CONFIG__: {
22
+ daemonUrl: string;
23
+ apiKey: string;
24
+ cwd: string;
25
+ };
26
+ }
27
+ ).__DROID_SDK_EXAMPLE_CONFIG__;
28
+ if (!apiKey || !cwd) {
29
+ throw new Error('Set FACTORY_API_KEY and DROID_SDK_EXAMPLE_CWD.');
30
+ }
31
+
32
+ const droid = await connectToDaemon({
33
+ url: daemonUrl,
34
+ auth: { apiKey },
35
+ });
36
+
37
+ async function runTurn(
38
+ session: ConnectedDroidSession,
39
+ label: string,
40
+ prompt: string
41
+ ): Promise<void> {
42
+ for await (const message of session.stream(prompt)) {
43
+ if (message.type === DroidMessageType.Assistant) {
44
+ console.log(`${label}: ${message.text}`);
45
+ }
46
+ }
47
+ }
48
+
49
+ const sessions: ConnectedDroidSession[] = [];
50
+ try {
51
+ const first = await droid.sessions.create({ cwd });
52
+ sessions.push(first);
53
+ const second = await droid.sessions.create({ cwd });
54
+ sessions.push(second);
55
+
56
+ console.log(`First session: ${first.id}`);
57
+ console.log(`Second session: ${second.id}`);
58
+
59
+ await Promise.all([
60
+ runTurn(first, 'First', 'Say "one" and nothing else.'),
61
+ runTurn(second, 'Second', 'Say "two" and nothing else.'),
62
+ ]);
63
+ } finally {
64
+ await Promise.allSettled(sessions.map((session) => session.close()));
65
+ droid.disconnect();
66
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Daemon connection from a browser: run a turn and approve its tool use.
3
+ *
4
+ * Waits for the turn to end rather than for the permission request, since a
5
+ * turn can finish without asking.
6
+ *
7
+ * Runs in a real browser (no `process`, no Node builtins). From a source
8
+ * checkout of this repository:
9
+ * npm run build
10
+ * FACTORY_API_KEY=... DROID_SDK_EXAMPLE_CWD=/repo npm run serve:browser-example
11
+ * open http://127.0.0.1:8420/?example=daemon-session
12
+ *
13
+ * Notes:
14
+ * - If your page is served over https://, the browser will block ws:// URLs as
15
+ * mixed content. Use wss:// (or serve the page over http:// for localhost).
16
+ * - Never commit a real API key or deploy it in browser code.
17
+ */
18
+
19
+ import {
20
+ AutonomyLevel,
21
+ connectToDaemon,
22
+ DroidMessageType,
23
+ ToolConfirmationOutcome,
24
+ type ConnectedDroidSession,
25
+ } from '@factory/droid-sdk';
26
+
27
+ const { daemonUrl, apiKey, cwd } = (
28
+ globalThis as typeof globalThis & {
29
+ __DROID_SDK_EXAMPLE_CONFIG__: {
30
+ daemonUrl: string;
31
+ apiKey: string;
32
+ cwd: string;
33
+ };
34
+ }
35
+ ).__DROID_SDK_EXAMPLE_CONFIG__;
36
+ if (!apiKey || !cwd) {
37
+ throw new Error('Set FACTORY_API_KEY and DROID_SDK_EXAMPLE_CWD.');
38
+ }
39
+
40
+ const droid = await connectToDaemon({
41
+ url: daemonUrl,
42
+ auth: { apiKey },
43
+ });
44
+
45
+ let approvedPermission = false;
46
+ let session: ConnectedDroidSession | undefined;
47
+
48
+ try {
49
+ session = await droid.sessions.create({
50
+ cwd,
51
+ // Off ensures even low-risk commands exercise the permission handler.
52
+ autonomyLevel: AutonomyLevel.Off,
53
+ permissionHandler: () => {
54
+ approvedPermission = true;
55
+ return ToolConfirmationOutcome.ProceedOnce;
56
+ },
57
+ });
58
+
59
+ for await (const message of session.stream(
60
+ 'Run `echo hello from the browser example` and show me the output.'
61
+ )) {
62
+ if (message.type === DroidMessageType.Assistant) {
63
+ console.log(message.text);
64
+ }
65
+ }
66
+
67
+ if (!approvedPermission) {
68
+ throw new Error('Turn finished without requesting permission.');
69
+ }
70
+ } finally {
71
+ await Promise.allSettled(session ? [session.close()] : []);
72
+ droid.disconnect();
73
+ }
@@ -0,0 +1,68 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Droid SDK browser examples</title>
6
+ <style>
7
+ body {
8
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
9
+ margin: 2rem;
10
+ line-height: 1.5;
11
+ }
12
+ pre {
13
+ background: #f4f4f5;
14
+ padding: 0.75rem;
15
+ overflow-x: auto;
16
+ white-space: pre-wrap;
17
+ }
18
+ #error:empty {
19
+ display: none;
20
+ }
21
+ #error {
22
+ background: #fee2e2;
23
+ padding: 0.75rem;
24
+ white-space: pre-wrap;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <h1>Droid SDK browser examples</h1>
30
+ <p>
31
+ Served by <code>npm run serve:browser-example</code>. The example module
32
+ is bundled for the browser on each request, so a reload picks up edits.
33
+ </p>
34
+ <p>
35
+ Set <code>FACTORY_API_KEY</code> and
36
+ <code>DROID_SDK_EXAMPLE_CWD</code> before starting the server, then pick
37
+ one with <code>?example=daemon-session</code> (default),
38
+ <code>?example=daemon-multi-session</code>,
39
+ <code>?example=daemon-lifecycle</code>, or
40
+ <code>?example=list-sessions</code>.
41
+ </p>
42
+
43
+ <div id="error"></div>
44
+ <div id="output"></div>
45
+
46
+ <script>
47
+ globalThis.__DROID_SDK_EXAMPLE_CONFIG__ =
48
+ __INJECTED_DROID_SDK_EXAMPLE_CONFIG__;
49
+ </script>
50
+ <script type="module">
51
+ // Surface load-time failures (for example a missing config value, which
52
+ // throws during module evaluation) instead of leaving a blank page.
53
+ const errorBox = document.querySelector('#error');
54
+ const showError = (err) => {
55
+ errorBox.textContent = String(
56
+ (err && (err.stack || err.message)) || err
57
+ );
58
+ };
59
+ window.addEventListener('error', (e) => showError(e.error ?? e.message));
60
+ window.addEventListener('unhandledrejection', (e) => showError(e.reason));
61
+
62
+ const params = new URLSearchParams(location.search);
63
+ const example = params.get('example') ?? 'daemon-session';
64
+
65
+ import(`./${example}.js${location.search}`).catch(showError);
66
+ </script>
67
+ </body>
68
+ </html>
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Lists recent daemon sessions and their messages.
3
+ *
4
+ * Runs in a real browser (no `process`, no Node builtins). From a source
5
+ * checkout of this repository:
6
+ * npm run build
7
+ * FACTORY_API_KEY=... npm run serve:browser-example
8
+ * open http://127.0.0.1:8420/?example=list-sessions
9
+ *
10
+ * Never commit a real API key or deploy it in browser code.
11
+ */
12
+
13
+ import { connectToDaemon } from '@factory/droid-sdk';
14
+
15
+ const { daemonUrl, apiKey } = (
16
+ globalThis as typeof globalThis & {
17
+ __DROID_SDK_EXAMPLE_CONFIG__: {
18
+ daemonUrl: string;
19
+ apiKey: string;
20
+ };
21
+ }
22
+ ).__DROID_SDK_EXAMPLE_CONFIG__;
23
+ if (!apiKey) {
24
+ throw new Error('Set FACTORY_API_KEY.');
25
+ }
26
+
27
+ const droid = await connectToDaemon({
28
+ url: daemonUrl,
29
+ auth: { apiKey },
30
+ });
31
+
32
+ try {
33
+ const sessions = await droid.sessions.list({ limit: 10 });
34
+
35
+ if (sessions.length === 0) {
36
+ console.log('No sessions found.');
37
+ }
38
+
39
+ for (const session of sessions) {
40
+ const messages = await droid.sessions.getMessages(session.id, {
41
+ limit: 20,
42
+ });
43
+
44
+ console.log(
45
+ session.id,
46
+ session.title ?? '(untitled)',
47
+ `${messages.length} messages`
48
+ );
49
+ }
50
+ } finally {
51
+ droid.disconnect();
52
+ }
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Abort a running turn with AbortController.
3
3
  *
4
+ * Passing an `abortSignal` to `session.stream()` makes the stream throw when
5
+ * the signal fires. See interrupt-session.ts for the server-side equivalent.
6
+ *
4
7
  * Usage:
5
- * npx tsx examples/abort-session-stream.ts
8
+ * npx tsx examples/node/abort-session-stream.ts
6
9
  */
7
10
 
8
- import { createSession } from '@factory/droid-sdk';
11
+ import { createSession } from '@factory/droid-sdk/node';
9
12
 
10
- const session = await createSession({
11
- apiKey: process.env.FACTORY_API_KEY!,
12
- cwd: process.cwd(),
13
- });
13
+ const session = await createSession();
14
14
  const controller = new AbortController();
15
15
  const timeout = setTimeout(
16
16
  () => controller.abort(new Error('Stopped by AbortController')),
@@ -20,11 +20,9 @@ const timeout = setTimeout(
20
20
  try {
21
21
  for await (const _msg of session.stream(
22
22
  'Write a long explanation of how compilers work.',
23
- {
24
- abortSignal: controller.signal,
25
- }
23
+ { abortSignal: controller.signal }
26
24
  )) {
27
- void _msg;
25
+ // Consume the stream until the abort signal cuts it short.
28
26
  }
29
27
  } catch (error) {
30
28
  console.log(error instanceof Error ? error.message : String(error));
@@ -0,0 +1,35 @@
1
+ /**
2
+ * AskUser elicitation example.
3
+ *
4
+ * When Droid needs input mid-turn it sends an `askUser` request. `askUserHandler`
5
+ * answers it programmatically; without a handler the request goes unanswered.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/ask-user-elicitation.ts
9
+ */
10
+
11
+ import { run } from '@factory/droid-sdk/node';
12
+
13
+ const { text } = await run(
14
+ 'Use the AskUser tool to ask me which color I prefer, then reply with the color I picked.',
15
+ {
16
+ askUserHandler(params) {
17
+ for (const question of params.questions) {
18
+ console.log(`[asked] ${question.question}`);
19
+ console.log(`[options] ${question.options.join(', ')}`);
20
+ }
21
+
22
+ // Answer every question with its first option. A real integration would
23
+ // prompt the user; returning `cancelled: true` declines instead.
24
+ return {
25
+ answers: params.questions.map((question) => ({
26
+ index: question.index,
27
+ question: question.question,
28
+ answer: question.options[0] ?? 'no preference',
29
+ })),
30
+ };
31
+ },
32
+ }
33
+ );
34
+
35
+ console.log(`\n${text}`);
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Compact session example.
3
+ *
4
+ * Builds up history over a few turns, then calls `compact()` to
5
+ * summarize it into a new session.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/compact-session.ts
9
+ */
10
+
11
+ import { createSession, DroidMessageType } from '@factory/droid-sdk/node';
12
+
13
+ const session = await createSession();
14
+
15
+ try {
16
+ for (const prompt of [
17
+ 'Say "one" and nothing else',
18
+ 'Say "two" and nothing else',
19
+ 'Say "three" and nothing else',
20
+ ]) {
21
+ for await (const msg of session.stream(prompt)) {
22
+ if (msg.type === DroidMessageType.Assistant) {
23
+ console.log(msg.text);
24
+ }
25
+ }
26
+ }
27
+
28
+ const { session: compacted, removedCount } = await session.compact();
29
+
30
+ // `session` is now a retired wrapper. Its persisted ID can still be passed
31
+ // to resumeSession() later if the uncompacted history is needed again.
32
+ console.log(`\noriginal session: ${session.id}`);
33
+ console.log(`compacted into: ${compacted.id}`);
34
+ console.log(`messages removed: ${removedCount}`);
35
+
36
+ for await (const msg of compacted.stream(
37
+ 'Which numbers did I ask you to say?'
38
+ )) {
39
+ if (msg.type === DroidMessageType.Assistant) {
40
+ console.log(`continued: ${msg.text}`);
41
+ }
42
+ }
43
+
44
+ await compacted.close();
45
+ } finally {
46
+ // Closes the active source, or does nothing if it was retired.
47
+ await session.close();
48
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Enter spec mode on a live session.
3
+ *
4
+ * The other spec-mode examples start the session in spec mode. This one starts
5
+ * in the default mode and switches mid-session with `enterSpecMode()`, which is
6
+ * what a host app does when the user toggles spec mode on an open conversation.
7
+ *
8
+ * The switch is observable in the confirmations Droid asks for: it proposes a
9
+ * plan (`ExitSpecMode`) before touching the filesystem, instead of going
10
+ * straight to `Create`.
11
+ *
12
+ * Usage:
13
+ * npx tsx examples/node/enter-spec-mode.ts
14
+ */
15
+
16
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ import {
21
+ ToolConfirmationOutcome,
22
+ ToolConfirmationType,
23
+ createSession,
24
+ } from '@factory/droid-sdk/node';
25
+
26
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-enter-spec-'));
27
+ const outputPath = join(tempDir, 'hello.txt');
28
+
29
+ try {
30
+ const session = await createSession({
31
+ permissionHandler(params) {
32
+ for (const toolUse of params.toolUses) {
33
+ const label =
34
+ toolUse.details.type === ToolConfirmationType.ExitSpecMode
35
+ ? 'plan proposed'
36
+ : 'implementing';
37
+ console.log(`[${label}] ${toolUse.details.type}`);
38
+ }
39
+ // Approving ExitSpecMode accepts the plan and lets implementation run.
40
+ return ToolConfirmationOutcome.ProceedOnce;
41
+ },
42
+ });
43
+
44
+ try {
45
+ console.log(
46
+ `mode at creation: ${String(session.settings.interactionMode)}`
47
+ );
48
+
49
+ await session.enterSpecMode();
50
+ console.log('switched to spec mode\n');
51
+
52
+ for await (const _msg of session.stream(
53
+ `Create ${outputPath} containing "hello".`
54
+ )) {
55
+ // Consume the stream until the plan is approved and implemented.
56
+ }
57
+ } finally {
58
+ await session.close();
59
+ }
60
+
61
+ console.log(
62
+ `\nfile contents: ${JSON.stringify(await readFile(outputPath, 'utf8'))}`
63
+ );
64
+ } finally {
65
+ await rm(tempDir, { recursive: true, force: true });
66
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Fork session example.
3
+ *
4
+ * Establishes context in one session, forks it with `fork()`, then
5
+ * uses the returned fork handle and confirms the copied context carried over.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/node/fork-session.ts
9
+ */
10
+
11
+ import { DroidMessageType, createSession } from '@factory/droid-sdk/node';
12
+
13
+ async function streamText(
14
+ session: Awaited<ReturnType<typeof createSession>>,
15
+ prompt: string
16
+ ): Promise<string> {
17
+ let text = '';
18
+ for await (const msg of session.stream(prompt)) {
19
+ if (msg.type === DroidMessageType.Assistant) {
20
+ text += msg.text;
21
+ }
22
+ }
23
+ return text;
24
+ }
25
+
26
+ const session = await createSession();
27
+
28
+ try {
29
+ await streamText(session, 'Remember this phrase exactly: mango sunrise');
30
+
31
+ const fork = await session.fork();
32
+
33
+ // `session` is now a retired wrapper. Its persisted ID can still be passed
34
+ // to resumeSession() later if the original branch is needed again.
35
+ console.log(`original session: ${session.id}`);
36
+ console.log(`forked session: ${fork.id}`);
37
+ console.log(
38
+ `fork recalls: ${await streamText(fork, 'What phrase did I ask you to remember?')}`
39
+ );
40
+
41
+ await fork.close();
42
+ } finally {
43
+ // Closes the active source, or does nothing if it was retired.
44
+ await session.close();
45
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Hook execution example.
3
+ *
4
+ * Registers a PreToolUse hook for the `Execute` tool and prints the hook
5
+ * lifecycle messages that arrive in the session stream.
6
+ *
7
+ * The hook lives in a throwaway project directory (`<tempdir>/.factory/hooks.json`)
8
+ * that is passed as the session `cwd` and deleted afterwards, so running this
9
+ * example never writes to your own `~/.factory` configuration. It does not
10
+ * isolate you from it: hook lists are concatenated across settings levels
11
+ * rather than replaced, so your own user-level `PreToolUse` hooks are appended
12
+ * to the one below and fire during this run whenever their matcher matches.
13
+ *
14
+ * `hooksDisabled` resolves first-defined-wins across settings levels, so the
15
+ * config below sets it explicitly rather than leaving it absent. An absent
16
+ * value would inherit a global `"hooksDisabled": true` from `~/.factory` and
17
+ * the hook would never run.
18
+ *
19
+ * See https://docs.factory.ai/cli/configuration/hooks-guide for the `/hooks`
20
+ * slash command, the other events, and security guidance.
21
+ *
22
+ * Usage:
23
+ * npx tsx examples/node/hook-execution.ts
24
+ */
25
+
26
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
27
+ import { tmpdir } from 'node:os';
28
+ import { join } from 'node:path';
29
+
30
+ import {
31
+ DroidMessageType,
32
+ ToolConfirmationOutcome,
33
+ createSession,
34
+ } from '@factory/droid-sdk/node';
35
+
36
+ const hookSettings = {
37
+ hooksDisabled: false,
38
+ PreToolUse: [
39
+ {
40
+ matcher: 'Execute',
41
+ hooks: [{ type: 'command', command: 'echo hook-from-sdk-example' }],
42
+ },
43
+ ],
44
+ };
45
+
46
+ const projectDir = await mkdtemp(join(tmpdir(), 'droid-sdk-hook-project-'));
47
+
48
+ let hookCount = 0;
49
+
50
+ try {
51
+ await mkdir(join(projectDir, '.factory'), { recursive: true });
52
+ await writeFile(
53
+ join(projectDir, '.factory', 'hooks.json'),
54
+ JSON.stringify(hookSettings, null, 2)
55
+ );
56
+
57
+ const session = await createSession({
58
+ cwd: projectDir,
59
+ permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
60
+ });
61
+
62
+ try {
63
+ for await (const msg of session.stream(
64
+ 'Run the shell command `echo hello` using the Execute tool.'
65
+ )) {
66
+ if (msg.type !== DroidMessageType.Hook) {
67
+ continue;
68
+ }
69
+
70
+ hookCount++;
71
+ if (msg.status === 'started') {
72
+ console.log(`[hook started] ${msg.eventName}: ${msg.command}`);
73
+ } else {
74
+ console.log(
75
+ `[hook ${msg.status}] exit code ${String(msg.exitCode)}: ${(msg.stdout ?? '').trim()}`
76
+ );
77
+ }
78
+ }
79
+ } finally {
80
+ await session.close();
81
+ }
82
+ } finally {
83
+ await rm(projectDir, { recursive: true, force: true });
84
+ }
85
+
86
+ // A silent zero-hook run would look identical to a passing one, so fail loudly.
87
+ if (hookCount === 0) {
88
+ throw new Error(
89
+ 'No hook messages arrived: the registered PreToolUse hook never ran. ' +
90
+ 'The most likely cause is that the droid answered this turn without ' +
91
+ 'calling the Execute tool, so the `Execute` matcher never fired. ' +
92
+ 'Re-run, or widen the matcher to the tool the droid actually used.'
93
+ );
94
+ }