@procedures/claude-cli 0.0.0-stage → 0.4.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 (65) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +92 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/events.d.ts +93 -0
  6. package/dist/events.d.ts.map +1 -0
  7. package/dist/events.js +153 -0
  8. package/dist/events.js.map +1 -0
  9. package/dist/executable.d.ts +28 -0
  10. package/dist/executable.d.ts.map +1 -0
  11. package/dist/executable.js +37 -0
  12. package/dist/executable.js.map +1 -0
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +8 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/module.d.ts +19 -0
  18. package/dist/module.d.ts.map +1 -0
  19. package/dist/module.js +81 -0
  20. package/dist/module.js.map +1 -0
  21. package/dist/procedures/claude/cancel.d.ts +13 -0
  22. package/dist/procedures/claude/cancel.d.ts.map +1 -0
  23. package/dist/procedures/claude/cancel.js +15 -0
  24. package/dist/procedures/claude/cancel.js.map +1 -0
  25. package/dist/procedures/claude/follow.d.ts +15 -0
  26. package/dist/procedures/claude/follow.d.ts.map +1 -0
  27. package/dist/procedures/claude/follow.js +21 -0
  28. package/dist/procedures/claude/follow.js.map +1 -0
  29. package/dist/procedures/claude/index.d.ts +5 -0
  30. package/dist/procedures/claude/index.d.ts.map +1 -0
  31. package/dist/procedures/claude/index.js +5 -0
  32. package/dist/procedures/claude/index.js.map +1 -0
  33. package/dist/procedures/claude/read.d.ts +12 -0
  34. package/dist/procedures/claude/read.d.ts.map +1 -0
  35. package/dist/procedures/claude/read.js +14 -0
  36. package/dist/procedures/claude/read.js.map +1 -0
  37. package/dist/procedures/claude/start.d.ts +14 -0
  38. package/dist/procedures/claude/start.d.ts.map +1 -0
  39. package/dist/procedures/claude/start.js +26 -0
  40. package/dist/procedures/claude/start.js.map +1 -0
  41. package/dist/register.d.ts +6 -0
  42. package/dist/register.d.ts.map +1 -0
  43. package/dist/register.js +8 -0
  44. package/dist/register.js.map +1 -0
  45. package/dist/turn.d.ts +142 -0
  46. package/dist/turn.d.ts.map +1 -0
  47. package/dist/turn.js +208 -0
  48. package/dist/turn.js.map +1 -0
  49. package/dist/types.d.ts +61 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +61 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +78 -3
  54. package/src/events.ts +250 -0
  55. package/src/executable.ts +50 -0
  56. package/src/index.ts +7 -0
  57. package/src/module.ts +116 -0
  58. package/src/procedures/claude/cancel.ts +17 -0
  59. package/src/procedures/claude/follow.ts +26 -0
  60. package/src/procedures/claude/index.ts +4 -0
  61. package/src/procedures/claude/read.ts +16 -0
  62. package/src/procedures/claude/start.ts +28 -0
  63. package/src/register.ts +10 -0
  64. package/src/turn.ts +317 -0
  65. package/src/types.ts +142 -0
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@procedures/claude-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.4.0",
5
+ "description": "One turn of the Claude Code CLI as a detached process that writes stream-json to a file: start, follow, normalised events and cancel.",
6
+ "keywords": [
7
+ "claude",
8
+ "claude-code",
9
+ "cli",
10
+ "stream-json",
11
+ "agent",
12
+ "procedures"
13
+ ],
14
+ "homepage": "https://mark1russell7.github.io/client/",
15
+ "bugs": {
16
+ "url": "https://github.com/mark1russell7/client/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mark1russell7/client.git",
21
+ "directory": "packages/claude-cli"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Mark Russell (https://github.com/mark1russell7)",
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./module": {
34
+ "types": "./dist/module.d.ts",
35
+ "import": "./dist/module.js"
36
+ },
37
+ "./register": {
38
+ "types": "./dist/register.d.ts",
39
+ "import": "./dist/register.js"
40
+ }
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "src",
45
+ "!src/**/*.test.ts",
46
+ "!src/**/*.test.tsx",
47
+ "!src/**/__tests__",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "sideEffects": [
51
+ "./dist/register.js"
52
+ ],
53
+ "engines": {
54
+ "node": ">=22"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "provenance": true
59
+ },
60
+ "dependencies": {
61
+ "@procedures/client": "0.4.0",
62
+ "@procedures/shell": "0.4.0",
63
+ "zod": "^4.6.5"
64
+ },
65
+ "devDependencies": {
66
+ "typescript": "^7.0.2",
67
+ "@types/node": "^22.0.0",
68
+ "@mark1russell7/cue": "^1.0.0",
69
+ "vitest": "^5.0.3"
70
+ },
71
+ "client": {
72
+ "procedures": "./dist/register.js"
73
+ },
74
+ "scripts": {
75
+ "build": "tsc -b",
76
+ "typecheck": "tsc --noEmit",
77
+ "clean": "rm -rf dist .tsbuildinfo",
78
+ "test": "vitest run",
79
+ "test:watch": "vitest"
80
+ }
6
81
  }
package/src/events.ts ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * The normaliser of the output of `claude -p --output-format stream-json --verbose`.
3
+ *
4
+ * Each line of that output is one JSON object. The normaliser turns one line into zero or more
5
+ * events of a small, stable set. It is pure: it reads no file and keeps no state.
6
+ */
7
+
8
+ /** The start of a session: the `system` line of the CLI. */
9
+ export interface ClaudeMetaEvent {
10
+ readonly type: "meta";
11
+ /** The subtype of the `system` line, for example `init`. */
12
+ readonly subtype?: string;
13
+ readonly sessionId?: string;
14
+ readonly model?: string;
15
+ readonly cwd?: string;
16
+ readonly tools?: readonly string[];
17
+ readonly permissionMode?: string;
18
+ }
19
+
20
+ /** A text block of the assistant. */
21
+ export interface ClaudeTextEvent {
22
+ readonly type: "text";
23
+ readonly text: string;
24
+ }
25
+
26
+ /** A part of a text block, with `--include-partial-messages`. */
27
+ export interface ClaudeTextDeltaEvent {
28
+ readonly type: "text-delta";
29
+ readonly text: string;
30
+ }
31
+
32
+ /** A thinking block of the assistant. The CLI gives an empty block in `-p` mode, and the normaliser drops it. */
33
+ export interface ClaudeThinkingEvent {
34
+ readonly type: "thinking";
35
+ readonly text: string;
36
+ }
37
+
38
+ /** A tool call of the assistant. */
39
+ export interface ClaudeToolUseEvent {
40
+ readonly type: "tool-use";
41
+ readonly id?: string;
42
+ readonly name: string;
43
+ readonly input: unknown;
44
+ }
45
+
46
+ /** The result of a tool call, as text. */
47
+ export interface ClaudeToolResultEvent {
48
+ readonly type: "tool-result";
49
+ readonly toolUseId?: string;
50
+ readonly output: string;
51
+ readonly isError: boolean;
52
+ }
53
+
54
+ /** The last line of a turn: the final text, the session, the cost and the duration. */
55
+ export interface ClaudeResultEvent {
56
+ readonly type: "result";
57
+ /** The subtype, for example `success` or `error_max_turns`. */
58
+ readonly subtype?: string;
59
+ readonly isError: boolean;
60
+ /** The final text of the assistant. */
61
+ readonly text?: string;
62
+ /** The session. Give it to `--resume` for the next turn. */
63
+ readonly sessionId?: string;
64
+ /** The cost of the turn, in US dollars. */
65
+ readonly costUsd?: number;
66
+ readonly durationMs?: number;
67
+ /** The number of model turns. */
68
+ readonly turns?: number;
69
+ readonly usage?: Readonly<Record<string, unknown>>;
70
+ readonly errors?: readonly string[];
71
+ }
72
+
73
+ /** A line that is not JSON. The CLI writes its errors and warnings as text. */
74
+ export interface ClaudeLogEvent {
75
+ readonly type: "log";
76
+ readonly text: string;
77
+ }
78
+
79
+ /** An error of the turn, for example a process that ended without a result. The normaliser does not make it. */
80
+ export interface ClaudeErrorEvent {
81
+ readonly type: "error";
82
+ readonly message: string;
83
+ }
84
+
85
+ /** One normalised event of a turn. */
86
+ export type ClaudeEvent =
87
+ | ClaudeMetaEvent
88
+ | ClaudeTextEvent
89
+ | ClaudeTextDeltaEvent
90
+ | ClaudeThinkingEvent
91
+ | ClaudeToolUseEvent
92
+ | ClaudeToolResultEvent
93
+ | ClaudeResultEvent
94
+ | ClaudeLogEvent
95
+ | ClaudeErrorEvent;
96
+
97
+ /** The types of the events. */
98
+ export type ClaudeEventType = ClaudeEvent["type"];
99
+
100
+ type Json = Record<string, unknown>;
101
+
102
+ function isObject(value: unknown): value is Json {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
104
+ }
105
+
106
+ function str(value: unknown): string | undefined {
107
+ return typeof value === "string" ? value : undefined;
108
+ }
109
+
110
+ function num(value: unknown): number | undefined {
111
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
112
+ }
113
+
114
+ /** The fields of `T`: a field that can be `undefined` becomes optional. */
115
+ type Optional<T> = { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } & {
116
+ [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
117
+ };
118
+
119
+ /** This function gives the fields whose value is not `undefined`. */
120
+ function defined<T extends Json>(fields: T): Optional<T> {
121
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)) as Optional<T>;
122
+ }
123
+
124
+ /**
125
+ * This function gives a value as text with a maximum length. It adds `…` when it cuts the text.
126
+ * The normaliser does not cut. A caller uses this function for a short preview.
127
+ */
128
+ export function preview(value: unknown, max = 400): string {
129
+ const text = typeof value === "string" ? value : JSON.stringify(value);
130
+ if (text === undefined) return "";
131
+ return text.length > max ? `${text.slice(0, max)}…` : text;
132
+ }
133
+
134
+ /** This function gives the content of a tool result as text. A block that is not text gives `[type]`. */
135
+ export function toolResultText(content: unknown): string {
136
+ if (typeof content === "string") return content;
137
+ if (Array.isArray(content)) {
138
+ return content
139
+ .map((block: unknown) => {
140
+ if (typeof block === "string") return block;
141
+ if (isObject(block)) return typeof block["text"] === "string" ? block["text"] : `[${str(block["type"]) ?? "block"}]`;
142
+ return "";
143
+ })
144
+ .join("\n");
145
+ }
146
+ return content === undefined || content === null ? "" : JSON.stringify(content);
147
+ }
148
+
149
+ function metaEvent(line: Json): ClaudeMetaEvent {
150
+ const tools = Array.isArray(line["tools"]) ? line["tools"].filter((tool): tool is string => typeof tool === "string") : undefined;
151
+ return defined({
152
+ type: "meta" as const,
153
+ subtype: str(line["subtype"]),
154
+ sessionId: str(line["session_id"]),
155
+ model: str(line["model"]),
156
+ cwd: str(line["cwd"]),
157
+ tools,
158
+ permissionMode: str(line["permissionMode"]),
159
+ });
160
+ }
161
+
162
+ function messageEvents(line: Json): ClaudeEvent[] {
163
+ const message = line["message"];
164
+ const content = isObject(message) && Array.isArray(message["content"]) ? (message["content"] as unknown[]) : [];
165
+ const fromAssistant = line["type"] === "assistant";
166
+ const events: ClaudeEvent[] = [];
167
+ for (const block of content) {
168
+ if (!isObject(block)) continue;
169
+ const kind = block["type"];
170
+ if (fromAssistant && kind === "text") {
171
+ const text = str(block["text"]);
172
+ if (text) events.push({ type: "text", text });
173
+ } else if (fromAssistant && kind === "thinking") {
174
+ const text = str(block["thinking"]);
175
+ if (text) events.push({ type: "thinking", text });
176
+ } else if (kind === "tool_use") {
177
+ const id = str(block["id"]);
178
+ events.push({ type: "tool-use", ...(id !== undefined ? { id } : {}), name: str(block["name"]) ?? "tool", input: block["input"] ?? {} });
179
+ } else if (kind === "tool_result") {
180
+ events.push(
181
+ defined({
182
+ type: "tool-result" as const,
183
+ toolUseId: str(block["tool_use_id"]),
184
+ output: toolResultText(block["content"]),
185
+ isError: block["is_error"] === true,
186
+ })
187
+ );
188
+ }
189
+ }
190
+ return events;
191
+ }
192
+
193
+ function resultEvent(line: Json): ClaudeResultEvent {
194
+ const errors = Array.isArray(line["errors"]) ? line["errors"].filter((error): error is string => typeof error === "string") : undefined;
195
+ return defined({
196
+ type: "result" as const,
197
+ subtype: str(line["subtype"]),
198
+ isError: line["is_error"] === true,
199
+ text: str(line["result"]),
200
+ sessionId: str(line["session_id"]),
201
+ costUsd: num(line["total_cost_usd"]) ?? num(line["cost_usd"]),
202
+ durationMs: num(line["duration_ms"]),
203
+ turns: num(line["num_turns"]),
204
+ usage: isObject(line["usage"]) ? line["usage"] : undefined,
205
+ errors: errors !== undefined && errors.length > 0 ? errors : undefined,
206
+ });
207
+ }
208
+
209
+ function streamEvents(line: Json): ClaudeEvent[] {
210
+ const event = line["event"];
211
+ if (!isObject(event) || event["type"] !== "content_block_delta") return [];
212
+ const delta = event["delta"];
213
+ if (!isObject(delta) || delta["type"] !== "text_delta") return [];
214
+ const text = str(delta["text"]);
215
+ return text ? [{ type: "text-delta", text }] : [];
216
+ }
217
+
218
+ /**
219
+ * This function normalises one line of `claude -p --output-format stream-json --verbose`. An
220
+ * empty line or an unknown JSON object gives no event. A line that is not JSON gives a `log` event.
221
+ */
222
+ export function normalizeLine(line: string): ClaudeEvent[] {
223
+ const trimmed = line.trim();
224
+ if (trimmed.length === 0) return [];
225
+ let parsed: unknown;
226
+ try {
227
+ parsed = JSON.parse(trimmed);
228
+ } catch {
229
+ return [{ type: "log", text: trimmed }];
230
+ }
231
+ if (!isObject(parsed)) return [];
232
+ switch (parsed["type"]) {
233
+ case "system":
234
+ return [metaEvent(parsed)];
235
+ case "assistant":
236
+ case "user":
237
+ return messageEvents(parsed);
238
+ case "result":
239
+ return [resultEvent(parsed)];
240
+ case "stream_event":
241
+ return streamEvents(parsed);
242
+ default:
243
+ return [];
244
+ }
245
+ }
246
+
247
+ /** This function normalises each line of a text. */
248
+ export function normalizeText(text: string): ClaudeEvent[] {
249
+ return text.split("\n").flatMap((line) => normalizeLine(line));
250
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Find the `claude` program of Claude Code.
3
+ */
4
+
5
+ import { existsSync } from "node:fs";
6
+ import { posix, win32 } from "node:path";
7
+
8
+ /** The environment variable that names the `claude` program. It comes before the search. */
9
+ export const CLAUDE_ENV = "CLAUDE_BIN";
10
+
11
+ /** The options of `findClaude`. Each one has a default from the host. */
12
+ export interface FindClaudeOptions {
13
+ /** The `claude` program. When it is set, the search does not occur. */
14
+ readonly executable?: string | undefined;
15
+ /** The environment (default: `process.env`). */
16
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
17
+ /** The operating system (default: `process.platform`). */
18
+ readonly platform?: NodeJS.Platform | undefined;
19
+ /** The function that tells if a file exists (default: `existsSync`). */
20
+ readonly exists?: ((path: string) => boolean) | undefined;
21
+ }
22
+
23
+ /**
24
+ * This function gives the paths that the search examines, in order: the folders of the `PATH`,
25
+ * then the install folders of the native installer. On Windows, it looks only for `claude.exe`,
26
+ * because a batch file (`claude.cmd`) does not start without a shell.
27
+ */
28
+ export function claudeCandidates(platform: NodeJS.Platform, env: Readonly<Record<string, string | undefined>>): string[] {
29
+ const windows = platform === "win32";
30
+ const path = windows ? win32 : posix;
31
+ const name = windows ? "claude.exe" : "claude";
32
+ const pathValue = windows ? (env["Path"] ?? env["PATH"]) : env["PATH"];
33
+ const folders = (pathValue ?? "").split(windows ? ";" : ":").filter((folder) => folder.length > 0);
34
+ const home = windows ? (env["USERPROFILE"] ?? env["HOME"]) : env["HOME"];
35
+ const installed = home !== undefined ? [path.join(home, ".local", "bin", name), path.join(home, ".claude", "local", name)] : [];
36
+ return [...folders.map((folder) => path.join(folder, name)), ...installed];
37
+ }
38
+
39
+ /**
40
+ * This function finds the `claude` program. The order is `executable`, then `CLAUDE_BIN`, then
41
+ * the `PATH` and the install folders. It gives `undefined` when it finds none.
42
+ */
43
+ export function findClaude(options: FindClaudeOptions = {}): string | undefined {
44
+ if (options.executable !== undefined && options.executable.length > 0) return options.executable;
45
+ const env = options.env ?? process.env;
46
+ const override = env[CLAUDE_ENV];
47
+ if (override !== undefined && override.length > 0) return override;
48
+ const exists = options.exists ?? existsSync;
49
+ return claudeCandidates(options.platform ?? process.platform, env).find((candidate) => exists(candidate));
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // The root registers nothing: import "<package>/register", or give the module to createRegistry()
2
+ export * from "./module.js";
3
+ export * from "./types.js";
4
+ export * from "./events.js";
5
+ export * from "./executable.js";
6
+ export * from "./turn.js";
7
+ export { claudeStart, claudeRead, claudeFollow, claudeCancel } from "./procedures/claude/index.js";
package/src/module.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The procedures of @procedures/claude-cli as data. An import of this file registers nothing.
3
+ *
4
+ * Scaffold a procedure with: mark procedure new <name> --path packages/claude-cli
5
+ */
6
+
7
+ import {
8
+ defineModule,
9
+ registerModules,
10
+ PROCEDURE_REGISTRY,
11
+ type AnyProcedure,
12
+ type ProcedureModule,
13
+ type ProcedureRegistry,
14
+ createProcedure,
15
+ zodAdapter,
16
+ } from "@procedures/client";
17
+ import { claudeStart } from "./procedures/claude/start.js";
18
+ import { claudeRead } from "./procedures/claude/read.js";
19
+ import { claudeFollow } from "./procedures/claude/follow.js";
20
+ import { claudeCancel } from "./procedures/claude/cancel.js";
21
+ import {
22
+ ClaudeStartInputSchema,
23
+ ClaudeStartOutputSchema,
24
+ ClaudeReadInputSchema,
25
+ ClaudeReadOutputSchema,
26
+ ClaudeFollowInputSchema,
27
+ ClaudeFollowOutputSchema,
28
+ ClaudeCancelInputSchema,
29
+ ClaudeCancelOutputSchema,
30
+ type ClaudeStartInput,
31
+ type ClaudeStartOutput,
32
+ type ClaudeReadInput,
33
+ type ClaudeReadOutput,
34
+ type ClaudeFollowInput,
35
+ type ClaudeFollowOutput,
36
+ type ClaudeCancelInput,
37
+ type ClaudeCancelOutput,
38
+ } from "./types.js";
39
+
40
+ /** The effects of a read of the output file. The check of the process id reads the turn process. */
41
+ const READS_TURN = { reads: ["fs", "jobs"], writes: [] } as const;
42
+
43
+ export const claudeStartProcedure: AnyProcedure = createProcedure()
44
+ .path(["claude", "start"])
45
+ .input(zodAdapter<ClaudeStartInput>(ClaudeStartInputSchema))
46
+ .output(zodAdapter<ClaudeStartOutput>(ClaudeStartOutputSchema))
47
+ .meta({
48
+ description: "Start one turn of the Claude Code CLI as a detached process that writes to a file",
49
+ args: ["prompt"],
50
+ shorts: { cwd: "C" },
51
+ output: "json",
52
+ // The agent of the turn can read and change files, start programs and use the network
53
+ effects: { reads: ["fs", "network", "process"], writes: ["fs", "network", "process", "jobs"] },
54
+ })
55
+ .handler(async (input: ClaudeStartInput): Promise<ClaudeStartOutput> => claudeStart(input))
56
+ .build() as AnyProcedure;
57
+
58
+ export const claudeReadProcedure: AnyProcedure = createProcedure()
59
+ .path(["claude", "read"])
60
+ .input(zodAdapter<ClaudeReadInput>(ClaudeReadInputSchema))
61
+ .output(zodAdapter<ClaudeReadOutput>(ClaudeReadOutputSchema))
62
+ .meta({
63
+ description: "Read the new events of a turn from its output file",
64
+ args: ["outputFile"],
65
+ shorts: {},
66
+ output: "json",
67
+ effects: READS_TURN,
68
+ })
69
+ .handler(async (input: ClaudeReadInput): Promise<ClaudeReadOutput> => claudeRead(input))
70
+ .build() as AnyProcedure;
71
+
72
+ export const claudeFollowProcedure: AnyProcedure = createProcedure()
73
+ .path(["claude", "follow"])
74
+ .input(zodAdapter<ClaudeFollowInput>(ClaudeFollowInputSchema))
75
+ .output(zodAdapter<ClaudeFollowOutput>(ClaudeFollowOutputSchema))
76
+ .meta({
77
+ description: "Follow the output file of a turn and stream its events until the turn ends",
78
+ args: ["outputFile"],
79
+ shorts: {},
80
+ output: "json",
81
+ streaming: true,
82
+ // A turn has no time limit: the stream ends with the turn
83
+ stream: { unbounded: true },
84
+ effects: READS_TURN,
85
+ })
86
+ .handler(claudeFollow)
87
+ .build() as AnyProcedure;
88
+
89
+ export const claudeCancelProcedure: AnyProcedure = createProcedure()
90
+ .path(["claude", "cancel"])
91
+ .input(zodAdapter<ClaudeCancelInput>(ClaudeCancelInputSchema))
92
+ .output(zodAdapter<ClaudeCancelOutput>(ClaudeCancelOutputSchema))
93
+ .meta({
94
+ description: "Stop a turn and the processes that it started",
95
+ args: ["pid"],
96
+ shorts: {},
97
+ output: "json",
98
+ // It stops the process tree of the id that it gets
99
+ effects: { reads: ["jobs"], writes: ["jobs"], destructive: true, idempotent: true },
100
+ })
101
+ .handler(async (input: ClaudeCancelInput): Promise<ClaudeCancelOutput> => claudeCancel(input))
102
+ .build() as AnyProcedure;
103
+
104
+ export const claudeCliProcedures: AnyProcedure[] = [claudeStartProcedure, claudeReadProcedure, claudeFollowProcedure, claudeCancelProcedure];
105
+
106
+ export const claudeCliModule: ProcedureModule = defineModule("@procedures/claude-cli", claudeCliProcedures);
107
+
108
+ export default claudeCliModule;
109
+
110
+ /**
111
+ * This function registers the procedures of @procedures/claude-cli in a registry. The default is the
112
+ * global registry.
113
+ */
114
+ export function registerClaudeCliProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
115
+ registerModules(registry, [claudeCliModule]);
116
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * claude.cancel procedure
3
+ *
4
+ * This procedure stops a turn and the processes that it started, by the process id of the turn.
5
+ * It also stops a turn that a previous host started.
6
+ */
7
+
8
+ import { cancelTurn } from "../../turn.js";
9
+ import type { ClaudeCancelInput, ClaudeCancelOutput } from "../../types.js";
10
+
11
+ /**
12
+ * This function stops the process tree of the turn. It gives `stopped: false` and a reason when
13
+ * the process has ended.
14
+ */
15
+ export async function claudeCancel(input: ClaudeCancelInput): Promise<ClaudeCancelOutput> {
16
+ return cancelTurn(input.pid);
17
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * claude.follow procedure
3
+ *
4
+ * This procedure follows the output file of a turn and streams its events until the turn ends.
5
+ * When the caller stops reading, or its signal aborts, the follow stops. The turn continues.
6
+ */
7
+
8
+ import { followTurn } from "../../turn.js";
9
+ import type { ClaudeFollowInput, ClaudeFollowOutput } from "../../types.js";
10
+
11
+ /**
12
+ * This function gives each event of the turn. It stops after the result, or after an error event
13
+ * when the process ends without a result.
14
+ */
15
+ export async function* claudeFollow(
16
+ input: ClaudeFollowInput,
17
+ ctx?: { signal?: AbortSignal | undefined }
18
+ ): AsyncGenerator<ClaudeFollowOutput, void, undefined> {
19
+ yield* followTurn({
20
+ outputFile: input.outputFile,
21
+ offset: input.offset,
22
+ pid: input.pid,
23
+ pollMs: input.pollMs,
24
+ signal: ctx?.signal,
25
+ });
26
+ }
@@ -0,0 +1,4 @@
1
+ export { claudeStart } from "./start.js";
2
+ export { claudeRead } from "./read.js";
3
+ export { claudeFollow } from "./follow.js";
4
+ export { claudeCancel } from "./cancel.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * claude.read procedure
3
+ *
4
+ * This procedure reads the new events of a turn from its output file, from a byte offset.
5
+ */
6
+
7
+ import { readTurn } from "../../turn.js";
8
+ import type { ClaudeReadInput, ClaudeReadOutput } from "../../types.js";
9
+
10
+ /**
11
+ * This function reads the complete lines after `input.offset` and gives their events and the
12
+ * offset for the next read.
13
+ */
14
+ export async function claudeRead(input: ClaudeReadInput): Promise<ClaudeReadOutput> {
15
+ return readTurn({ outputFile: input.outputFile, offset: input.offset, pid: input.pid });
16
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * claude.start procedure
3
+ *
4
+ * This procedure starts one turn of the Claude Code CLI as a detached process that writes to a
5
+ * file. The turn continues when the host stops.
6
+ */
7
+
8
+ import { startTurn, type Spawner } from "../../turn.js";
9
+ import type { ClaudeStartInput, ClaudeStartOutput } from "../../types.js";
10
+
11
+ /**
12
+ * This function starts a turn. The variables of `input.env` go on top of the environment of the
13
+ * host. It gives the process id and the output file of the turn.
14
+ */
15
+ export async function claudeStart(input: ClaudeStartInput, spawner?: Spawner): Promise<ClaudeStartOutput> {
16
+ const turn = await startTurn({
17
+ prompt: input.prompt,
18
+ cwd: input.cwd,
19
+ args: input.args,
20
+ outputFile: input.outputFile,
21
+ promptFile: input.promptFile,
22
+ executable: input.executable,
23
+ executableArgs: input.executableArgs,
24
+ env: input.env !== undefined ? { ...process.env, ...input.env } : undefined,
25
+ spawner,
26
+ });
27
+ return { ...turn, args: [...turn.args] };
28
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * An import of this file registers the procedures of @procedures/claude-cli in the global registry:
3
+ * `import "@procedures/claude-cli/register"`. The package root registers nothing.
4
+ */
5
+
6
+ import { registerClaudeCliProcedures } from "./module.js";
7
+
8
+ export * from "./module.js";
9
+
10
+ registerClaudeCliProcedures();