@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.
- package/dist/cli/parser.d.ts +6 -0
- package/dist/cli/parser.js +35 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/spawn.d.ts +29 -0
- package/dist/cli/spawn.js +77 -0
- package/dist/cli/spawn.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/session/session-map.d.ts +16 -0
- package/dist/session/session-map.js +55 -0
- package/dist/session/session-map.js.map +1 -0
- package/dist/transform/request.d.ts +11 -0
- package/dist/transform/request.js +40 -0
- package/dist/transform/request.js.map +1 -0
- package/dist/transform/response.d.ts +18 -0
- package/dist/transform/response.js +41 -0
- package/dist/transform/response.js.map +1 -0
- package/dist/transform/stream.d.ts +24 -0
- package/dist/transform/stream.js +106 -0
- package/dist/transform/stream.js.map +1 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/cli/parser.ts +33 -0
- package/src/cli/spawn.ts +100 -0
- package/src/index.ts +25 -0
- package/src/session/session-map.ts +64 -0
- package/src/transform/request.ts +40 -0
- package/src/transform/response.ts +61 -0
- package/src/transform/stream.ts +126 -0
- package/src/types.ts +119 -0
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|
package/src/cli/spawn.ts
ADDED
|
@@ -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
|
+
}
|