@factory/droid-sdk 0.1.4 → 0.3.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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * One-off integration smoke test for JSON output against Droid.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/droid-dev-structured-output.ts
6
+ * DROID_EXEC_PATH=/path/to/droid npx tsx examples/droid-dev-structured-output.ts
7
+ */
8
+
9
+ import { run } from '@factory/droid-sdk';
10
+ import { z } from 'zod';
11
+
12
+ const PersonSchema = z.object({
13
+ name: z.literal('Ada Lovelace'),
14
+ language: z.literal('TypeScript'),
15
+ score: z.number(),
16
+ });
17
+
18
+ async function main(): Promise<void> {
19
+ const execPath = process.env['DROID_EXEC_PATH'] ?? 'droid';
20
+
21
+ const result = await run(
22
+ [
23
+ 'Return a structured object for Ada Lovelace.',
24
+ 'Use name "Ada Lovelace", language "TypeScript", and score 99.',
25
+ 'Return only valid JSON and do not include markdown fences.',
26
+ ].join(' '),
27
+ {
28
+ execPath,
29
+ cwd: process.cwd(),
30
+ }
31
+ );
32
+
33
+ const parsed = PersonSchema.parse(JSON.parse(result.text));
34
+
35
+ console.log(`droid executable: ${execPath}`);
36
+ console.log('structured output:', JSON.stringify(parsed, null, 2));
37
+ console.log(`messages received: ${result.messages.length}`);
38
+ }
39
+
40
+ main().catch((error: unknown) => {
41
+ console.error(error);
42
+ process.exit(1);
43
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Fork session example.
3
+ *
4
+ * Demonstrates:
5
+ * - creating a session and establishing some context
6
+ * - calling `forkSession()` to get a new server-side session ID
7
+ * - resuming the fork and continuing from the copied context
8
+ *
9
+ * Usage:
10
+ * npx tsx examples/fork-session.ts
11
+ */
12
+
13
+ import {
14
+ DroidMessageType,
15
+ createSession,
16
+ resumeSession,
17
+ } from '@factory/droid-sdk';
18
+
19
+ async function streamText(
20
+ session: Awaited<ReturnType<typeof createSession>>,
21
+ prompt: string
22
+ ): Promise<string> {
23
+ let text = '';
24
+ for await (const msg of session.stream(prompt)) {
25
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
26
+ text += msg.text;
27
+ }
28
+ }
29
+ return text;
30
+ }
31
+
32
+ async function main(): Promise<void> {
33
+ const session = await createSession({ cwd: process.cwd() });
34
+ let fork: Awaited<ReturnType<typeof resumeSession>> | null = null;
35
+
36
+ try {
37
+ console.log(`Original session: ${session.sessionId}\n`);
38
+ await streamText(session, 'Remember this phrase exactly: mango sunrise');
39
+
40
+ const { newSessionId } = await session.forkSession();
41
+ console.log(`Forked session: ${newSessionId}\n`);
42
+
43
+ fork = await resumeSession(newSessionId);
44
+
45
+ const result = await streamText(
46
+ fork,
47
+ 'What phrase did I ask you to remember?'
48
+ );
49
+ console.log('Fork response:');
50
+ console.log(result);
51
+ } finally {
52
+ await fork?.close();
53
+ await session.close();
54
+ console.log('\nBoth sessions closed.');
55
+ }
56
+ }
57
+
58
+ main().catch((err: unknown) => {
59
+ console.error('Error:', err);
60
+ process.exit(1);
61
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Hook execution streaming example.
3
+ *
4
+ * Demonstrates how to handle hook execution messages in a session stream.
5
+ *
6
+ * Usage:
7
+ * npx tsx examples/hook-execution.ts
8
+ */
9
+
10
+ import { DroidMessageType, createSession } from '../src/index.js';
11
+
12
+ async function main(): Promise<void> {
13
+ const prompt = 'Run a simple shell command using Execute tool.';
14
+
15
+ console.log(`Sending prompt: "${prompt}"\n`);
16
+
17
+ // Note: To actually see hooks, you need to have hooks configured in your droid settings.
18
+ const session = await createSession({ cwd: process.cwd() });
19
+
20
+ try {
21
+ for await (const msg of session.stream(prompt)) {
22
+ switch (msg.type) {
23
+ case DroidMessageType.Assistant:
24
+ process.stdout.write(msg.text);
25
+ break;
26
+
27
+ case DroidMessageType.ToolCall:
28
+ console.log(`\n[Tool Call] ${msg.toolUse.name} (${msg.toolUse.id})`);
29
+ break;
30
+
31
+ case DroidMessageType.Hook:
32
+ if (msg.status === 'started') {
33
+ console.log(
34
+ ` [Hook Started] ID: ${msg.hookId}, Event: ${msg.eventName}, Command: ${msg.command}`
35
+ );
36
+ } else {
37
+ console.log(
38
+ ` [Hook ${msg.status}] ID: ${msg.hookId}, Exit Code: ${msg.exitCode}`
39
+ );
40
+ if (msg.stdout) console.log(` stdout: ${msg.stdout.trim()}`);
41
+ if (msg.stderr) console.log(` stderr: ${msg.stderr.trim()}`);
42
+ }
43
+ break;
44
+
45
+ case DroidMessageType.ToolResult:
46
+ console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
47
+ break;
48
+
49
+ case DroidMessageType.Result:
50
+ console.log('\n\n--- Turn complete ---');
51
+ break;
52
+ }
53
+ }
54
+ } finally {
55
+ await session.close();
56
+ }
57
+ }
58
+
59
+ main().catch((err: unknown) => {
60
+ console.error('Error:', err);
61
+ process.exit(1);
62
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Initialization metadata example.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/init-metadata.ts
6
+ */
7
+
8
+ import { createSession, resumeSession } from '@factory/droid-sdk';
9
+
10
+ const session = await createSession({ cwd: process.cwd() });
11
+ const resumed = await resumeSession(session.sessionId);
12
+
13
+ console.log(`created session: ${session.sessionId}`);
14
+ console.log(`resumed session: ${resumed.sessionId}`);
15
+ console.log(`created model: ${String(session.initResult.settings.modelId)}`);
16
+ console.log(`resumed cwd: ${String(resumed.initResult.cwd)}`);
17
+
18
+ await resumed.close();
19
+ await session.close();
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Interrupt a running turn.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/interrupt-session.ts
6
+ */
7
+
8
+ import { createSession, DroidMessageType } from '@factory/droid-sdk';
9
+
10
+ const session = await createSession({ cwd: process.cwd() });
11
+ let deltaCount = 0;
12
+
13
+ try {
14
+ for await (const msg of session.stream(
15
+ 'Write a long history of computing.'
16
+ )) {
17
+ if (msg.type !== DroidMessageType.AssistantTextDelta) {
18
+ continue;
19
+ }
20
+
21
+ process.stdout.write(msg.text);
22
+ deltaCount++;
23
+
24
+ if (deltaCount === 5) {
25
+ await session.interrupt();
26
+ }
27
+ }
28
+
29
+ console.log('\nInterrupted turn.');
30
+ } finally {
31
+ await session.close();
32
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * List saved sessions example.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/list-sessions.ts
6
+ */
7
+
8
+ import { listSessions } from '@factory/droid-sdk';
9
+
10
+ const sessions = await listSessions({ numSessions: 5 });
11
+
12
+ for (const session of sessions) {
13
+ const title = session.sessionTitle ?? session.title ?? '(untitled)';
14
+ console.log(`${session.id}: ${title}`);
15
+ }
16
+
17
+ if (sessions.length === 0) {
18
+ console.log('No sessions found for this project.');
19
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Multi-turn session example.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/multi-turn-session.ts
6
+ */
7
+
8
+ import { DroidMessageType, createSession } from '@factory/droid-sdk';
9
+
10
+ async function streamText(
11
+ session: Awaited<ReturnType<typeof createSession>>,
12
+ prompt: string
13
+ ): Promise<string> {
14
+ let text = '';
15
+ for await (const msg of session.stream(prompt)) {
16
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
17
+ text += msg.text;
18
+ }
19
+ }
20
+ return text;
21
+ }
22
+
23
+ const session = await createSession({ cwd: process.cwd() });
24
+
25
+ try {
26
+ console.log(`Session: ${session.sessionId}\n`);
27
+
28
+ const first = await streamText(session, 'What is this project?');
29
+ console.log(first);
30
+
31
+ const second = await streamText(session, 'What should I test first?');
32
+ console.log('\n', second);
33
+ } finally {
34
+ await session.close();
35
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Permission handler example.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/permission-handler.ts
6
+ */
7
+
8
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ import {
13
+ ToolConfirmationOutcome,
14
+ ToolConfirmationType,
15
+ run,
16
+ } from '@factory/droid-sdk';
17
+
18
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-permission-'));
19
+ const outputPath = join(tempDir, 'hello.txt');
20
+
21
+ try {
22
+ await run(`Create ${outputPath} with the text "Hello, World!"`, {
23
+ cwd: process.cwd(),
24
+ permissionHandler(params) {
25
+ const onlyCreatesExpectedFile = params.toolUses.every(
26
+ (item) =>
27
+ item.details.type === ToolConfirmationType.Create &&
28
+ item.details.filePath === outputPath
29
+ );
30
+
31
+ return onlyCreatesExpectedFile
32
+ ? ToolConfirmationOutcome.ProceedOnce
33
+ : ToolConfirmationOutcome.Cancel;
34
+ },
35
+ });
36
+
37
+ console.log(await readFile(outputPath, 'utf8'));
38
+ } finally {
39
+ await rm(tempDir, { recursive: true, force: true });
40
+ }
@@ -0,0 +1,25 @@
1
+ import { OutputFormatType, run } from '@factory/droid-sdk';
2
+
3
+ type FavoriteNumber = { favoriteNumber: number };
4
+
5
+ const result = await run('Pick a favorite number between 1 and 42.', {
6
+ cwd: process.cwd(),
7
+ outputFormat: {
8
+ type: OutputFormatType.JsonSchema,
9
+ schema: {
10
+ type: 'object',
11
+ properties: {
12
+ favoriteNumber: {
13
+ type: 'number',
14
+ minimum: 1,
15
+ maximum: 42,
16
+ },
17
+ },
18
+ required: ['favoriteNumber'],
19
+ },
20
+ },
21
+ });
22
+
23
+ console.log(
24
+ (result.structuredOutput as FavoriteNumber | undefined)?.favoriteNumber
25
+ );
@@ -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 '@factory/droid-sdk';
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 '@factory/droid-sdk';
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,42 @@
1
+ import {
2
+ DroidMessageType,
3
+ ToolConfirmationOutcome,
4
+ createSession,
5
+ createSdkMcpServer,
6
+ tool,
7
+ } from '@factory/droid-sdk';
8
+ import { z } from 'zod';
9
+
10
+ const execPath = process.env['DROID_EXEC_PATH'] ?? 'droid-dev';
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
+ for await (const msg of session.stream(
33
+ 'Use the favorite_number tool for Ada and tell me the answer.'
34
+ )) {
35
+ if (msg.type === DroidMessageType.AssistantTextDelta) {
36
+ process.stdout.write(msg.text);
37
+ }
38
+ }
39
+ console.log();
40
+ } finally {
41
+ await session.close();
42
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Simple session streaming example.
3
+ *
4
+ * Demonstrates using `session.stream()` to send a prompt, streaming
5
+ * full assistant/tool messages, and handling the final `result`.
6
+ *
7
+ * Usage:
8
+ * npx tsx examples/session-stream.ts
9
+ */
10
+
11
+ import { DroidMessageType, createSession } from '@factory/droid-sdk';
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 session = await createSession({ cwd: process.cwd() });
19
+
20
+ try {
21
+ for await (const msg of session.stream(prompt)) {
22
+ switch (msg.type) {
23
+ case DroidMessageType.Assistant:
24
+ process.stdout.write(msg.text);
25
+ break;
26
+
27
+ case DroidMessageType.ToolCall:
28
+ console.log(`\n[Tool] ${msg.toolUse.name}`);
29
+ break;
30
+
31
+ case DroidMessageType.ToolResult:
32
+ console.log(`[Tool Result] ${msg.isError ? 'Error' : 'OK'}`);
33
+ break;
34
+
35
+ case DroidMessageType.Result:
36
+ console.log('\n\n--- Turn complete ---');
37
+ if (msg.tokenUsage) {
38
+ console.log(
39
+ `Tokens — input: ${msg.tokenUsage.inputTokens}, ` +
40
+ `output: ${msg.tokenUsage.outputTokens}`
41
+ );
42
+ }
43
+ break;
44
+ }
45
+ }
46
+ } finally {
47
+ await session.close();
48
+ }
49
+ }
50
+
51
+ main().catch((err: unknown) => {
52
+ console.error('Error:', err);
53
+ process.exit(1);
54
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Spec mode: approve and hand off implementation to a new session.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/spec-mode-new-session.ts
6
+ */
7
+
8
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ import {
13
+ DroidInteractionMode,
14
+ ReasoningEffort,
15
+ ToolConfirmationOutcome,
16
+ ToolConfirmationType,
17
+ createSession,
18
+ } from '@factory/droid-sdk';
19
+
20
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
21
+ const outputPath = join(tempDir, 'hello.txt');
22
+
23
+ try {
24
+ const session = await createSession({
25
+ cwd: process.cwd(),
26
+ interactionMode: DroidInteractionMode.Spec,
27
+ specModeReasoningEffort: ReasoningEffort.High,
28
+ permissionHandler(params) {
29
+ const canExitSpec = params.toolUses.some(
30
+ (item) => item.details.type === ToolConfirmationType.ExitSpecMode
31
+ );
32
+ const onlyCreatesFile = params.toolUses.every(
33
+ (item) =>
34
+ item.details.type === ToolConfirmationType.Create &&
35
+ item.details.filePath === outputPath
36
+ );
37
+
38
+ if (canExitSpec) {
39
+ return ToolConfirmationOutcome.ProceedNewSessionHigh;
40
+ }
41
+ return onlyCreatesFile
42
+ ? ToolConfirmationOutcome.ProceedOnce
43
+ : ToolConfirmationOutcome.Cancel;
44
+ },
45
+ });
46
+
47
+ try {
48
+ for await (const _msg of session.stream(
49
+ `Plan then create ${outputPath} containing "Hello from Droid".`
50
+ )) {
51
+ // Consume the stream until the handoff implementation finishes.
52
+ }
53
+ } finally {
54
+ await session.close();
55
+ }
56
+
57
+ console.log(await readFile(outputPath, 'utf8'));
58
+ } finally {
59
+ await rm(tempDir, { recursive: true, force: true });
60
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Spec mode: approve and implement in the same session.
3
+ *
4
+ * Usage:
5
+ * npx tsx examples/spec-mode-same-session.ts
6
+ */
7
+
8
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ import {
13
+ DroidInteractionMode,
14
+ ReasoningEffort,
15
+ ToolConfirmationOutcome,
16
+ ToolConfirmationType,
17
+ createSession,
18
+ } from '@factory/droid-sdk';
19
+
20
+ const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
21
+ const outputPath = join(tempDir, 'hello.txt');
22
+
23
+ try {
24
+ const session = await createSession({
25
+ cwd: process.cwd(),
26
+ interactionMode: DroidInteractionMode.Spec,
27
+ specModeReasoningEffort: ReasoningEffort.High,
28
+ permissionHandler(params) {
29
+ const canExitSpec = params.toolUses.some(
30
+ (item) => item.details.type === ToolConfirmationType.ExitSpecMode
31
+ );
32
+ const onlyCreatesFile = params.toolUses.every(
33
+ (item) =>
34
+ item.details.type === ToolConfirmationType.Create &&
35
+ item.details.filePath === outputPath
36
+ );
37
+ const onlyEditsTempFile = params.toolUses.every(
38
+ (item) =>
39
+ item.details.type === ToolConfirmationType.ApplyPatch &&
40
+ item.details.filePath === outputPath
41
+ );
42
+ const onlyRunsTempCommand = params.toolUses.every(
43
+ (item) =>
44
+ item.details.type === ToolConfirmationType.Execute &&
45
+ item.details.fullCommand.includes(outputPath) &&
46
+ item.details.fullCommand.includes(tempDir)
47
+ );
48
+
49
+ return canExitSpec ||
50
+ onlyCreatesFile ||
51
+ onlyEditsTempFile ||
52
+ onlyRunsTempCommand
53
+ ? ToolConfirmationOutcome.ProceedOnce
54
+ : ToolConfirmationOutcome.Cancel;
55
+ },
56
+ });
57
+
58
+ try {
59
+ for await (const _msg of session.stream(
60
+ `Plan then create ${outputPath} containing "Hello from Droid".`
61
+ )) {
62
+ // Consume the stream until implementation finishes.
63
+ }
64
+ } finally {
65
+ await session.close();
66
+ }
67
+
68
+ console.log(await readFile(outputPath, 'utf8'));
69
+ } finally {
70
+ await rm(tempDir, { recursive: true, force: true });
71
+ }