@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
@@ -0,0 +1,65 @@
1
+ /**
2
+ * List sessions example.
3
+ *
4
+ * Demonstrates:
5
+ * - scanning ~/.factory/sessions/ for saved droid sessions
6
+ * - filtering by current working directory
7
+ * - including sessions from other projects with a cap
8
+ *
9
+ * This helper reads the local filesystem directly — no droid process is
10
+ * spawned — so it's safe to call even when no droid is running.
11
+ *
12
+ * Usage:
13
+ * npx tsx examples/list-sessions.ts
14
+ */
15
+
16
+ import { listSessions } from '../src/index.js';
17
+
18
+ function formatDate(date: Date): string {
19
+ return date.toISOString().replace('T', ' ').slice(0, 19);
20
+ }
21
+
22
+ async function main(): Promise<void> {
23
+ console.log(`=== Sessions for ${process.cwd()} ===\n`);
24
+
25
+ // Default call: scopes to process.cwd().
26
+ const currentProject = await listSessions({ numSessions: 10 });
27
+
28
+ if (currentProject.length === 0) {
29
+ console.log('(no sessions recorded for this directory yet)');
30
+ } else {
31
+ console.log(`10 most recent for this project:\n`);
32
+ for (const session of currentProject) {
33
+ const fav = session.isFavorite ? '*' : ' ';
34
+ const title = session.sessionTitle ?? session.title ?? '(untitled)';
35
+ console.log(
36
+ ` ${fav} [${session.id.slice(0, 8)}] ${formatDate(session.modifiedTime)} ${session.messageCount} msgs — ${title}`
37
+ );
38
+ }
39
+ }
40
+
41
+ console.log('\n=== 5 most recent sessions across all projects ===\n');
42
+
43
+ const allSessions = await listSessions({
44
+ fetchOutsideCWD: true,
45
+ numSessions: 5,
46
+ });
47
+
48
+ if (allSessions.length === 0) {
49
+ console.log('(no sessions found)');
50
+ } else {
51
+ for (const session of allSessions) {
52
+ const title = session.sessionTitle ?? session.title ?? '(untitled)';
53
+ const cwd = session.cwd ?? '(no cwd)';
54
+ console.log(
55
+ ` [${session.id.slice(0, 8)}] ${formatDate(session.modifiedTime)} — ${title}`
56
+ );
57
+ console.log(` cwd: ${cwd}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ main().catch((err: unknown) => {
63
+ console.error('Error:', err);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Multi-turn session example.
3
+ *
4
+ * Demonstrates `createSession()` for a persistent session, multiple
5
+ * `stream()` calls for streaming turns, `send()` for a non-streaming
6
+ * turn, and `close()` for cleanup.
7
+ *
8
+ * Usage:
9
+ * npx tsx examples/multi-turn-session.ts
10
+ */
11
+
12
+ import { createSession, DroidMessageType } from '../src/index.js';
13
+
14
+ async function main(): Promise<void> {
15
+ const session = await createSession({ cwd: process.cwd() });
16
+ try {
17
+ console.log(`Session created: ${session.sessionId}\n`);
18
+
19
+ console.log('=== Turn 1 (streaming) ===');
20
+ console.log('Prompt: "List the TypeScript files in this project"\n');
21
+
22
+ for await (const msg of session.stream(
23
+ 'List the TypeScript files in this project'
24
+ )) {
25
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
26
+ process.stdout.write(msg.text);
27
+ }
28
+ if (msg.type === DroidMessageType.TurnComplete) {
29
+ console.log('\n');
30
+ }
31
+ }
32
+
33
+ console.log('=== Turn 2 (streaming) ===');
34
+ console.log('Prompt: "How many lines of code total?"\n');
35
+
36
+ for await (const msg of session.stream('How many lines of code total?')) {
37
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
38
+ process.stdout.write(msg.text);
39
+ }
40
+ if (msg.type === DroidMessageType.TurnComplete) {
41
+ console.log('\n');
42
+ }
43
+ }
44
+
45
+ console.log('=== Turn 3 (non-streaming) ===');
46
+ console.log('Prompt: "Summarize the project in one sentence"\n');
47
+
48
+ const result = await session.send('Summarize the project in one sentence');
49
+
50
+ console.log('Response:', result.text);
51
+ console.log(`Messages received: ${result.messages.length}`);
52
+ if (result.tokenUsage) {
53
+ console.log(
54
+ `Tokens — input: ${result.tokenUsage.inputTokens}, ` +
55
+ `output: ${result.tokenUsage.outputTokens}`
56
+ );
57
+ }
58
+ } finally {
59
+ await session.close();
60
+ console.log('\nSession closed.');
61
+ }
62
+ }
63
+
64
+ main().catch((err: unknown) => {
65
+ console.error('Error:', err);
66
+ process.exit(1);
67
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Permission handler example.
3
+ *
4
+ * Demonstrates using `query()` with a custom `permissionHandler` that
5
+ * logs tool confirmation details, approves only one expected file creation,
6
+ * and cancels any unexpected tool request.
7
+ *
8
+ * Usage:
9
+ * npx tsx examples/permission-handler.ts
10
+ */
11
+
12
+ import { mkdtemp, rm } from 'node:fs/promises';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import {
17
+ DroidMessageType,
18
+ query,
19
+ ToolConfirmationOutcome,
20
+ ToolConfirmationType,
21
+ type RequestPermissionRequestParams,
22
+ } from '../src/index.js';
23
+
24
+ function permissionHandler(
25
+ allowedFilePath: string,
26
+ params: RequestPermissionRequestParams
27
+ ): ToolConfirmationOutcome {
28
+ for (const item of params.toolUses) {
29
+ console.log(`\n[Permission] Tool: ${item.toolUse.name}`);
30
+ console.log(` Type: ${item.confirmationType}`);
31
+ console.log(` Input: ${JSON.stringify(item.toolUse.input, null, 2)}`);
32
+ }
33
+
34
+ const onlyAllowedCreate = params.toolUses.every(
35
+ (item) =>
36
+ item.details.type === ToolConfirmationType.Create &&
37
+ item.details.filePath === allowedFilePath
38
+ );
39
+
40
+ if (!onlyAllowedCreate) {
41
+ console.log(' → Cancelling unexpected tool request\n');
42
+ return ToolConfirmationOutcome.Cancel;
43
+ }
44
+
45
+ console.log(' → Approving expected file creation with ProceedOnce\n');
46
+ return ToolConfirmationOutcome.ProceedOnce;
47
+ }
48
+
49
+ async function main(): Promise<void> {
50
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-permission-'));
51
+ const outputPath = join(tempDir, 'hello.txt');
52
+ const prompt =
53
+ process.argv[2] ??
54
+ `Create a file called ${outputPath} with the text 'Hello, World!'`;
55
+
56
+ try {
57
+ console.log(`Sending prompt: "${prompt}"\n`);
58
+
59
+ const stream = query({
60
+ prompt,
61
+ cwd: process.cwd(),
62
+ permissionHandler: (params) => permissionHandler(outputPath, params),
63
+ });
64
+
65
+ for await (const msg of stream) {
66
+ switch (msg.type) {
67
+ case DroidMessageType.AssistantTextDelta:
68
+ process.stdout.write(msg.text);
69
+ break;
70
+
71
+ case DroidMessageType.ToolUse:
72
+ console.log(`\n[Tool Use] ${msg.toolName}`);
73
+ break;
74
+
75
+ case DroidMessageType.ToolResult:
76
+ console.log(`[Tool Result] ${msg.isError ? 'Error' : 'Success'}`);
77
+ break;
78
+
79
+ case DroidMessageType.TurnComplete:
80
+ console.log('\n\n--- Turn complete ---');
81
+ if (msg.tokenUsage) {
82
+ console.log(
83
+ `Tokens — input: ${msg.tokenUsage.inputTokens}, ` +
84
+ `output: ${msg.tokenUsage.outputTokens}`
85
+ );
86
+ }
87
+ break;
88
+ }
89
+ }
90
+ } finally {
91
+ await rm(tempDir, { recursive: true, force: true });
92
+ }
93
+ }
94
+
95
+ main().catch((err: unknown) => {
96
+ console.error('Error:', err);
97
+ process.exit(1);
98
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * DroidResult metadata example.
3
+ *
4
+ * Runs a prompt with the actual Droid CLI and prints the aggregated `run()`
5
+ * result metadata: sessionId, durationMs, turnCount, success, error, and token
6
+ * usage.
7
+ *
8
+ * Usage:
9
+ * npx tsx examples/result-metadata.ts
10
+ * npx tsx examples/result-metadata.ts "Reply with metadata details."
11
+ */
12
+
13
+ import assert from 'node:assert/strict';
14
+
15
+ import { run } from '../src/index.js';
16
+
17
+ async function main(): Promise<void> {
18
+ const prompt =
19
+ process.argv.slice(2).join(' ') ||
20
+ 'Reply with "Metadata OK" and nothing else.';
21
+
22
+ console.log(`Sending prompt: "${prompt}"\n`);
23
+
24
+ const result = await run(prompt, {
25
+ cwd: process.cwd(),
26
+ });
27
+
28
+ assert.equal(typeof result.sessionId, 'string');
29
+ assert.ok(result.sessionId.length > 0);
30
+ assert.equal(typeof result.durationMs, 'number');
31
+ assert.ok(result.durationMs >= 0);
32
+ assert.equal(typeof result.turnCount, 'number');
33
+ assert.ok(result.turnCount >= 1);
34
+ assert.equal(typeof result.success, 'boolean');
35
+ assert.ok(Array.isArray(result.messages));
36
+
37
+ console.log('=== Result ===');
38
+ console.log(result.text);
39
+
40
+ console.log('\n=== Metadata ===');
41
+ console.log(`Session ID: ${result.sessionId}`);
42
+ console.log(`Duration: ${result.durationMs}ms`);
43
+ console.log(`Turn count: ${result.turnCount}`);
44
+ console.log(`Success: ${String(result.success)}`);
45
+ console.log(`Messages received: ${result.messages.length}`);
46
+
47
+ if (result.error) {
48
+ console.log(`Error: ${result.error.message}`);
49
+ }
50
+
51
+ if (result.tokenUsage) {
52
+ console.log(
53
+ `Tokens — input: ${result.tokenUsage.inputTokens}, ` +
54
+ `output: ${result.tokenUsage.outputTokens}`
55
+ );
56
+ }
57
+ }
58
+
59
+ main().catch((error: unknown) => {
60
+ console.error('Error:', error);
61
+ process.exit(1);
62
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Manual smoke test for the one-shot `run()` API.
3
+ *
4
+ * Sends a single prompt, prints the aggregated result, and exits after the
5
+ * underlying session has been closed by `run()`.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/run.ts
9
+ * npx tsx examples/run.ts "What is 2 + 2?"
10
+ */
11
+
12
+ import { run } from '../src/index.js';
13
+
14
+ async function main(): Promise<void> {
15
+ const text = process.argv.slice(2).join(' ') || 'What is 2 + 2?';
16
+
17
+ console.log(`Sending prompt: "${text}"\n`);
18
+
19
+ const result = await run(text, {
20
+ cwd: process.cwd(),
21
+ });
22
+
23
+ console.log('=== Result ===');
24
+ console.log(result.text);
25
+ console.log(`Messages received: ${result.messages.length}`);
26
+
27
+ if (result.tokenUsage) {
28
+ console.log(
29
+ `Tokens — input: ${result.tokenUsage.inputTokens}, ` +
30
+ `output: ${result.tokenUsage.outputTokens}`
31
+ );
32
+ }
33
+ }
34
+
35
+ main().catch((err: unknown) => {
36
+ console.error('Error:', err);
37
+ process.exit(1);
38
+ });
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+
3
+ import {
4
+ ToolConfirmationOutcome,
5
+ createSession,
6
+ createSdkMcpServer,
7
+ tool,
8
+ } from '../src/index.js';
9
+
10
+ const execPath = process.env['DROID_EXEC_PATH'] ?? 'droid';
11
+
12
+ const sdkTools = createSdkMcpServer({
13
+ name: 'sdk-tools',
14
+ tools: [
15
+ tool(
16
+ 'favorite_number',
17
+ 'Returns a favorite number for a person',
18
+ { name: z.string() },
19
+ ({ name }) => `${name}'s favorite number is 42.`
20
+ ),
21
+ ],
22
+ });
23
+
24
+ const session = await createSession({
25
+ execPath,
26
+ mcpServers: [sdkTools],
27
+ cwd: process.cwd(),
28
+ permissionHandler: () => ToolConfirmationOutcome.ProceedOnce,
29
+ });
30
+
31
+ try {
32
+ const result = await session.send(
33
+ 'Use the favorite_number tool for Ada and tell me the answer.'
34
+ );
35
+ console.log(result.text);
36
+ } finally {
37
+ await session.close();
38
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Simple one-shot query example.
3
+ *
4
+ * Demonstrates using `query()` to send a single prompt, streaming
5
+ * `AssistantTextDelta` messages to stdout, and handling `TurnComplete`.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/simple-query.ts
9
+ */
10
+
11
+ import { DroidMessageType, query } from '../src/index.js';
12
+
13
+ async function main(): Promise<void> {
14
+ const prompt = process.argv[2] ?? 'What files are in the current directory?';
15
+
16
+ console.log(`Sending prompt: "${prompt}"\n`);
17
+
18
+ const stream = query({
19
+ prompt,
20
+ cwd: process.cwd(),
21
+ });
22
+
23
+ for await (const msg of stream) {
24
+ switch (msg.type) {
25
+ case DroidMessageType.AssistantTextDelta:
26
+ process.stdout.write(msg.text);
27
+ break;
28
+
29
+ case DroidMessageType.ToolUse:
30
+ console.log(`\n[Tool] ${msg.toolName}`);
31
+ break;
32
+
33
+ case DroidMessageType.ToolResult:
34
+ console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
35
+ break;
36
+
37
+ case DroidMessageType.TurnComplete:
38
+ console.log('\n\n--- Turn complete ---');
39
+ if (msg.tokenUsage) {
40
+ console.log(
41
+ `Tokens — input: ${msg.tokenUsage.inputTokens}, ` +
42
+ `output: ${msg.tokenUsage.outputTokens}`
43
+ );
44
+ }
45
+ break;
46
+ }
47
+ }
48
+ }
49
+
50
+ main().catch((err: unknown) => {
51
+ console.error('Error:', err);
52
+ process.exit(1);
53
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Spec mode: approve and hand off to a new session.
3
+ *
4
+ * The agent drafts a plan in spec mode. On approval with
5
+ * ProceedNewSessionHigh, the server automatically creates a
6
+ * fresh session and implements the plan. The results stream
7
+ * back through the same query — no manual session creation needed.
8
+ *
9
+ * Usage:
10
+ * npx tsx examples/spec-mode-new-session.ts
11
+ */
12
+
13
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+
17
+ import {
18
+ DroidMessageType,
19
+ DroidInteractionMode,
20
+ query,
21
+ ReasoningEffort,
22
+ ToolConfirmationOutcome,
23
+ ToolConfirmationType,
24
+ } from '../src/index.js';
25
+
26
+ const EXPECTED_CONTENT = 'Hello from Droid';
27
+
28
+ async function main(): Promise<void> {
29
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
30
+ const outputPath = join(tempDir, 'hello-from-droid.txt');
31
+ const prompt =
32
+ `Plan how to create a small ${outputPath} file containing the text ` +
33
+ '"Hello from Droid". Keep the plan short and concrete.';
34
+
35
+ console.log('=== Spec Mode: New Session Handoff ===\n');
36
+
37
+ try {
38
+ let toolError: string | null = null;
39
+
40
+ const stream = query({
41
+ prompt,
42
+ cwd: process.cwd(),
43
+ interactionMode: DroidInteractionMode.Spec,
44
+ specModeReasoningEffort: ReasoningEffort.High,
45
+ permissionHandler(params) {
46
+ const exitSpec = params.toolUses.find(
47
+ (item) => item.details.type === ToolConfirmationType.ExitSpecMode
48
+ );
49
+
50
+ if (exitSpec?.details.type === ToolConfirmationType.ExitSpecMode) {
51
+ console.log(
52
+ '\n--- Plan received, approving with new session handoff ---'
53
+ );
54
+ console.log(exitSpec.details.plan);
55
+ console.log('---\n');
56
+
57
+ // The server creates a fresh session and implements the plan.
58
+ // Results continue streaming through this same query.
59
+ return ToolConfirmationOutcome.ProceedNewSessionHigh;
60
+ }
61
+
62
+ const onlyAllowedCreate = params.toolUses.every(
63
+ (item) =>
64
+ item.details.type === ToolConfirmationType.Create &&
65
+ item.details.filePath === outputPath
66
+ );
67
+
68
+ return onlyAllowedCreate
69
+ ? ToolConfirmationOutcome.ProceedOnce
70
+ : ToolConfirmationOutcome.Cancel;
71
+ },
72
+ });
73
+
74
+ for await (const msg of stream) {
75
+ switch (msg.type) {
76
+ case DroidMessageType.AssistantTextDelta:
77
+ process.stdout.write(msg.text);
78
+ break;
79
+ case DroidMessageType.ToolUse:
80
+ console.log(`\n[Tool] ${msg.toolName}`);
81
+ break;
82
+ case DroidMessageType.ToolResult:
83
+ if (msg.isError) {
84
+ toolError = JSON.stringify(msg.content);
85
+ }
86
+ console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
87
+ break;
88
+ case DroidMessageType.TurnComplete:
89
+ console.log('\n\n--- Turn complete ---');
90
+ if (msg.tokenUsage) {
91
+ console.log(
92
+ `Tokens — input: ${msg.tokenUsage.inputTokens}, output: ${msg.tokenUsage.outputTokens}`
93
+ );
94
+ }
95
+ break;
96
+ }
97
+ }
98
+
99
+ if (toolError) {
100
+ throw new Error(
101
+ `Tool failed during spec-mode implementation: ${toolError}`
102
+ );
103
+ }
104
+
105
+ const content = await readFile(outputPath, 'utf8');
106
+ if (content.trim() !== EXPECTED_CONTENT) {
107
+ throw new Error(
108
+ `Expected ${outputPath} to contain ${JSON.stringify(EXPECTED_CONTENT)}, got ${JSON.stringify(content)}`
109
+ );
110
+ }
111
+
112
+ console.log(`\nVerified ${outputPath}.`);
113
+ } finally {
114
+ await rm(tempDir, { recursive: true, force: true });
115
+ }
116
+ }
117
+
118
+ main().catch((err: unknown) => {
119
+ console.error('Error:', err);
120
+ process.exit(1);
121
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Spec mode: approve and implement in the same session.
3
+ *
4
+ * The agent drafts a plan in spec mode, then implements it
5
+ * in the same query stream after approval.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/spec-mode-same-session.ts
9
+ */
10
+
11
+ import { mkdtemp, rm } from 'node:fs/promises';
12
+ import { readFile } from 'node:fs/promises';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import {
17
+ DroidMessageType,
18
+ DroidInteractionMode,
19
+ query,
20
+ ReasoningEffort,
21
+ ToolConfirmationOutcome,
22
+ ToolConfirmationType,
23
+ } from '../src/index.js';
24
+
25
+ const EXPECTED_CONTENT = 'Hello from Droid';
26
+
27
+ async function main(): Promise<void> {
28
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
29
+ const outputPath = join(tempDir, 'hello-from-droid.txt');
30
+ const prompt =
31
+ `Plan how to create a small ${outputPath} file containing the text ` +
32
+ '"Hello from Droid". Keep the plan short and concrete.';
33
+
34
+ console.log('=== Spec Mode: Same Session ===\n');
35
+
36
+ try {
37
+ let toolError: string | null = null;
38
+
39
+ const stream = query({
40
+ prompt,
41
+ cwd: process.cwd(),
42
+ interactionMode: DroidInteractionMode.Spec,
43
+ specModeReasoningEffort: ReasoningEffort.High,
44
+ permissionHandler(params) {
45
+ const exitSpec = params.toolUses.find(
46
+ (item) => item.details.type === ToolConfirmationType.ExitSpecMode
47
+ );
48
+
49
+ if (exitSpec?.details.type === ToolConfirmationType.ExitSpecMode) {
50
+ console.log('\n--- Plan received, approving in same session ---');
51
+ console.log(exitSpec.details.plan);
52
+ console.log('---\n');
53
+ return ToolConfirmationOutcome.ProceedOnce;
54
+ }
55
+
56
+ const onlyAllowedCreate = params.toolUses.every(
57
+ (item) =>
58
+ item.details.type === ToolConfirmationType.Create &&
59
+ item.details.filePath === outputPath
60
+ );
61
+ const onlyAllowedApplyPatch = params.toolUses.every(
62
+ (item) =>
63
+ item.details.type === ToolConfirmationType.ApplyPatch &&
64
+ item.details.filePath === outputPath
65
+ );
66
+ const onlyAllowedExecute = params.toolUses.every((item) => {
67
+ if (item.details.type !== ToolConfirmationType.Execute) {
68
+ return false;
69
+ }
70
+
71
+ const command = item.details.fullCommand;
72
+ return (
73
+ command.includes(outputPath) &&
74
+ command.includes(tempDir) &&
75
+ !command.includes('sudo') &&
76
+ !command.includes('rm -rf /')
77
+ );
78
+ });
79
+
80
+ return onlyAllowedCreate || onlyAllowedApplyPatch || onlyAllowedExecute
81
+ ? ToolConfirmationOutcome.ProceedOnce
82
+ : ToolConfirmationOutcome.Cancel;
83
+ },
84
+ });
85
+
86
+ for await (const msg of stream) {
87
+ switch (msg.type) {
88
+ case DroidMessageType.AssistantTextDelta:
89
+ process.stdout.write(msg.text);
90
+ break;
91
+ case DroidMessageType.ToolUse:
92
+ console.log(`\n[Tool] ${msg.toolName}`);
93
+ break;
94
+ case DroidMessageType.ToolResult:
95
+ if (msg.isError) {
96
+ toolError = JSON.stringify(msg.content);
97
+ }
98
+ console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
99
+ break;
100
+ case DroidMessageType.TurnComplete:
101
+ console.log('\n\n--- Turn complete ---');
102
+ if (msg.tokenUsage) {
103
+ console.log(
104
+ `Tokens — input: ${msg.tokenUsage.inputTokens}, output: ${msg.tokenUsage.outputTokens}`
105
+ );
106
+ }
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (toolError) {
112
+ throw new Error(
113
+ `Tool failed during spec-mode implementation: ${toolError}`
114
+ );
115
+ }
116
+
117
+ const content = await readFile(outputPath, 'utf8');
118
+ if (content.trim() !== EXPECTED_CONTENT) {
119
+ throw new Error(
120
+ `Expected ${outputPath} to contain ${JSON.stringify(EXPECTED_CONTENT)}, got ${JSON.stringify(content)}`
121
+ );
122
+ }
123
+
124
+ console.log(`\nVerified ${outputPath}.`);
125
+ } finally {
126
+ await rm(tempDir, { recursive: true, force: true });
127
+ }
128
+ }
129
+
130
+ main().catch((err: unknown) => {
131
+ console.error('Error:', err);
132
+ process.exit(1);
133
+ });