@anagnole/claude-cli 0.1.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,6 @@
1
+ /** Buffers partial lines from a stream and yields complete parsed JSON objects. */
2
+ export declare class NdjsonParser {
3
+ private buffer;
4
+ feed(chunk: string): unknown[];
5
+ flush(): unknown[];
6
+ }
@@ -0,0 +1,35 @@
1
+ /** Buffers partial lines from a stream and yields complete parsed JSON objects. */
2
+ export class NdjsonParser {
3
+ buffer = "";
4
+ feed(chunk) {
5
+ this.buffer += chunk;
6
+ const lines = this.buffer.split("\n");
7
+ this.buffer = lines.pop() ?? "";
8
+ const results = [];
9
+ for (const line of lines) {
10
+ const trimmed = line.trim();
11
+ if (!trimmed)
12
+ continue;
13
+ try {
14
+ results.push(JSON.parse(trimmed));
15
+ }
16
+ catch {
17
+ // Non-JSON line, skip
18
+ }
19
+ }
20
+ return results;
21
+ }
22
+ flush() {
23
+ const remaining = this.buffer.trim();
24
+ this.buffer = "";
25
+ if (!remaining)
26
+ return [];
27
+ try {
28
+ return [JSON.parse(remaining)];
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ }
35
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/cli/parser.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,OAAO,YAAY;IACf,MAAM,GAAG,EAAE,CAAC;IAEpB,IAAI,CAAC,KAAa;QAChB,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;QACrB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;QAEhC,MAAM,OAAO,GAAc,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,OAAO;gBAAE,SAAS;YACvB,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,sBAAsB;YACxB,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK;QACH,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,29 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ export interface SpawnOptions {
3
+ prompt: string;
4
+ model?: string;
5
+ systemPrompt?: string;
6
+ appendSystemPrompt?: boolean;
7
+ resumeSessionId?: string;
8
+ continueConversation?: boolean;
9
+ forkSession?: boolean;
10
+ streaming: boolean;
11
+ effort?: string;
12
+ jsonSchema?: unknown;
13
+ permissionMode?: string;
14
+ allowedTools?: string[];
15
+ disallowedTools?: string[];
16
+ cliTools?: string;
17
+ mcpConfig?: string;
18
+ strictMcpConfig?: boolean;
19
+ worktree?: string;
20
+ workingDirectory?: string;
21
+ maxTurns?: number;
22
+ maxBudgetUsd?: number;
23
+ fallbackModel?: string;
24
+ dangerouslySkipPermissions?: boolean;
25
+ addDirs?: string[];
26
+ /** Path to the claude CLI binary. Defaults to "claude". */
27
+ claudePath?: string;
28
+ }
29
+ export declare function spawnClaude(options: SpawnOptions): ChildProcess;
@@ -0,0 +1,77 @@
1
+ import { spawn } from "node:child_process";
2
+ export function spawnClaude(options) {
3
+ const args = ["--print"];
4
+ // Output format
5
+ if (options.streaming) {
6
+ args.push("--output-format", "stream-json", "--verbose", "--include-partial-messages");
7
+ }
8
+ else {
9
+ args.push("--output-format", "json");
10
+ }
11
+ // Model
12
+ if (options.model)
13
+ args.push("--model", options.model);
14
+ if (options.fallbackModel)
15
+ args.push("--fallback-model", options.fallbackModel);
16
+ // System prompt
17
+ if (options.systemPrompt) {
18
+ if (options.appendSystemPrompt) {
19
+ args.push("--append-system-prompt", options.systemPrompt);
20
+ }
21
+ else {
22
+ args.push("--system-prompt", options.systemPrompt);
23
+ }
24
+ }
25
+ // Session management
26
+ if (options.resumeSessionId)
27
+ args.push("--resume", options.resumeSessionId);
28
+ if (options.continueConversation)
29
+ args.push("--continue");
30
+ if (options.forkSession)
31
+ args.push("--fork-session");
32
+ // Effort & structured output
33
+ if (options.effort)
34
+ args.push("--effort", options.effort);
35
+ if (options.jsonSchema)
36
+ args.push("--json-schema", JSON.stringify(options.jsonSchema));
37
+ // Permission & tools
38
+ if (options.permissionMode)
39
+ args.push("--permission-mode", options.permissionMode);
40
+ if (options.dangerouslySkipPermissions)
41
+ args.push("--dangerously-skip-permissions");
42
+ if (options.allowedTools?.length)
43
+ args.push("--allowedTools", ...options.allowedTools);
44
+ if (options.disallowedTools?.length)
45
+ args.push("--disallowedTools", ...options.disallowedTools);
46
+ if (options.cliTools != null)
47
+ args.push("--tools", options.cliTools);
48
+ // MCP
49
+ if (options.mcpConfig)
50
+ args.push(`--mcp-config=${options.mcpConfig}`);
51
+ if (options.strictMcpConfig)
52
+ args.push("--strict-mcp-config");
53
+ // Worktree & directories
54
+ if (options.worktree)
55
+ args.push("--worktree", options.worktree);
56
+ if (options.addDirs?.length)
57
+ args.push("--add-dir", ...options.addDirs);
58
+ // Safety limits
59
+ if (options.maxTurns != null)
60
+ args.push("--max-turns", String(options.maxTurns));
61
+ if (options.maxBudgetUsd != null)
62
+ args.push("--max-budget-usd", String(options.maxBudgetUsd));
63
+ // Prompt is always last
64
+ if (options.prompt) {
65
+ args.push("--", options.prompt);
66
+ }
67
+ // Prevent recursion / interference if running inside a Claude Code session
68
+ const env = { ...process.env };
69
+ delete env.CLAUDECODE;
70
+ delete env.CLAUDE_CODE_ENTRYPOINT;
71
+ return spawn(options.claudePath ?? "claude", args, {
72
+ cwd: options.workingDirectory,
73
+ stdio: ["ignore", "pipe", "pipe"],
74
+ env,
75
+ });
76
+ }
77
+ //# sourceMappingURL=spawn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn.js","sourceRoot":"","sources":["../../src/cli/spawn.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export { spawnClaude } from "./cli/spawn.js";
2
+ export type { SpawnOptions } from "./cli/spawn.js";
3
+ export { NdjsonParser } from "./cli/parser.js";
4
+ export { SessionMap } from "./session/session-map.js";
5
+ export { extractPrompt, extractSystem, warnUnsupported } from "./transform/request.js";
6
+ export { buildResponse, generateMsgId } from "./transform/response.js";
7
+ export { createStreamState, transformEvent } from "./transform/stream.js";
8
+ export type { StreamState } from "./transform/stream.js";
9
+ export type { ContentBlock, SystemBlock, MessageParam, MessagesRequest, MessagesResponse, Usage, ApiError, } from "./types.js";
10
+ export { apiError } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // CLI
2
+ export { spawnClaude } from "./cli/spawn.js";
3
+ export { NdjsonParser } from "./cli/parser.js";
4
+ // Session
5
+ export { SessionMap } from "./session/session-map.js";
6
+ // Transform
7
+ export { extractPrompt, extractSystem, warnUnsupported } from "./transform/request.js";
8
+ export { buildResponse, generateMsgId } from "./transform/response.js";
9
+ export { createStreamState, transformEvent } from "./transform/stream.js";
10
+ export { apiError } from "./types.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM;AACN,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,UAAU;AACV,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAEtD,YAAY;AACZ,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAa1E,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { MessageParam } from "../types.js";
2
+ export declare class SessionMap {
3
+ private map;
4
+ private maxEntries;
5
+ /** Hash all messages except the last one to fingerprint the conversation context. */
6
+ static hashContext(messages: MessageParam[]): string;
7
+ /** Look up a CLI session ID for a given context hash + model. */
8
+ lookup(hash: string, model: string): string | null;
9
+ /**
10
+ * Store a session after a successful CLI invocation.
11
+ * `fullMessages` should include the original messages + the assistant response,
12
+ * so the *next* request with that history will find this session.
13
+ */
14
+ store(fullMessages: MessageParam[], cliSessionId: string, model: string): void;
15
+ private evictIfNeeded;
16
+ }
@@ -0,0 +1,55 @@
1
+ import { createHash } from "node:crypto";
2
+ export class SessionMap {
3
+ map = new Map();
4
+ maxEntries = 1000;
5
+ /** Hash all messages except the last one to fingerprint the conversation context. */
6
+ static hashContext(messages) {
7
+ const context = messages.slice(0, -1);
8
+ if (context.length === 0)
9
+ return "empty";
10
+ const payload = JSON.stringify(context);
11
+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
12
+ }
13
+ /** Look up a CLI session ID for a given context hash + model. */
14
+ lookup(hash, model) {
15
+ const entry = this.map.get(hash);
16
+ if (!entry || entry.model !== model)
17
+ return null;
18
+ entry.lastUsedAt = Date.now();
19
+ return entry.cliSessionId;
20
+ }
21
+ /**
22
+ * Store a session after a successful CLI invocation.
23
+ * `fullMessages` should include the original messages + the assistant response,
24
+ * so the *next* request with that history will find this session.
25
+ */
26
+ store(fullMessages, cliSessionId, model) {
27
+ // The next request will hash messages[0..n-1] (all except its new user msg),
28
+ // which equals the full messages from this turn.
29
+ const nextHash = createHash("sha256")
30
+ .update(JSON.stringify(fullMessages))
31
+ .digest("hex")
32
+ .slice(0, 16);
33
+ this.map.set(nextHash, {
34
+ cliSessionId,
35
+ lastUsedAt: Date.now(),
36
+ model,
37
+ });
38
+ this.evictIfNeeded();
39
+ }
40
+ evictIfNeeded() {
41
+ if (this.map.size <= this.maxEntries)
42
+ return;
43
+ let oldestKey = null;
44
+ let oldestTime = Infinity;
45
+ for (const [key, entry] of this.map) {
46
+ if (entry.lastUsedAt < oldestTime) {
47
+ oldestTime = entry.lastUsedAt;
48
+ oldestKey = key;
49
+ }
50
+ }
51
+ if (oldestKey)
52
+ this.map.delete(oldestKey);
53
+ }
54
+ }
55
+ //# sourceMappingURL=session-map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-map.js","sourceRoot":"","sources":["../../src/session/session-map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASzC,MAAM,OAAO,UAAU;IACb,GAAG,GAAG,IAAI,GAAG,EAAwB,CAAC;IACtC,UAAU,GAAG,IAAI,CAAC;IAE1B,qFAAqF;IACrF,MAAM,CAAC,WAAW,CAAC,QAAwB;QACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACtC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,OAAO,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACxC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,iEAAiE;IACjE,MAAM,CAAC,IAAY,EAAE,KAAa;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO,IAAI,CAAC;QACjD,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,YAAY,CAAC;IAC5B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,YAA4B,EAAE,YAAoB,EAAE,KAAa;QACrE,6EAA6E;QAC7E,iDAAiD;QACjD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;aAClC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;aACpC,MAAM,CAAC,KAAK,CAAC;aACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAEhB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE;YACrB,YAAY;YACZ,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;YACtB,KAAK;SACN,CAAC,CAAC;QAEH,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAEO,aAAa;QACnB,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC7C,IAAI,SAAS,GAAkB,IAAI,CAAC;QACpC,IAAI,UAAU,GAAG,QAAQ,CAAC;QAC1B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,UAAU,GAAG,UAAU,EAAE,CAAC;gBAClC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;gBAC9B,SAAS,GAAG,GAAG,CAAC;YAClB,CAAC;QACH,CAAC;QACD,IAAI,SAAS;YAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ import type { MessageParam, MessagesRequest, SystemBlock } from "../types.js";
2
+ interface Logger {
3
+ warn(msg: string): void;
4
+ }
5
+ /** Extract the text content from the last user message. */
6
+ export declare function extractPrompt(messages: MessageParam[]): string;
7
+ /** Flatten system prompt to a single string. */
8
+ export declare function extractSystem(system: string | SystemBlock[] | undefined): string | undefined;
9
+ /** Log warnings for API params the CLI truly can't handle. */
10
+ export declare function warnUnsupported(req: MessagesRequest, log: Logger): void;
11
+ export {};
@@ -0,0 +1,40 @@
1
+ /** Extract the text content from the last user message. */
2
+ export function extractPrompt(messages) {
3
+ const last = messages[messages.length - 1];
4
+ if (!last || last.role !== "user") {
5
+ throw new Error("Last message must have role 'user'");
6
+ }
7
+ if (typeof last.content === "string")
8
+ return last.content;
9
+ return last.content
10
+ .filter((b) => b.type === "text")
11
+ .map((b) => b.text)
12
+ .join("\n");
13
+ }
14
+ /** Flatten system prompt to a single string. */
15
+ export function extractSystem(system) {
16
+ if (!system)
17
+ return undefined;
18
+ if (typeof system === "string")
19
+ return system;
20
+ return system
21
+ .filter((b) => b.type === "text")
22
+ .map((b) => b.text)
23
+ .join("\n");
24
+ }
25
+ /** Log warnings for API params the CLI truly can't handle. */
26
+ export function warnUnsupported(req, log) {
27
+ if (req.temperature != null)
28
+ log.warn("temperature param ignored (CLI does not support it)");
29
+ if (req.top_p != null)
30
+ log.warn("top_p param ignored");
31
+ if (req.top_k != null)
32
+ log.warn("top_k param ignored");
33
+ if (req.stop_sequences?.length)
34
+ log.warn("stop_sequences param ignored");
35
+ if (req.tools?.length)
36
+ log.warn("tools param ignored (CLI uses its own built-in tools, use allowed_tools/disallowed_tools instead)");
37
+ if (req.tool_choice != null)
38
+ log.warn("tool_choice param ignored");
39
+ }
40
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/transform/request.ts"],"names":[],"mappings":"AAMA,2DAA2D;AAC3D,MAAM,UAAU,aAAa,CAAC,QAAwB;IACpD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,OAAO,CAAC;IAC1D,OAAO,IAAI,CAAC,OAAO;SAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;SAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,aAAa,CAC3B,MAA0C;IAE1C,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC9C,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;SAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,eAAe,CAAC,GAAoB,EAAE,GAAW;IAC/D,IAAI,GAAG,CAAC,WAAW,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;IAC7F,IAAI,GAAG,CAAC,KAAK,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACvD,IAAI,GAAG,CAAC,KAAK,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IACvD,IAAI,GAAG,CAAC,cAAc,EAAE,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;IACzE,IAAI,GAAG,CAAC,KAAK,EAAE,MAAM;QAAE,GAAG,CAAC,IAAI,CAAC,mGAAmG,CAAC,CAAC;IACrI,IAAI,GAAG,CAAC,WAAW,IAAI,IAAI;QAAE,GAAG,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;AACrE,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { MessagesResponse } from "../types.js";
2
+ export declare function generateMsgId(): string;
3
+ interface CliResult {
4
+ result: string;
5
+ stop_reason?: string;
6
+ session_id: string;
7
+ total_cost_usd?: number;
8
+ duration_ms?: number;
9
+ num_turns?: number;
10
+ usage?: {
11
+ input_tokens?: number;
12
+ output_tokens?: number;
13
+ cache_creation_input_tokens?: number;
14
+ cache_read_input_tokens?: number;
15
+ };
16
+ }
17
+ export declare function buildResponse(cli: CliResult, model: string): MessagesResponse;
18
+ export {};
@@ -0,0 +1,41 @@
1
+ import crypto from "node:crypto";
2
+ export function generateMsgId() {
3
+ return "msg_" + crypto.randomBytes(18).toString("base64url");
4
+ }
5
+ export function buildResponse(cli, model) {
6
+ const usage = {
7
+ input_tokens: cli.usage?.input_tokens ?? 0,
8
+ output_tokens: cli.usage?.output_tokens ?? 0,
9
+ };
10
+ if (cli.usage?.cache_creation_input_tokens) {
11
+ usage.cache_creation_input_tokens = cli.usage.cache_creation_input_tokens;
12
+ }
13
+ if (cli.usage?.cache_read_input_tokens) {
14
+ usage.cache_read_input_tokens = cli.usage.cache_read_input_tokens;
15
+ }
16
+ // Build content blocks — for now just text, but could include tool_use in the future
17
+ const content = [{ type: "text", text: cli.result }];
18
+ return {
19
+ id: generateMsgId(),
20
+ type: "message",
21
+ role: "assistant",
22
+ content,
23
+ model,
24
+ stop_reason: mapStopReason(cli.stop_reason),
25
+ stop_sequence: null,
26
+ usage,
27
+ // CLI extras
28
+ session_id: cli.session_id,
29
+ cost_usd: cli.total_cost_usd,
30
+ duration_ms: cli.duration_ms,
31
+ num_turns: cli.num_turns,
32
+ };
33
+ }
34
+ function mapStopReason(reason) {
35
+ if (reason === "max_tokens")
36
+ return "max_tokens";
37
+ if (reason === "stop_sequence")
38
+ return "stop_sequence";
39
+ return "end_turn";
40
+ }
41
+ //# sourceMappingURL=response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.js","sourceRoot":"","sources":["../../src/transform/response.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AAGjC,MAAM,UAAU,aAAa;IAC3B,OAAO,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/D,CAAC;AAiBD,MAAM,UAAU,aAAa,CAAC,GAAc,EAAE,KAAa;IACzD,MAAM,KAAK,GAAU;QACnB,YAAY,EAAE,GAAG,CAAC,KAAK,EAAE,YAAY,IAAI,CAAC;QAC1C,aAAa,EAAE,GAAG,CAAC,KAAK,EAAE,aAAa,IAAI,CAAC;KAC7C,CAAC;IACF,IAAI,GAAG,CAAC,KAAK,EAAE,2BAA2B,EAAE,CAAC;QAC3C,KAAK,CAAC,2BAA2B,GAAG,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC;IAC5E,CAAC;IACD,IAAI,GAAG,CAAC,KAAK,EAAE,uBAAuB,EAAE,CAAC;QACvC,KAAK,CAAC,uBAAuB,GAAG,GAAG,CAAC,KAAK,CAAC,uBAAuB,CAAC;IACpE,CAAC;IAED,qFAAqF;IACrF,MAAM,OAAO,GAAmB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAErE,OAAO;QACL,EAAE,EAAE,aAAa,EAAE;QACnB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,WAAW;QACjB,OAAO;QACP,KAAK;QACL,WAAW,EAAE,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC;QAC3C,aAAa,EAAE,IAAI;QACnB,KAAK;QACL,aAAa;QACb,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,QAAQ,EAAE,GAAG,CAAC,cAAc;QAC5B,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CACpB,MAAe;IAEf,IAAI,MAAM,KAAK,YAAY;QAAE,OAAO,YAAY,CAAC;IACjD,IAAI,MAAM,KAAK,eAAe;QAAE,OAAO,eAAe,CAAC;IACvD,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Transforms CLI stream-json NDJSON events into Anthropic SSE format.
3
+ *
4
+ * The CLI's `--output-format stream-json --verbose` emits events like:
5
+ * {"type": "stream_event", "event": { ...anthropic_event... }, "session_id": "..."}
6
+ *
7
+ * The inner `event` payload is almost identical to the Anthropic SSE data.
8
+ * We unwrap it, inject our msg_id where needed, and format as SSE lines.
9
+ */
10
+ export interface StreamState {
11
+ msgId: string;
12
+ model: string;
13
+ sessionId: string | null;
14
+ inputTokens: number;
15
+ outputTokens: number;
16
+ started: boolean;
17
+ finished: boolean;
18
+ }
19
+ export declare function createStreamState(model: string): StreamState;
20
+ /**
21
+ * Transform a parsed CLI NDJSON object into zero or more SSE strings.
22
+ * Returns an array of SSE-formatted strings to write to the response.
23
+ */
24
+ export declare function transformEvent(obj: Record<string, unknown>, state: StreamState): string[];
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Transforms CLI stream-json NDJSON events into Anthropic SSE format.
3
+ *
4
+ * The CLI's `--output-format stream-json --verbose` emits events like:
5
+ * {"type": "stream_event", "event": { ...anthropic_event... }, "session_id": "..."}
6
+ *
7
+ * The inner `event` payload is almost identical to the Anthropic SSE data.
8
+ * We unwrap it, inject our msg_id where needed, and format as SSE lines.
9
+ */
10
+ import { generateMsgId } from "./response.js";
11
+ /** Format a single SSE event. */
12
+ function sse(event, data) {
13
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
14
+ }
15
+ export function createStreamState(model) {
16
+ return {
17
+ msgId: generateMsgId(),
18
+ model,
19
+ sessionId: null,
20
+ inputTokens: 0,
21
+ outputTokens: 0,
22
+ started: false,
23
+ finished: false,
24
+ };
25
+ }
26
+ /**
27
+ * Transform a parsed CLI NDJSON object into zero or more SSE strings.
28
+ * Returns an array of SSE-formatted strings to write to the response.
29
+ */
30
+ export function transformEvent(obj, state) {
31
+ const results = [];
32
+ // Capture session_id from any event
33
+ if (typeof obj.session_id === "string") {
34
+ state.sessionId = obj.session_id;
35
+ }
36
+ // stream_event wraps Anthropic-format events
37
+ if (obj.type === "stream_event" && obj.event && typeof obj.event === "object") {
38
+ const event = obj.event;
39
+ const eventType = event.type;
40
+ switch (eventType) {
41
+ case "message_start": {
42
+ state.started = true;
43
+ // Rebuild message_start with our msg_id
44
+ const msg = (event.message ?? {});
45
+ const usage = (msg.usage ?? {});
46
+ if (typeof usage.input_tokens === "number")
47
+ state.inputTokens = usage.input_tokens;
48
+ results.push(sse("message_start", {
49
+ type: "message_start",
50
+ message: {
51
+ id: state.msgId,
52
+ type: "message",
53
+ role: "assistant",
54
+ content: [],
55
+ model: state.model,
56
+ stop_reason: null,
57
+ stop_sequence: null,
58
+ usage: { input_tokens: state.inputTokens, output_tokens: 1 },
59
+ },
60
+ }));
61
+ break;
62
+ }
63
+ case "content_block_start":
64
+ results.push(sse("content_block_start", event));
65
+ break;
66
+ case "content_block_delta":
67
+ results.push(sse("content_block_delta", event));
68
+ break;
69
+ case "content_block_stop":
70
+ results.push(sse("content_block_stop", event));
71
+ break;
72
+ case "message_delta": {
73
+ const usage = (event.usage ?? {});
74
+ if (typeof usage.output_tokens === "number")
75
+ state.outputTokens = usage.output_tokens;
76
+ results.push(sse("message_delta", event));
77
+ break;
78
+ }
79
+ case "message_stop":
80
+ state.finished = true;
81
+ results.push(sse("message_stop", { type: "message_stop" }));
82
+ break;
83
+ default:
84
+ // Pass through unknown event types (ping, etc.)
85
+ if (eventType === "ping") {
86
+ results.push(sse("ping", { type: "ping" }));
87
+ }
88
+ break;
89
+ }
90
+ }
91
+ // result event — capture session_id and usage for session map
92
+ if (obj.type === "result") {
93
+ if (typeof obj.session_id === "string")
94
+ state.sessionId = obj.session_id;
95
+ const usage = obj.usage;
96
+ if (usage) {
97
+ if (typeof usage.input_tokens === "number")
98
+ state.inputTokens = usage.input_tokens;
99
+ if (typeof usage.output_tokens === "number")
100
+ state.outputTokens = usage.output_tokens;
101
+ }
102
+ // Don't emit SSE — message_stop was already sent by the stream events
103
+ }
104
+ return results;
105
+ }
106
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/transform/stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ export interface ContentBlock {
2
+ type: string;
3
+ text?: string;
4
+ id?: string;
5
+ name?: string;
6
+ input?: unknown;
7
+ tool_use_id?: string;
8
+ content?: string | ContentBlock[];
9
+ is_error?: boolean;
10
+ }
11
+ export interface SystemBlock {
12
+ type: "text";
13
+ text: string;
14
+ cache_control?: {
15
+ type: "ephemeral";
16
+ ttl?: string;
17
+ };
18
+ }
19
+ export interface MessageParam {
20
+ role: "user" | "assistant";
21
+ content: string | ContentBlock[];
22
+ }
23
+ export interface MessagesRequest {
24
+ model: string;
25
+ messages: MessageParam[];
26
+ max_tokens?: number;
27
+ system?: string | SystemBlock[];
28
+ stream?: boolean;
29
+ temperature?: number;
30
+ top_p?: number;
31
+ top_k?: number;
32
+ stop_sequences?: string[];
33
+ tools?: unknown[];
34
+ tool_choice?: unknown;
35
+ thinking?: unknown;
36
+ metadata?: {
37
+ user_id?: string;
38
+ };
39
+ effort?: string;
40
+ json_schema?: unknown;
41
+ /** Permission mode: "plan" | "bypassPermissions" | etc. Maps to --permission-mode */
42
+ permission_mode?: string;
43
+ /** Tools to auto-approve without prompting. Maps to --allowedTools */
44
+ allowed_tools?: string[];
45
+ /** Tools to remove from context entirely. Maps to --disallowedTools */
46
+ disallowed_tools?: string[];
47
+ /** Restrict built-in tools. "" disables all, "default" for all. Maps to --tools */
48
+ cli_tools?: string;
49
+ /** Path to MCP server config JSON file. Maps to --mcp-config */
50
+ mcp_config?: string;
51
+ /** Use only MCP servers from mcp_config, ignore all others. Maps to --strict-mcp-config */
52
+ strict_mcp_config?: boolean;
53
+ /** Run in isolated git worktree. Maps to --worktree */
54
+ worktree?: string;
55
+ /** Working directory for the CLI process. Maps to cwd */
56
+ working_directory?: string;
57
+ /** Max agentic turns before stopping. Maps to --max-turns */
58
+ max_turns?: number;
59
+ /** Max dollar spend before stopping. Maps to --max-budget-usd */
60
+ max_budget_usd?: number;
61
+ /** Append to default system prompt instead of replacing. Maps to --append-system-prompt */
62
+ append_system_prompt?: boolean;
63
+ /** Continue most recent conversation. Maps to --continue */
64
+ continue_conversation?: boolean;
65
+ /** Resume a specific CLI session by ID. Maps to --resume */
66
+ resume_session_id?: string;
67
+ /** Fork the session (new ID) when resuming. Maps to --fork-session */
68
+ fork_session?: boolean;
69
+ /** Fallback model if primary is overloaded. Maps to --fallback-model */
70
+ fallback_model?: string;
71
+ /** Skip all permission prompts. Maps to --dangerously-skip-permissions */
72
+ dangerously_skip_permissions?: boolean;
73
+ /** Additional working directories. Maps to --add-dir */
74
+ add_dirs?: string[];
75
+ }
76
+ export interface Usage {
77
+ input_tokens: number;
78
+ output_tokens: number;
79
+ cache_creation_input_tokens?: number;
80
+ cache_read_input_tokens?: number;
81
+ }
82
+ export interface MessagesResponse {
83
+ id: string;
84
+ type: "message";
85
+ role: "assistant";
86
+ content: ContentBlock[];
87
+ model: string;
88
+ stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | null;
89
+ stop_sequence: string | null;
90
+ usage: Usage;
91
+ session_id?: string;
92
+ cost_usd?: number;
93
+ duration_ms?: number;
94
+ num_turns?: number;
95
+ }
96
+ export interface ApiError {
97
+ type: "error";
98
+ error: {
99
+ type: string;
100
+ message: string;
101
+ };
102
+ }
103
+ export declare function apiError(type: string, message: string): ApiError;
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ // --- Anthropic Messages API types ---
2
+ export function apiError(type, message) {
3
+ return { type: "error", error: { type, message } };
4
+ }
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,uCAAuC;AAoHvC,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,OAAe;IACpD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;AACrD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@anagnole/claude-cli",
3
+ "version": "0.1.0",
4
+ "description": "Shared library for spawning, parsing, and managing Claude Code CLI sessions",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./cli": {
14
+ "types": "./dist/cli/spawn.d.ts",
15
+ "default": "./dist/cli/spawn.js"
16
+ },
17
+ "./parser": {
18
+ "types": "./dist/cli/parser.d.ts",
19
+ "default": "./dist/cli/parser.js"
20
+ },
21
+ "./session": {
22
+ "types": "./dist/session/session-map.d.ts",
23
+ "default": "./dist/session/session-map.js"
24
+ },
25
+ "./types": {
26
+ "types": "./dist/types.d.ts",
27
+ "default": "./dist/types.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "prepublishOnly": "tsc"
37
+ },
38
+ "keywords": ["claude", "cli", "anthropic", "proxy", "agent"],
39
+ "license": "MIT",
40
+ "engines": {
41
+ "node": ">=18"
42
+ }
43
+ }
@@ -0,0 +1,33 @@
1
+ /** Buffers partial lines from a stream and yields complete parsed JSON objects. */
2
+ export class NdjsonParser {
3
+ private buffer = "";
4
+
5
+ feed(chunk: string): unknown[] {
6
+ this.buffer += chunk;
7
+ const lines = this.buffer.split("\n");
8
+ this.buffer = lines.pop() ?? "";
9
+
10
+ const results: unknown[] = [];
11
+ for (const line of lines) {
12
+ const trimmed = line.trim();
13
+ if (!trimmed) continue;
14
+ try {
15
+ results.push(JSON.parse(trimmed));
16
+ } catch {
17
+ // Non-JSON line, skip
18
+ }
19
+ }
20
+ return results;
21
+ }
22
+
23
+ flush(): unknown[] {
24
+ const remaining = this.buffer.trim();
25
+ this.buffer = "";
26
+ if (!remaining) return [];
27
+ try {
28
+ return [JSON.parse(remaining)];
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,100 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+
3
+ export interface SpawnOptions {
4
+ prompt: string;
5
+ model?: string;
6
+ systemPrompt?: string;
7
+ appendSystemPrompt?: boolean;
8
+ resumeSessionId?: string;
9
+ continueConversation?: boolean;
10
+ forkSession?: boolean;
11
+ streaming: boolean;
12
+ effort?: string;
13
+ jsonSchema?: unknown;
14
+
15
+ // CLI-specific options
16
+ permissionMode?: string;
17
+ allowedTools?: string[];
18
+ disallowedTools?: string[];
19
+ cliTools?: string;
20
+ mcpConfig?: string;
21
+ strictMcpConfig?: boolean;
22
+ worktree?: string;
23
+ workingDirectory?: string;
24
+ maxTurns?: number;
25
+ maxBudgetUsd?: number;
26
+ fallbackModel?: string;
27
+ dangerouslySkipPermissions?: boolean;
28
+ addDirs?: string[];
29
+
30
+ /** Path to the claude CLI binary. Defaults to "claude". */
31
+ claudePath?: string;
32
+ }
33
+
34
+ export function spawnClaude(options: SpawnOptions): ChildProcess {
35
+ const args = ["--print"];
36
+
37
+ // Output format
38
+ if (options.streaming) {
39
+ args.push("--output-format", "stream-json", "--verbose", "--include-partial-messages");
40
+ } else {
41
+ args.push("--output-format", "json");
42
+ }
43
+
44
+ // Model
45
+ if (options.model) args.push("--model", options.model);
46
+ if (options.fallbackModel) args.push("--fallback-model", options.fallbackModel);
47
+
48
+ // System prompt
49
+ if (options.systemPrompt) {
50
+ if (options.appendSystemPrompt) {
51
+ args.push("--append-system-prompt", options.systemPrompt);
52
+ } else {
53
+ args.push("--system-prompt", options.systemPrompt);
54
+ }
55
+ }
56
+
57
+ // Session management
58
+ if (options.resumeSessionId) args.push("--resume", options.resumeSessionId);
59
+ if (options.continueConversation) args.push("--continue");
60
+ if (options.forkSession) args.push("--fork-session");
61
+
62
+ // Effort & structured output
63
+ if (options.effort) args.push("--effort", options.effort);
64
+ if (options.jsonSchema) args.push("--json-schema", JSON.stringify(options.jsonSchema));
65
+
66
+ // Permission & tools
67
+ if (options.permissionMode) args.push("--permission-mode", options.permissionMode);
68
+ if (options.dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
69
+ if (options.allowedTools?.length) args.push("--allowedTools", ...options.allowedTools);
70
+ if (options.disallowedTools?.length) args.push("--disallowedTools", ...options.disallowedTools);
71
+ if (options.cliTools != null) args.push("--tools", options.cliTools);
72
+
73
+ // MCP
74
+ if (options.mcpConfig) args.push(`--mcp-config=${options.mcpConfig}`);
75
+ if (options.strictMcpConfig) args.push("--strict-mcp-config");
76
+
77
+ // Worktree & directories
78
+ if (options.worktree) args.push("--worktree", options.worktree);
79
+ if (options.addDirs?.length) args.push("--add-dir", ...options.addDirs);
80
+
81
+ // Safety limits
82
+ if (options.maxTurns != null) args.push("--max-turns", String(options.maxTurns));
83
+ if (options.maxBudgetUsd != null) args.push("--max-budget-usd", String(options.maxBudgetUsd));
84
+
85
+ // Prompt is always last
86
+ if (options.prompt) {
87
+ args.push("--", options.prompt);
88
+ }
89
+
90
+ // Prevent recursion / interference if running inside a Claude Code session
91
+ const env = { ...process.env };
92
+ delete env.CLAUDECODE;
93
+ delete env.CLAUDE_CODE_ENTRYPOINT;
94
+
95
+ return spawn(options.claudePath ?? "claude", args, {
96
+ cwd: options.workingDirectory,
97
+ stdio: ["ignore", "pipe", "pipe"],
98
+ env,
99
+ });
100
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ // CLI
2
+ export { spawnClaude } from "./cli/spawn.js";
3
+ export type { SpawnOptions } from "./cli/spawn.js";
4
+ export { NdjsonParser } from "./cli/parser.js";
5
+
6
+ // Session
7
+ export { SessionMap } from "./session/session-map.js";
8
+
9
+ // Transform
10
+ export { extractPrompt, extractSystem, warnUnsupported } from "./transform/request.js";
11
+ export { buildResponse, generateMsgId } from "./transform/response.js";
12
+ export { createStreamState, transformEvent } from "./transform/stream.js";
13
+ export type { StreamState } from "./transform/stream.js";
14
+
15
+ // Types
16
+ export type {
17
+ ContentBlock,
18
+ SystemBlock,
19
+ MessageParam,
20
+ MessagesRequest,
21
+ MessagesResponse,
22
+ Usage,
23
+ ApiError,
24
+ } from "./types.js";
25
+ export { apiError } from "./types.js";
@@ -0,0 +1,64 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { MessageParam } from "../types.js";
3
+
4
+ interface SessionEntry {
5
+ cliSessionId: string;
6
+ lastUsedAt: number;
7
+ model: string;
8
+ }
9
+
10
+ export class SessionMap {
11
+ private map = new Map<string, SessionEntry>();
12
+ private maxEntries = 1000;
13
+
14
+ /** Hash all messages except the last one to fingerprint the conversation context. */
15
+ static hashContext(messages: MessageParam[]): string {
16
+ const context = messages.slice(0, -1);
17
+ if (context.length === 0) return "empty";
18
+ const payload = JSON.stringify(context);
19
+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
20
+ }
21
+
22
+ /** Look up a CLI session ID for a given context hash + model. */
23
+ lookup(hash: string, model: string): string | null {
24
+ const entry = this.map.get(hash);
25
+ if (!entry || entry.model !== model) return null;
26
+ entry.lastUsedAt = Date.now();
27
+ return entry.cliSessionId;
28
+ }
29
+
30
+ /**
31
+ * Store a session after a successful CLI invocation.
32
+ * `fullMessages` should include the original messages + the assistant response,
33
+ * so the *next* request with that history will find this session.
34
+ */
35
+ store(fullMessages: MessageParam[], cliSessionId: string, model: string): void {
36
+ // The next request will hash messages[0..n-1] (all except its new user msg),
37
+ // which equals the full messages from this turn.
38
+ const nextHash = createHash("sha256")
39
+ .update(JSON.stringify(fullMessages))
40
+ .digest("hex")
41
+ .slice(0, 16);
42
+
43
+ this.map.set(nextHash, {
44
+ cliSessionId,
45
+ lastUsedAt: Date.now(),
46
+ model,
47
+ });
48
+
49
+ this.evictIfNeeded();
50
+ }
51
+
52
+ private evictIfNeeded(): void {
53
+ if (this.map.size <= this.maxEntries) return;
54
+ let oldestKey: string | null = null;
55
+ let oldestTime = Infinity;
56
+ for (const [key, entry] of this.map) {
57
+ if (entry.lastUsedAt < oldestTime) {
58
+ oldestTime = entry.lastUsedAt;
59
+ oldestKey = key;
60
+ }
61
+ }
62
+ if (oldestKey) this.map.delete(oldestKey);
63
+ }
64
+ }
@@ -0,0 +1,40 @@
1
+ import type { MessageParam, MessagesRequest, SystemBlock } from "../types.js";
2
+
3
+ interface Logger {
4
+ warn(msg: string): void;
5
+ }
6
+
7
+ /** Extract the text content from the last user message. */
8
+ export function extractPrompt(messages: MessageParam[]): string {
9
+ const last = messages[messages.length - 1];
10
+ if (!last || last.role !== "user") {
11
+ throw new Error("Last message must have role 'user'");
12
+ }
13
+ if (typeof last.content === "string") return last.content;
14
+ return last.content
15
+ .filter((b) => b.type === "text")
16
+ .map((b) => b.text)
17
+ .join("\n");
18
+ }
19
+
20
+ /** Flatten system prompt to a single string. */
21
+ export function extractSystem(
22
+ system: string | SystemBlock[] | undefined,
23
+ ): string | undefined {
24
+ if (!system) return undefined;
25
+ if (typeof system === "string") return system;
26
+ return system
27
+ .filter((b) => b.type === "text")
28
+ .map((b) => b.text)
29
+ .join("\n");
30
+ }
31
+
32
+ /** Log warnings for API params the CLI truly can't handle. */
33
+ export function warnUnsupported(req: MessagesRequest, log: Logger): void {
34
+ if (req.temperature != null) log.warn("temperature param ignored (CLI does not support it)");
35
+ if (req.top_p != null) log.warn("top_p param ignored");
36
+ if (req.top_k != null) log.warn("top_k param ignored");
37
+ if (req.stop_sequences?.length) log.warn("stop_sequences param ignored");
38
+ if (req.tools?.length) log.warn("tools param ignored (CLI uses its own built-in tools, use allowed_tools/disallowed_tools instead)");
39
+ if (req.tool_choice != null) log.warn("tool_choice param ignored");
40
+ }
@@ -0,0 +1,61 @@
1
+ import crypto from "node:crypto";
2
+ import type { ContentBlock, MessagesResponse, Usage } from "../types.js";
3
+
4
+ export function generateMsgId(): string {
5
+ return "msg_" + crypto.randomBytes(18).toString("base64url");
6
+ }
7
+
8
+ interface CliResult {
9
+ result: string;
10
+ stop_reason?: string;
11
+ session_id: string;
12
+ total_cost_usd?: number;
13
+ duration_ms?: number;
14
+ num_turns?: number;
15
+ usage?: {
16
+ input_tokens?: number;
17
+ output_tokens?: number;
18
+ cache_creation_input_tokens?: number;
19
+ cache_read_input_tokens?: number;
20
+ };
21
+ }
22
+
23
+ export function buildResponse(cli: CliResult, model: string): MessagesResponse {
24
+ const usage: Usage = {
25
+ input_tokens: cli.usage?.input_tokens ?? 0,
26
+ output_tokens: cli.usage?.output_tokens ?? 0,
27
+ };
28
+ if (cli.usage?.cache_creation_input_tokens) {
29
+ usage.cache_creation_input_tokens = cli.usage.cache_creation_input_tokens;
30
+ }
31
+ if (cli.usage?.cache_read_input_tokens) {
32
+ usage.cache_read_input_tokens = cli.usage.cache_read_input_tokens;
33
+ }
34
+
35
+ // Build content blocks — for now just text, but could include tool_use in the future
36
+ const content: ContentBlock[] = [{ type: "text", text: cli.result }];
37
+
38
+ return {
39
+ id: generateMsgId(),
40
+ type: "message",
41
+ role: "assistant",
42
+ content,
43
+ model,
44
+ stop_reason: mapStopReason(cli.stop_reason),
45
+ stop_sequence: null,
46
+ usage,
47
+ // CLI extras
48
+ session_id: cli.session_id,
49
+ cost_usd: cli.total_cost_usd,
50
+ duration_ms: cli.duration_ms,
51
+ num_turns: cli.num_turns,
52
+ };
53
+ }
54
+
55
+ function mapStopReason(
56
+ reason?: string,
57
+ ): "end_turn" | "max_tokens" | "stop_sequence" {
58
+ if (reason === "max_tokens") return "max_tokens";
59
+ if (reason === "stop_sequence") return "stop_sequence";
60
+ return "end_turn";
61
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Transforms CLI stream-json NDJSON events into Anthropic SSE format.
3
+ *
4
+ * The CLI's `--output-format stream-json --verbose` emits events like:
5
+ * {"type": "stream_event", "event": { ...anthropic_event... }, "session_id": "..."}
6
+ *
7
+ * The inner `event` payload is almost identical to the Anthropic SSE data.
8
+ * We unwrap it, inject our msg_id where needed, and format as SSE lines.
9
+ */
10
+
11
+ import { generateMsgId } from "./response.js";
12
+
13
+ /** Format a single SSE event. */
14
+ function sse(event: string, data: unknown): string {
15
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
16
+ }
17
+
18
+ export interface StreamState {
19
+ msgId: string;
20
+ model: string;
21
+ sessionId: string | null;
22
+ inputTokens: number;
23
+ outputTokens: number;
24
+ started: boolean;
25
+ finished: boolean;
26
+ }
27
+
28
+ export function createStreamState(model: string): StreamState {
29
+ return {
30
+ msgId: generateMsgId(),
31
+ model,
32
+ sessionId: null,
33
+ inputTokens: 0,
34
+ outputTokens: 0,
35
+ started: false,
36
+ finished: false,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Transform a parsed CLI NDJSON object into zero or more SSE strings.
42
+ * Returns an array of SSE-formatted strings to write to the response.
43
+ */
44
+ export function transformEvent(obj: Record<string, unknown>, state: StreamState): string[] {
45
+ const results: string[] = [];
46
+
47
+ // Capture session_id from any event
48
+ if (typeof obj.session_id === "string") {
49
+ state.sessionId = obj.session_id;
50
+ }
51
+
52
+ // stream_event wraps Anthropic-format events
53
+ if (obj.type === "stream_event" && obj.event && typeof obj.event === "object") {
54
+ const event = obj.event as Record<string, unknown>;
55
+ const eventType = event.type as string;
56
+
57
+ switch (eventType) {
58
+ case "message_start": {
59
+ state.started = true;
60
+ // Rebuild message_start with our msg_id
61
+ const msg = (event.message ?? {}) as Record<string, unknown>;
62
+ const usage = (msg.usage ?? {}) as Record<string, unknown>;
63
+ if (typeof usage.input_tokens === "number") state.inputTokens = usage.input_tokens;
64
+
65
+ results.push(sse("message_start", {
66
+ type: "message_start",
67
+ message: {
68
+ id: state.msgId,
69
+ type: "message",
70
+ role: "assistant",
71
+ content: [],
72
+ model: state.model,
73
+ stop_reason: null,
74
+ stop_sequence: null,
75
+ usage: { input_tokens: state.inputTokens, output_tokens: 1 },
76
+ },
77
+ }));
78
+ break;
79
+ }
80
+
81
+ case "content_block_start":
82
+ results.push(sse("content_block_start", event));
83
+ break;
84
+
85
+ case "content_block_delta":
86
+ results.push(sse("content_block_delta", event));
87
+ break;
88
+
89
+ case "content_block_stop":
90
+ results.push(sse("content_block_stop", event));
91
+ break;
92
+
93
+ case "message_delta": {
94
+ const usage = (event.usage ?? {}) as Record<string, unknown>;
95
+ if (typeof usage.output_tokens === "number") state.outputTokens = usage.output_tokens;
96
+ results.push(sse("message_delta", event));
97
+ break;
98
+ }
99
+
100
+ case "message_stop":
101
+ state.finished = true;
102
+ results.push(sse("message_stop", { type: "message_stop" }));
103
+ break;
104
+
105
+ default:
106
+ // Pass through unknown event types (ping, etc.)
107
+ if (eventType === "ping") {
108
+ results.push(sse("ping", { type: "ping" }));
109
+ }
110
+ break;
111
+ }
112
+ }
113
+
114
+ // result event — capture session_id and usage for session map
115
+ if (obj.type === "result") {
116
+ if (typeof obj.session_id === "string") state.sessionId = obj.session_id;
117
+ const usage = obj.usage as Record<string, unknown> | undefined;
118
+ if (usage) {
119
+ if (typeof usage.input_tokens === "number") state.inputTokens = usage.input_tokens;
120
+ if (typeof usage.output_tokens === "number") state.outputTokens = usage.output_tokens;
121
+ }
122
+ // Don't emit SSE — message_stop was already sent by the stream events
123
+ }
124
+
125
+ return results;
126
+ }
package/src/types.ts ADDED
@@ -0,0 +1,119 @@
1
+ // --- Anthropic Messages API types ---
2
+
3
+ export interface ContentBlock {
4
+ type: string;
5
+ text?: string;
6
+ // tool_use fields
7
+ id?: string;
8
+ name?: string;
9
+ input?: unknown;
10
+ // tool_result fields
11
+ tool_use_id?: string;
12
+ content?: string | ContentBlock[];
13
+ is_error?: boolean;
14
+ }
15
+
16
+ export interface SystemBlock {
17
+ type: "text";
18
+ text: string;
19
+ cache_control?: { type: "ephemeral"; ttl?: string };
20
+ }
21
+
22
+ export interface MessageParam {
23
+ role: "user" | "assistant";
24
+ content: string | ContentBlock[];
25
+ }
26
+
27
+ export interface MessagesRequest {
28
+ model: string;
29
+ messages: MessageParam[];
30
+ max_tokens?: number;
31
+ system?: string | SystemBlock[];
32
+ stream?: boolean;
33
+ temperature?: number;
34
+ top_p?: number;
35
+ top_k?: number;
36
+ stop_sequences?: string[];
37
+ tools?: unknown[];
38
+ tool_choice?: unknown;
39
+ thinking?: unknown;
40
+ metadata?: { user_id?: string };
41
+
42
+ // --- Standard extensions ---
43
+ effort?: string;
44
+ json_schema?: unknown;
45
+
46
+ // --- CLI extensions (all optional) ---
47
+ // These map directly to Claude CLI flags.
48
+ // Pass them in the request body alongside standard Anthropic params.
49
+
50
+ /** Permission mode: "plan" | "bypassPermissions" | etc. Maps to --permission-mode */
51
+ permission_mode?: string;
52
+ /** Tools to auto-approve without prompting. Maps to --allowedTools */
53
+ allowed_tools?: string[];
54
+ /** Tools to remove from context entirely. Maps to --disallowedTools */
55
+ disallowed_tools?: string[];
56
+ /** Restrict built-in tools. "" disables all, "default" for all. Maps to --tools */
57
+ cli_tools?: string;
58
+ /** Path to MCP server config JSON file. Maps to --mcp-config */
59
+ mcp_config?: string;
60
+ /** Use only MCP servers from mcp_config, ignore all others. Maps to --strict-mcp-config */
61
+ strict_mcp_config?: boolean;
62
+ /** Run in isolated git worktree. Maps to --worktree */
63
+ worktree?: string;
64
+ /** Working directory for the CLI process. Maps to cwd */
65
+ working_directory?: string;
66
+ /** Max agentic turns before stopping. Maps to --max-turns */
67
+ max_turns?: number;
68
+ /** Max dollar spend before stopping. Maps to --max-budget-usd */
69
+ max_budget_usd?: number;
70
+ /** Append to default system prompt instead of replacing. Maps to --append-system-prompt */
71
+ append_system_prompt?: boolean;
72
+ /** Continue most recent conversation. Maps to --continue */
73
+ continue_conversation?: boolean;
74
+ /** Resume a specific CLI session by ID. Maps to --resume */
75
+ resume_session_id?: string;
76
+ /** Fork the session (new ID) when resuming. Maps to --fork-session */
77
+ fork_session?: boolean;
78
+ /** Fallback model if primary is overloaded. Maps to --fallback-model */
79
+ fallback_model?: string;
80
+ /** Skip all permission prompts. Maps to --dangerously-skip-permissions */
81
+ dangerously_skip_permissions?: boolean;
82
+ /** Additional working directories. Maps to --add-dir */
83
+ add_dirs?: string[];
84
+ }
85
+
86
+ export interface Usage {
87
+ input_tokens: number;
88
+ output_tokens: number;
89
+ cache_creation_input_tokens?: number;
90
+ cache_read_input_tokens?: number;
91
+ }
92
+
93
+ export interface MessagesResponse {
94
+ id: string;
95
+ type: "message";
96
+ role: "assistant";
97
+ content: ContentBlock[];
98
+ model: string;
99
+ stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | null;
100
+ stop_sequence: string | null;
101
+ usage: Usage;
102
+ // CLI extras returned in response
103
+ session_id?: string;
104
+ cost_usd?: number;
105
+ duration_ms?: number;
106
+ num_turns?: number;
107
+ }
108
+
109
+ export interface ApiError {
110
+ type: "error";
111
+ error: {
112
+ type: string;
113
+ message: string;
114
+ };
115
+ }
116
+
117
+ export function apiError(type: string, message: string): ApiError {
118
+ return { type: "error", error: { type, message } };
119
+ }