@parall/claude-agent 1.15.1
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/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +66 -0
- package/dist/dispatch.d.ts +16 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +138 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/output-parser.d.ts +8 -0
- package/dist/output-parser.d.ts.map +1 -0
- package/dist/output-parser.js +183 -0
- package/dist/session-manager.d.ts +20 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +71 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +36 -0
- package/package.json +41 -0
- package/src/config.ts +92 -0
- package/src/dispatch.ts +179 -0
- package/src/index.ts +106 -0
- package/src/output-parser.ts +194 -0
- package/src/session-manager.ts +87 -0
- package/src/workspace.ts +39 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ClaudeAgentConfig = {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
orgId: string;
|
|
5
|
+
wsUrl?: string;
|
|
6
|
+
swimlaneName?: string;
|
|
7
|
+
claudeBin: string;
|
|
8
|
+
claudeHome: string;
|
|
9
|
+
stateDir: string;
|
|
10
|
+
workspaceDir: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
permissionMode: string;
|
|
13
|
+
allowedTools: string[];
|
|
14
|
+
disallowedTools: string[];
|
|
15
|
+
additionalDirs: string[];
|
|
16
|
+
appendSystemPrompt?: string;
|
|
17
|
+
runtimeKey?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function resolveClaudeAgentConfig(env?: NodeJS.ProcessEnv): ClaudeAgentConfig;
|
|
20
|
+
export declare function resolveWsUrl(apiUrl: string, explicitWsUrl?: string, swimlaneName?: string): string;
|
|
21
|
+
export declare function buildClaudeRuntimeKey(agentUserId: string): string;
|
|
22
|
+
export declare function sessionStateFilePathForRuntime(stateDir: string, runtimeKey: string): string;
|
|
23
|
+
export declare function stepIdFilePathForSession(stateDir: string, sessionKey: string): string;
|
|
24
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAsBF,wBAAgB,wBAAwB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,iBAAiB,CA2BhG;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAMlG;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG3F;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGrF"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
function requireEnv(env, name) {
|
|
4
|
+
const value = env[name]?.trim();
|
|
5
|
+
if (!value) {
|
|
6
|
+
throw new Error(`Missing required env var: ${name}`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function parseList(value) {
|
|
11
|
+
if (!value)
|
|
12
|
+
return [];
|
|
13
|
+
return value
|
|
14
|
+
.split(/[,\n]/)
|
|
15
|
+
.map((item) => item.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
function resolvePath(value) {
|
|
19
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
20
|
+
}
|
|
21
|
+
export function resolveClaudeAgentConfig(env = process.env) {
|
|
22
|
+
const apiUrl = requireEnv(env, "PRLL_API_URL");
|
|
23
|
+
const apiKey = requireEnv(env, "PRLL_API_KEY");
|
|
24
|
+
const orgId = requireEnv(env, "PRLL_ORG_ID");
|
|
25
|
+
const claudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
|
|
26
|
+
const stateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(claudeHome, ".parall-agent"));
|
|
27
|
+
const workspaceDir = resolvePath(env.PRLL_CLAUDE_WORKSPACE_DIR?.trim() || path.join(stateDir, "workspace"));
|
|
28
|
+
const additionalDirs = parseList(env.PRLL_CLAUDE_ADD_DIRS).map(resolvePath);
|
|
29
|
+
return {
|
|
30
|
+
apiUrl,
|
|
31
|
+
apiKey,
|
|
32
|
+
orgId,
|
|
33
|
+
wsUrl: env.PRLL_WS_URL?.trim() || undefined,
|
|
34
|
+
swimlaneName: env.PRLL_SWIMLANE_NAME?.trim() || undefined,
|
|
35
|
+
claudeBin: env.PRLL_CLAUDE_BIN?.trim() || "claude",
|
|
36
|
+
claudeHome,
|
|
37
|
+
stateDir,
|
|
38
|
+
workspaceDir,
|
|
39
|
+
model: env.PRLL_CLAUDE_MODEL?.trim() || undefined,
|
|
40
|
+
permissionMode: env.PRLL_CLAUDE_PERMISSION_MODE?.trim() || "bypassPermissions",
|
|
41
|
+
allowedTools: parseList(env.PRLL_CLAUDE_ALLOWED_TOOLS),
|
|
42
|
+
disallowedTools: parseList(env.PRLL_CLAUDE_DISALLOWED_TOOLS),
|
|
43
|
+
additionalDirs,
|
|
44
|
+
appendSystemPrompt: env.PRLL_CLAUDE_APPEND_SYSTEM_PROMPT?.trim() || undefined,
|
|
45
|
+
runtimeKey: env.PRLL_CLAUDE_RUNTIME_KEY?.trim() || undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
|
|
49
|
+
const base = explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
|
|
50
|
+
if (!swimlaneName)
|
|
51
|
+
return base;
|
|
52
|
+
const url = new URL(base);
|
|
53
|
+
url.searchParams.set("swimlane", swimlaneName);
|
|
54
|
+
return url.toString();
|
|
55
|
+
}
|
|
56
|
+
export function buildClaudeRuntimeKey(agentUserId) {
|
|
57
|
+
return `agent:main:claude:${agentUserId}:orchestrator`;
|
|
58
|
+
}
|
|
59
|
+
export function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
|
|
60
|
+
const fileName = Buffer.from(runtimeKey).toString("base64url");
|
|
61
|
+
return path.join(stateDir, "sessions", `${fileName}.json`);
|
|
62
|
+
}
|
|
63
|
+
export function stepIdFilePathForSession(stateDir, sessionKey) {
|
|
64
|
+
const fileName = Buffer.from(sessionKey).toString("base64url");
|
|
65
|
+
return path.join(stateDir, "step-ids", `${fileName}.txt`);
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CleanupForkOpts, DispatchAdapter, DispatchOpts, ForkOpts, RuntimeEvent } from "@parall/agent-core";
|
|
2
|
+
import type { ClaudeAgentConfig } from "./config.js";
|
|
3
|
+
import { ClaudeSessionManager } from "./session-manager.js";
|
|
4
|
+
type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, "additionalDirs" | "allowedTools" | "appendSystemPrompt" | "claudeBin" | "claudeHome" | "disallowedTools" | "model" | "permissionMode" | "workspaceDir"> & {
|
|
5
|
+
sessionManager: ClaudeSessionManager;
|
|
6
|
+
};
|
|
7
|
+
export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
8
|
+
private readonly opts;
|
|
9
|
+
constructor(opts: ClaudeCodeAdapterOptions);
|
|
10
|
+
dispatch({ bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent>;
|
|
11
|
+
forkSession({ sessionKey }: ForkOpts): import("@parall/agent-core").ForkSessionHandle | null;
|
|
12
|
+
cleanupFork({ fork }: CleanupForkOpts): void;
|
|
13
|
+
private buildArgs;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,KAAK,wBAAwB,GAAG,IAAI,CAClC,iBAAiB,EACf,gBAAgB,GAChB,cAAc,GACd,oBAAoB,GACpB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,OAAO,GACP,gBAAgB,GAChB,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,oBAAoB,CAAC;CACtC,CAAC;AAgBF,qBAAa,iBAAkB,YAAW,eAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,wBAAwB;IAEpD,QAAQ,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA2FjG,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,SAAS;CAkClB"}
|
package/dist/dispatch.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { parseClaudeStreamJson } from "./output-parser.js";
|
|
4
|
+
function extractBashCommand(input) {
|
|
5
|
+
if (!input || typeof input !== "object")
|
|
6
|
+
return undefined;
|
|
7
|
+
const command = input.command;
|
|
8
|
+
return typeof command === "string" && command.trim() ? command.trim() : undefined;
|
|
9
|
+
}
|
|
10
|
+
function isParallSendCommand(command) {
|
|
11
|
+
if (!command)
|
|
12
|
+
return false;
|
|
13
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
14
|
+
const invokesParallCli = /(?:^| )(?:parall|npx @parall\/cli(?:@latest)?|pnpm (?:exec|dlx) parall)(?: |$)/.test(normalized);
|
|
15
|
+
if (!invokesParallCli)
|
|
16
|
+
return false;
|
|
17
|
+
return /\b(messages send|dm)\b/.test(normalized);
|
|
18
|
+
}
|
|
19
|
+
export class ClaudeCodeAdapter {
|
|
20
|
+
opts;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
}
|
|
24
|
+
async *dispatch({ bodyForAgent, sessionKey, context }) {
|
|
25
|
+
const args = this.buildArgs(sessionKey, bodyForAgent);
|
|
26
|
+
const env = {
|
|
27
|
+
...process.env,
|
|
28
|
+
HOME: this.opts.claudeHome,
|
|
29
|
+
PRLL_API_URL: context.apiUrl,
|
|
30
|
+
PRLL_API_KEY: context.apiKey,
|
|
31
|
+
PRLL_ORG_ID: context.orgId,
|
|
32
|
+
PRLL_SESSION_ID: context.sessionId ?? "",
|
|
33
|
+
PRLL_CHAT_ID: context.chatId ?? "",
|
|
34
|
+
PRLL_TRIGGER_MESSAGE_ID: context.triggerMessageId ?? "",
|
|
35
|
+
PRLL_NO_REPLY: context.noReply ? "1" : "",
|
|
36
|
+
PRLL_STEP_ID_FILE: context.stepIdFilePath ?? "",
|
|
37
|
+
};
|
|
38
|
+
context.log?.info(`claude-agent[${context.accountId}]: spawn ${this.opts.claudeBin} ${args.slice(0, -1).join(" ")}`);
|
|
39
|
+
const proc = spawn(this.opts.claudeBin, args, {
|
|
40
|
+
cwd: this.opts.workspaceDir,
|
|
41
|
+
env,
|
|
42
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
43
|
+
});
|
|
44
|
+
if (!proc.stdout || !proc.stderr) {
|
|
45
|
+
throw new Error("Claude subprocess did not provide stdio pipes");
|
|
46
|
+
}
|
|
47
|
+
const stderr = [];
|
|
48
|
+
proc.stderr.on("data", (chunk) => {
|
|
49
|
+
stderr.push(chunk.toString());
|
|
50
|
+
});
|
|
51
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
52
|
+
proc.once("error", reject);
|
|
53
|
+
proc.once("close", (code, signal) => resolve({ code, signal }));
|
|
54
|
+
});
|
|
55
|
+
const groupKey = randomUUID();
|
|
56
|
+
let sawError = false;
|
|
57
|
+
let suppressProjectedText = false;
|
|
58
|
+
let completed = false;
|
|
59
|
+
try {
|
|
60
|
+
for await (const event of parseClaudeStreamJson(proc.stdout)) {
|
|
61
|
+
if (event.type === "session_id") {
|
|
62
|
+
this.opts.sessionManager.recordSessionId(sessionKey, event.sessionId);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (event.type === "tool_call" && isParallSendCommand(extractBashCommand(event.input))) {
|
|
66
|
+
suppressProjectedText = true;
|
|
67
|
+
}
|
|
68
|
+
if (event.type === "error") {
|
|
69
|
+
sawError = true;
|
|
70
|
+
yield event;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (event.type === "text") {
|
|
74
|
+
const project = suppressProjectedText ? false : event.project;
|
|
75
|
+
suppressProjectedText = false;
|
|
76
|
+
yield {
|
|
77
|
+
...event,
|
|
78
|
+
project,
|
|
79
|
+
groupKey,
|
|
80
|
+
};
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
yield {
|
|
84
|
+
...event,
|
|
85
|
+
groupKey,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
completed = true;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (!completed && proc.exitCode === null && proc.signalCode === null) {
|
|
92
|
+
proc.kill("SIGTERM");
|
|
93
|
+
}
|
|
94
|
+
const { code, signal } = await exitPromise;
|
|
95
|
+
if ((code ?? 0) !== 0 && !sawError) {
|
|
96
|
+
const detail = stderr.join("").trim();
|
|
97
|
+
yield {
|
|
98
|
+
type: "error",
|
|
99
|
+
message: detail || `Claude exited with code ${code ?? "unknown"}${signal ? ` (${signal})` : ""}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
forkSession({ sessionKey }) {
|
|
105
|
+
return this.opts.sessionManager.createForkSession(sessionKey);
|
|
106
|
+
}
|
|
107
|
+
cleanupFork({ fork }) {
|
|
108
|
+
this.opts.sessionManager.cleanupFork(fork.sessionKey);
|
|
109
|
+
}
|
|
110
|
+
buildArgs(sessionKey, prompt) {
|
|
111
|
+
const args = [
|
|
112
|
+
"--print",
|
|
113
|
+
"--verbose",
|
|
114
|
+
"--output-format",
|
|
115
|
+
"stream-json",
|
|
116
|
+
"--permission-mode",
|
|
117
|
+
this.opts.permissionMode,
|
|
118
|
+
];
|
|
119
|
+
if (this.opts.model) {
|
|
120
|
+
args.push("--model", this.opts.model);
|
|
121
|
+
}
|
|
122
|
+
if (this.opts.allowedTools.length > 0) {
|
|
123
|
+
args.push("--allowedTools", this.opts.allowedTools.join(","));
|
|
124
|
+
}
|
|
125
|
+
if (this.opts.disallowedTools.length > 0) {
|
|
126
|
+
args.push("--disallowedTools", this.opts.disallowedTools.join(","));
|
|
127
|
+
}
|
|
128
|
+
if (this.opts.appendSystemPrompt) {
|
|
129
|
+
args.push("--append-system-prompt", this.opts.appendSystemPrompt);
|
|
130
|
+
}
|
|
131
|
+
if (this.opts.additionalDirs.length > 0) {
|
|
132
|
+
args.push("--add-dir", ...this.opts.additionalDirs);
|
|
133
|
+
}
|
|
134
|
+
args.push(...this.opts.sessionManager.getResumeArgs(sessionKey));
|
|
135
|
+
args.push(prompt);
|
|
136
|
+
return args;
|
|
137
|
+
}
|
|
138
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import { ParallAgentGateway } from "@parall/agent-core";
|
|
4
|
+
import { ParallClient, ParallWs } from "@parall/sdk";
|
|
5
|
+
import { buildClaudeRuntimeKey, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
|
|
6
|
+
import { ClaudeCodeAdapter } from "./dispatch.js";
|
|
7
|
+
import { ClaudeSessionManager } from "./session-manager.js";
|
|
8
|
+
import { ensureClaudeWorkspace } from "./workspace.js";
|
|
9
|
+
function createLogger(prefix) {
|
|
10
|
+
return {
|
|
11
|
+
info: (msg) => console.log(`[${prefix}] ${msg}`),
|
|
12
|
+
warn: (msg) => console.warn(`[${prefix}] ${msg}`),
|
|
13
|
+
error: (msg) => console.error(`[${prefix}] ${msg}`),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function main() {
|
|
17
|
+
const config = resolveClaudeAgentConfig(process.env);
|
|
18
|
+
ensureClaudeWorkspace(config.workspaceDir);
|
|
19
|
+
const log = createLogger("claude-agent");
|
|
20
|
+
const client = new ParallClient({
|
|
21
|
+
baseUrl: config.apiUrl,
|
|
22
|
+
token: config.apiKey,
|
|
23
|
+
swimlaneName: config.swimlaneName,
|
|
24
|
+
});
|
|
25
|
+
const me = await client.getMe();
|
|
26
|
+
const agentUserId = me.id;
|
|
27
|
+
const runtimeKey = config.runtimeKey || buildClaudeRuntimeKey(agentUserId);
|
|
28
|
+
const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
|
|
29
|
+
const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, log);
|
|
30
|
+
const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
|
|
31
|
+
const ws = new ParallWs({
|
|
32
|
+
getTicket: () => client.getWsTicket(),
|
|
33
|
+
wsUrl: resolvedWsUrl,
|
|
34
|
+
});
|
|
35
|
+
const adapter = new ClaudeCodeAdapter({
|
|
36
|
+
claudeBin: config.claudeBin,
|
|
37
|
+
claudeHome: config.claudeHome,
|
|
38
|
+
workspaceDir: config.workspaceDir,
|
|
39
|
+
model: config.model,
|
|
40
|
+
permissionMode: config.permissionMode,
|
|
41
|
+
allowedTools: config.allowedTools,
|
|
42
|
+
disallowedTools: config.disallowedTools,
|
|
43
|
+
additionalDirs: config.additionalDirs,
|
|
44
|
+
appendSystemPrompt: config.appendSystemPrompt,
|
|
45
|
+
sessionManager,
|
|
46
|
+
});
|
|
47
|
+
const gateway = new ParallAgentGateway({
|
|
48
|
+
accountId: agentUserId,
|
|
49
|
+
client,
|
|
50
|
+
ws,
|
|
51
|
+
connectionLabel: resolvedWsUrl,
|
|
52
|
+
config: {
|
|
53
|
+
parall_url: config.apiUrl,
|
|
54
|
+
api_key: config.apiKey,
|
|
55
|
+
org_id: config.orgId,
|
|
56
|
+
},
|
|
57
|
+
agentUserId,
|
|
58
|
+
runtimeType: "claude-code",
|
|
59
|
+
runtimeKey,
|
|
60
|
+
runtimeRef: {
|
|
61
|
+
hostname: os.hostname(),
|
|
62
|
+
pid: process.pid,
|
|
63
|
+
workspace_dir: config.workspaceDir,
|
|
64
|
+
claude_home: config.claudeHome,
|
|
65
|
+
},
|
|
66
|
+
dispatchAdapter: adapter,
|
|
67
|
+
log,
|
|
68
|
+
stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
|
|
69
|
+
});
|
|
70
|
+
const abortController = new AbortController();
|
|
71
|
+
const abort = () => abortController.abort();
|
|
72
|
+
process.on("SIGINT", abort);
|
|
73
|
+
process.on("SIGTERM", abort);
|
|
74
|
+
try {
|
|
75
|
+
log.info(`starting self-hosted Claude runtime for ${me.display_name} (${agentUserId})`);
|
|
76
|
+
await gateway.run(abortController.signal);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
process.off("SIGINT", abort);
|
|
80
|
+
process.off("SIGTERM", abort);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
main().catch((err) => {
|
|
84
|
+
console.error(`[claude-agent] fatal: ${String(err)}`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import type { RuntimeEvent } from "@parall/agent-core";
|
|
3
|
+
export type ClaudeParsedEvent = RuntimeEvent | {
|
|
4
|
+
type: "session_id";
|
|
5
|
+
sessionId: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseClaudeStreamJson(readable: Readable): AsyncGenerator<ClaudeParsedEvent>;
|
|
8
|
+
//# sourceMappingURL=output-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AA4E9C,wBAAuB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAgHlG"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
function asTrimmedString(value) {
|
|
2
|
+
if (typeof value !== "string")
|
|
3
|
+
return undefined;
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
6
|
+
}
|
|
7
|
+
function parseEventTimestampMs(event) {
|
|
8
|
+
if (!event || typeof event !== "object")
|
|
9
|
+
return undefined;
|
|
10
|
+
const timestamp = event.timestamp;
|
|
11
|
+
if (typeof timestamp !== "string")
|
|
12
|
+
return undefined;
|
|
13
|
+
const ms = Date.parse(timestamp);
|
|
14
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
15
|
+
}
|
|
16
|
+
function stringifyContent(value) {
|
|
17
|
+
if (typeof value === "string")
|
|
18
|
+
return value;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value
|
|
21
|
+
.map((item) => {
|
|
22
|
+
if (typeof item === "string")
|
|
23
|
+
return item;
|
|
24
|
+
if (!item || typeof item !== "object")
|
|
25
|
+
return "";
|
|
26
|
+
const text = asTrimmedString(item.text);
|
|
27
|
+
if (text)
|
|
28
|
+
return text;
|
|
29
|
+
const thinking = asTrimmedString(item.thinking);
|
|
30
|
+
if (thinking)
|
|
31
|
+
return thinking;
|
|
32
|
+
return JSON.stringify(item);
|
|
33
|
+
})
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join("\n");
|
|
36
|
+
}
|
|
37
|
+
if (value && typeof value === "object") {
|
|
38
|
+
return JSON.stringify(value);
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
function normalizeToolResultContent(content, fallback) {
|
|
43
|
+
const direct = stringifyContent(content).trim();
|
|
44
|
+
if (direct)
|
|
45
|
+
return direct;
|
|
46
|
+
if (fallback && typeof fallback === "object") {
|
|
47
|
+
const stdout = asTrimmedString(fallback.stdout);
|
|
48
|
+
const stderr = asTrimmedString(fallback.stderr);
|
|
49
|
+
if (stdout && stderr)
|
|
50
|
+
return `${stdout}\n${stderr}`;
|
|
51
|
+
if (stdout)
|
|
52
|
+
return stdout;
|
|
53
|
+
if (stderr)
|
|
54
|
+
return stderr;
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
async function* readJsonLines(readable) {
|
|
59
|
+
let buffer = "";
|
|
60
|
+
readable.setEncoding("utf8");
|
|
61
|
+
for await (const chunk of readable) {
|
|
62
|
+
buffer += chunk;
|
|
63
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
64
|
+
while (newlineIndex >= 0) {
|
|
65
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
66
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
67
|
+
if (line)
|
|
68
|
+
yield line;
|
|
69
|
+
newlineIndex = buffer.indexOf("\n");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const tail = buffer.trim();
|
|
73
|
+
if (tail)
|
|
74
|
+
yield tail;
|
|
75
|
+
}
|
|
76
|
+
export async function* parseClaudeStreamJson(readable) {
|
|
77
|
+
const toolUses = new Map();
|
|
78
|
+
for await (const line of readJsonLines(readable)) {
|
|
79
|
+
let event;
|
|
80
|
+
try {
|
|
81
|
+
event = JSON.parse(line);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!event || typeof event !== "object")
|
|
87
|
+
continue;
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const eventTimestampMs = parseEventTimestampMs(event) ?? now;
|
|
90
|
+
const eventRecord = event;
|
|
91
|
+
if (eventRecord.type === "system" && eventRecord.subtype === "init") {
|
|
92
|
+
const sessionId = asTrimmedString(eventRecord.session_id);
|
|
93
|
+
if (sessionId) {
|
|
94
|
+
yield { type: "session_id", sessionId };
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (eventRecord.type === "assistant") {
|
|
99
|
+
if (eventRecord.error)
|
|
100
|
+
continue;
|
|
101
|
+
const message = eventRecord.message;
|
|
102
|
+
const content = message && typeof message === "object"
|
|
103
|
+
? message.content
|
|
104
|
+
: undefined;
|
|
105
|
+
if (!Array.isArray(content))
|
|
106
|
+
continue;
|
|
107
|
+
for (const block of content) {
|
|
108
|
+
if (!block || typeof block !== "object")
|
|
109
|
+
continue;
|
|
110
|
+
const blockRecord = block;
|
|
111
|
+
switch (blockRecord.type) {
|
|
112
|
+
case "thinking": {
|
|
113
|
+
const text = asTrimmedString(blockRecord.thinking);
|
|
114
|
+
if (text)
|
|
115
|
+
yield { type: "thinking", text };
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "text": {
|
|
119
|
+
const text = asTrimmedString(blockRecord.text);
|
|
120
|
+
if (text)
|
|
121
|
+
yield { type: "text", text, project: true };
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "tool_use": {
|
|
125
|
+
const callId = asTrimmedString(blockRecord.id);
|
|
126
|
+
const toolName = asTrimmedString(blockRecord.name);
|
|
127
|
+
if (!callId || !toolName)
|
|
128
|
+
break;
|
|
129
|
+
toolUses.set(callId, {
|
|
130
|
+
toolName,
|
|
131
|
+
startedAtMs: eventTimestampMs,
|
|
132
|
+
});
|
|
133
|
+
yield {
|
|
134
|
+
type: "tool_call",
|
|
135
|
+
callId,
|
|
136
|
+
toolName,
|
|
137
|
+
input: blockRecord.input ?? {},
|
|
138
|
+
startedAt: new Date(eventTimestampMs).toISOString(),
|
|
139
|
+
};
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (eventRecord.type === "user") {
|
|
147
|
+
const message = eventRecord.message;
|
|
148
|
+
const content = message && typeof message === "object"
|
|
149
|
+
? message.content
|
|
150
|
+
: undefined;
|
|
151
|
+
if (!Array.isArray(content))
|
|
152
|
+
continue;
|
|
153
|
+
for (const block of content) {
|
|
154
|
+
if (!block || typeof block !== "object")
|
|
155
|
+
continue;
|
|
156
|
+
const blockRecord = block;
|
|
157
|
+
if (blockRecord.type !== "tool_result")
|
|
158
|
+
continue;
|
|
159
|
+
const callId = asTrimmedString(blockRecord.tool_use_id);
|
|
160
|
+
if (!callId)
|
|
161
|
+
continue;
|
|
162
|
+
const meta = toolUses.get(callId);
|
|
163
|
+
const output = normalizeToolResultContent(blockRecord.content, eventRecord.tool_use_result);
|
|
164
|
+
yield {
|
|
165
|
+
type: "tool_result",
|
|
166
|
+
callId,
|
|
167
|
+
toolName: meta?.toolName ?? "unknown",
|
|
168
|
+
output,
|
|
169
|
+
...(blockRecord.is_error === true ? { error: output || "Tool call failed" } : {}),
|
|
170
|
+
...(meta ? { durationMs: Math.max(0, eventTimestampMs - meta.startedAtMs) } : {}),
|
|
171
|
+
};
|
|
172
|
+
toolUses.delete(callId);
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (eventRecord.type === "result" && eventRecord.is_error === true) {
|
|
177
|
+
const message = asTrimmedString(eventRecord.result)
|
|
178
|
+
|| asTrimmedString(eventRecord.error)
|
|
179
|
+
|| "Claude dispatch failed";
|
|
180
|
+
yield { type: "error", message };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ForkSessionHandle } from "@parall/agent-core";
|
|
2
|
+
type ClaudeSessionManagerLogger = {
|
|
3
|
+
warn(message: string): void;
|
|
4
|
+
};
|
|
5
|
+
export declare class ClaudeSessionManager {
|
|
6
|
+
private readonly mainSessionKey;
|
|
7
|
+
private readonly stateFilePath;
|
|
8
|
+
private readonly logger?;
|
|
9
|
+
private readonly sessionIds;
|
|
10
|
+
private readonly pendingForkParents;
|
|
11
|
+
constructor(mainSessionKey: string, stateFilePath: string, logger?: ClaudeSessionManagerLogger | undefined);
|
|
12
|
+
getResumeArgs(sessionKey: string): string[];
|
|
13
|
+
recordSessionId(sessionKey: string, sessionId: string): void;
|
|
14
|
+
createForkSession(parentSessionKey: string): ForkSessionHandle | null;
|
|
15
|
+
cleanupFork(sessionKey: string): void;
|
|
16
|
+
private restore;
|
|
17
|
+
private persist;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5D,KAAK,0BAA0B,GAAG;IAChC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,qBAAa,oBAAoB;IAK7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAN1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;gBAG7C,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,0BAA0B,YAAA;IAKtD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAU3C,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAQrD,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAYrE,WAAW,CAAC,UAAU,EAAE,MAAM;IAK9B,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,OAAO;CAahB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
export class ClaudeSessionManager {
|
|
5
|
+
mainSessionKey;
|
|
6
|
+
stateFilePath;
|
|
7
|
+
logger;
|
|
8
|
+
sessionIds = new Map();
|
|
9
|
+
pendingForkParents = new Map();
|
|
10
|
+
constructor(mainSessionKey, stateFilePath, logger) {
|
|
11
|
+
this.mainSessionKey = mainSessionKey;
|
|
12
|
+
this.stateFilePath = stateFilePath;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
this.restore();
|
|
15
|
+
}
|
|
16
|
+
getResumeArgs(sessionKey) {
|
|
17
|
+
const existing = this.sessionIds.get(sessionKey);
|
|
18
|
+
if (existing)
|
|
19
|
+
return ["--resume", existing];
|
|
20
|
+
const parent = this.pendingForkParents.get(sessionKey);
|
|
21
|
+
if (parent)
|
|
22
|
+
return ["--resume", parent, "--fork-session"];
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
recordSessionId(sessionKey, sessionId) {
|
|
26
|
+
this.sessionIds.set(sessionKey, sessionId);
|
|
27
|
+
this.pendingForkParents.delete(sessionKey);
|
|
28
|
+
if (sessionKey === this.mainSessionKey) {
|
|
29
|
+
this.persist(sessionId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
createForkSession(parentSessionKey) {
|
|
33
|
+
const parentSessionId = this.sessionIds.get(parentSessionKey);
|
|
34
|
+
if (!parentSessionId)
|
|
35
|
+
return null;
|
|
36
|
+
const sessionKey = `claude-fork:${randomUUID()}`;
|
|
37
|
+
this.pendingForkParents.set(sessionKey, parentSessionId);
|
|
38
|
+
return {
|
|
39
|
+
sessionKey,
|
|
40
|
+
parentSessionId,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
cleanupFork(sessionKey) {
|
|
44
|
+
this.pendingForkParents.delete(sessionKey);
|
|
45
|
+
this.sessionIds.delete(sessionKey);
|
|
46
|
+
}
|
|
47
|
+
restore() {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(this.stateFilePath, "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (parsed.runtimeKey === this.mainSessionKey && typeof parsed.sessionId === "string" && parsed.sessionId.trim()) {
|
|
52
|
+
this.sessionIds.set(this.mainSessionKey, parsed.sessionId.trim());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// No persisted main session yet.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
persist(sessionId) {
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(path.dirname(this.stateFilePath), { recursive: true });
|
|
62
|
+
fs.writeFileSync(this.stateFilePath, JSON.stringify({
|
|
63
|
+
runtimeKey: this.mainSessionKey,
|
|
64
|
+
sessionId,
|
|
65
|
+
}, null, 2));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.logger?.warn(`claude-agent: failed to persist main session state at ${this.stateFilePath}: ${String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AA8BA,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,QAQzD"}
|