@heyditto/cli 2.11.0 → 2.12.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/README.md +74 -0
- package/dist/agents/launch.d.ts +11 -0
- package/dist/agents/launch.js +207 -9
- package/dist/agents/launch.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +5 -1
- package/dist/commands.js.map +1 -1
- package/dist/remote/attachments.d.ts +22 -0
- package/dist/remote/attachments.js +0 -0
- package/dist/remote/attachments.js.map +1 -0
- package/dist/remote/catalog.d.ts +18 -0
- package/dist/remote/catalog.js +309 -0
- package/dist/remote/catalog.js.map +1 -0
- package/dist/remote/controller.d.ts +76 -0
- package/dist/remote/controller.js +352 -0
- package/dist/remote/controller.js.map +1 -0
- package/dist/remote/fork-command.d.ts +8 -0
- package/dist/remote/fork-command.js +50 -0
- package/dist/remote/fork-command.js.map +1 -0
- package/dist/remote/fork.d.ts +22 -0
- package/dist/remote/fork.js +113 -0
- package/dist/remote/fork.js.map +1 -0
- package/dist/remote/headless.d.ts +30 -0
- package/dist/remote/headless.js +75 -0
- package/dist/remote/headless.js.map +1 -0
- package/dist/remote/hook.d.ts +2 -0
- package/dist/remote/hook.js +69 -0
- package/dist/remote/hook.js.map +1 -0
- package/dist/remote/hooks.d.ts +30 -0
- package/dist/remote/hooks.js +95 -0
- package/dist/remote/hooks.js.map +1 -0
- package/dist/remote/host.d.ts +63 -0
- package/dist/remote/host.js +240 -0
- package/dist/remote/host.js.map +1 -0
- package/dist/remote/protocol.d.ts +163 -0
- package/dist/remote/protocol.js +11 -0
- package/dist/remote/protocol.js.map +1 -0
- package/dist/remote/pty.d.ts +56 -0
- package/dist/remote/pty.js +100 -0
- package/dist/remote/pty.js.map +1 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js.map +1 -1
- package/dist/teleport/commands.d.ts +6 -0
- package/dist/teleport/commands.js +11 -0
- package/dist/teleport/commands.js.map +1 -1
- package/package.json +9 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readSession, writeSession } from "../agents/sessions.js";
|
|
6
|
+
import { defaultWorktreeName, ensureWorktree, repoRoot, validWorktreeName } from "../agents/worktree.js";
|
|
7
|
+
import { canonicalCwd, locateClaude, locateCodex } from "../teleport/harness.js";
|
|
8
|
+
import { cwdSlug } from "../teleport/types.js";
|
|
9
|
+
async function exists(p) {
|
|
10
|
+
try {
|
|
11
|
+
await access(p);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function claudeProjectsDir() {
|
|
19
|
+
const home = process.env.CLAUDE_CONFIG_DIR?.trim() || path.join(os.homedir(), ".claude");
|
|
20
|
+
return path.join(home, "projects");
|
|
21
|
+
}
|
|
22
|
+
/** Copies a Claude transcript to a new session id (and, when the cwd moved, the new project slug). */
|
|
23
|
+
async function forkClaudeTranscript(oldId, newId, fromCwd, toCwd) {
|
|
24
|
+
const loc = await locateClaude(oldId, fromCwd);
|
|
25
|
+
if (!loc)
|
|
26
|
+
throw new Error(`no local Claude Code transcript for session ${oldId} under ${fromCwd}`);
|
|
27
|
+
const fromCanon = canonicalCwd(fromCwd);
|
|
28
|
+
const toCanon = canonicalCwd(toCwd);
|
|
29
|
+
const toDir = path.join(claudeProjectsDir(), cwdSlug(toCanon));
|
|
30
|
+
await mkdir(toDir, { recursive: true });
|
|
31
|
+
const jsonl = loc.files.find((f) => f.endsWith(`${oldId}.jsonl`));
|
|
32
|
+
if (!jsonl)
|
|
33
|
+
throw new Error(`transcript ${oldId}.jsonl not found`);
|
|
34
|
+
let text = await readFile(jsonl, "utf8");
|
|
35
|
+
text = text.split(oldId).join(newId);
|
|
36
|
+
if (fromCanon !== toCanon)
|
|
37
|
+
text = text.split(fromCanon).join(toCanon).split(fromCwd).join(toCwd);
|
|
38
|
+
const target = path.join(toDir, `${newId}.jsonl`);
|
|
39
|
+
await writeFile(target, text);
|
|
40
|
+
const subdir = path.join(path.dirname(jsonl), oldId);
|
|
41
|
+
if (await exists(subdir)) {
|
|
42
|
+
for (const f of loc.files.filter((x) => x.startsWith(`${subdir}${path.sep}`))) {
|
|
43
|
+
const dest = path.join(toDir, newId, path.relative(subdir, f));
|
|
44
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
45
|
+
await writeFile(dest, (await readFile(f, "utf8")).split(oldId).join(newId));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return target;
|
|
49
|
+
}
|
|
50
|
+
/** Copies a Codex rollout to a new thread id: same directory, id rewritten in the file name and body. */
|
|
51
|
+
async function forkCodexTranscript(oldId, newId) {
|
|
52
|
+
const loc = await locateCodex(oldId);
|
|
53
|
+
if (!loc)
|
|
54
|
+
throw new Error(`no local Codex transcript for thread ${oldId}`);
|
|
55
|
+
let target = "";
|
|
56
|
+
for (const f of loc.files) {
|
|
57
|
+
const dest = path.join(path.dirname(f), path.basename(f).split(oldId).join(newId));
|
|
58
|
+
await writeFile(dest, (await readFile(f, "utf8")).split(oldId).join(newId));
|
|
59
|
+
target = target || dest;
|
|
60
|
+
}
|
|
61
|
+
return target;
|
|
62
|
+
}
|
|
63
|
+
export async function forkSession(ref, options) {
|
|
64
|
+
const record = await readSession(ref);
|
|
65
|
+
if (!record)
|
|
66
|
+
throw new Error(`no local session "${ref}"; see \`heyditto sessions\``);
|
|
67
|
+
const oldHarnessId = record.harnessSessionId ?? record.id;
|
|
68
|
+
const newId = randomUUID();
|
|
69
|
+
const sourceCwd = record.worktree ?? record.cwd;
|
|
70
|
+
let cwd = sourceCwd;
|
|
71
|
+
let worktree;
|
|
72
|
+
if (options.worktree !== undefined && options.worktree !== false) {
|
|
73
|
+
const name = typeof options.worktree === "string" ? options.worktree : defaultWorktreeName(record.harness);
|
|
74
|
+
if (!validWorktreeName(name))
|
|
75
|
+
throw new Error(`invalid worktree name "${name}"`);
|
|
76
|
+
const root = repoRoot(record.cwd);
|
|
77
|
+
if (!root)
|
|
78
|
+
throw new Error(`${record.cwd} is not inside a git repository; forks into a worktree need one`);
|
|
79
|
+
const wt = await ensureWorktree(root, name);
|
|
80
|
+
cwd = wt.path;
|
|
81
|
+
worktree = wt.path;
|
|
82
|
+
}
|
|
83
|
+
const transcript = record.harness === "claude"
|
|
84
|
+
? await forkClaudeTranscript(oldHarnessId, newId, sourceCwd, cwd)
|
|
85
|
+
: await forkCodexTranscript(oldHarnessId, newId);
|
|
86
|
+
const now = new Date().toISOString();
|
|
87
|
+
const session = {
|
|
88
|
+
id: newId,
|
|
89
|
+
harness: record.harness,
|
|
90
|
+
endpointId: record.endpointId,
|
|
91
|
+
endpointSlug: record.endpointSlug,
|
|
92
|
+
harnessSessionId: newId,
|
|
93
|
+
cwd: worktree ? record.cwd : cwd,
|
|
94
|
+
worktree,
|
|
95
|
+
model: record.model,
|
|
96
|
+
createdAt: now,
|
|
97
|
+
lastLaunchedAt: now,
|
|
98
|
+
launches: 1,
|
|
99
|
+
};
|
|
100
|
+
await writeSession(session);
|
|
101
|
+
return { from: record.id, session, transcript };
|
|
102
|
+
}
|
|
103
|
+
/** Lists transcripts present for a project dir; used by tests and diagnostics. */
|
|
104
|
+
export async function listClaudeTranscripts(cwd) {
|
|
105
|
+
const dir = path.join(claudeProjectsDir(), cwdSlug(canonicalCwd(cwd)));
|
|
106
|
+
try {
|
|
107
|
+
return (await readdir(dir)).filter((f) => f.endsWith(".jsonl")).sort();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=fork.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fork.js","sourceRoot":"","sources":["../../src/remote/fork.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import { type Harness, type PlanInput } from "../agents/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Headless Remote Control: no terminal UI. Each delivered turn runs one
|
|
5
|
+
* harness invocation (`claude -p --resume …` / `codex exec resume --last …`)
|
|
6
|
+
* with the same endpoint, key and session id as an interactive launch, so the
|
|
7
|
+
* conversation lands in the same Ditto thread and can be picked up again from
|
|
8
|
+
* a TUI later.
|
|
9
|
+
*/
|
|
10
|
+
export interface HeadlessTurnInput {
|
|
11
|
+
harness: Harness;
|
|
12
|
+
/** Everything but prompt/resume, which this module fills in per turn. */
|
|
13
|
+
base: Omit<PlanInput, "prompt" | "resumeId" | "resumeLast" | "passthrough">;
|
|
14
|
+
prompt: string;
|
|
15
|
+
/** Claude: the session id to resume (after the first turn). Codex: resume the last thread. */
|
|
16
|
+
resumeId?: string;
|
|
17
|
+
resumeLast?: boolean;
|
|
18
|
+
cwd: string;
|
|
19
|
+
/** Extra args (e.g. Codex `-c notify=…`, Claude `--settings …`). */
|
|
20
|
+
extraArgs?: string[];
|
|
21
|
+
onProgress?: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface HeadlessTurn {
|
|
24
|
+
child: ChildProcess;
|
|
25
|
+
exit: Promise<number | null>;
|
|
26
|
+
interrupt(): void;
|
|
27
|
+
}
|
|
28
|
+
/** One line of progress from Claude's stream-json output, or undefined when the event is not worth showing. */
|
|
29
|
+
export declare function summarizeClaudeEvent(line: string): string | undefined;
|
|
30
|
+
export declare function startHeadlessTurn(input: HeadlessTurnInput): HeadlessTurn;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { planClaude } from "../agents/claude.js";
|
|
3
|
+
import { planCodex } from "../agents/codex.js";
|
|
4
|
+
import { childEnv } from "../agents/types.js";
|
|
5
|
+
import { withHookArgs } from "./hooks.js";
|
|
6
|
+
/** One line of progress from Claude's stream-json output, or undefined when the event is not worth showing. */
|
|
7
|
+
export function summarizeClaudeEvent(line) {
|
|
8
|
+
let ev;
|
|
9
|
+
try {
|
|
10
|
+
ev = JSON.parse(line);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const type = ev.type;
|
|
16
|
+
if (type === "assistant") {
|
|
17
|
+
const message = ev.message;
|
|
18
|
+
for (const part of message?.content ?? []) {
|
|
19
|
+
if (part.type === "tool_use")
|
|
20
|
+
return `tool ${part.name ?? "?"}`;
|
|
21
|
+
if (part.type === "text" && part.text)
|
|
22
|
+
return part.text.replace(/\s+/g, " ").slice(0, 100);
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (type === "result") {
|
|
27
|
+
const result = typeof ev.result === "string" ? ev.result : "";
|
|
28
|
+
return `done${ev.is_error ? " (error)" : ""}${result ? `: ${result.replace(/\s+/g, " ").slice(0, 100)}` : ""}`;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
export function startHeadlessTurn(input) {
|
|
33
|
+
const planInput = {
|
|
34
|
+
...input.base,
|
|
35
|
+
prompt: input.prompt,
|
|
36
|
+
resumeId: input.resumeId,
|
|
37
|
+
resumeLast: input.resumeLast,
|
|
38
|
+
passthrough: input.harness === "claude" ? ["--output-format", "stream-json", "--verbose"] : [],
|
|
39
|
+
};
|
|
40
|
+
const plan = input.harness === "claude" ? planClaude(planInput) : planCodex(planInput);
|
|
41
|
+
const args = withHookArgs(input.harness, plan.args, input.extraArgs ?? []);
|
|
42
|
+
const child = spawn(plan.command, args, {
|
|
43
|
+
cwd: input.cwd,
|
|
44
|
+
env: childEnv(plan, input.base.env),
|
|
45
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
46
|
+
});
|
|
47
|
+
let stdoutBuf = "";
|
|
48
|
+
child.stdout?.setEncoding("utf8");
|
|
49
|
+
child.stdout?.on("data", (chunk) => {
|
|
50
|
+
stdoutBuf += chunk;
|
|
51
|
+
let nl = stdoutBuf.indexOf("\n");
|
|
52
|
+
while (nl !== -1) {
|
|
53
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
54
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
55
|
+
if (line) {
|
|
56
|
+
const summary = input.harness === "claude" ? summarizeClaudeEvent(line) : line.slice(0, 120);
|
|
57
|
+
if (summary)
|
|
58
|
+
input.onProgress?.(summary);
|
|
59
|
+
}
|
|
60
|
+
nl = stdoutBuf.indexOf("\n");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
child.stderr?.setEncoding("utf8");
|
|
64
|
+
child.stderr?.on("data", (chunk) => {
|
|
65
|
+
const line = chunk.trim().split("\n").pop();
|
|
66
|
+
if (line)
|
|
67
|
+
input.onProgress?.(line.slice(0, 160));
|
|
68
|
+
});
|
|
69
|
+
const exit = new Promise((resolve) => {
|
|
70
|
+
child.on("error", () => resolve(null));
|
|
71
|
+
child.on("exit", (code, signal) => resolve(code ?? (signal ? 130 : null)));
|
|
72
|
+
});
|
|
73
|
+
return { child, exit, interrupt: () => child.kill("SIGINT") };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=headless.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headless.js","sourceRoot":"","sources":["../../src/remote/headless.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook relay run by the coding harness: `hook.js <socket> <source> [event] [json]`.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code hooks pass their payload on stdin (with `hook_event_name`);
|
|
6
|
+
* Codex's `notify` passes a JSON payload as the last argument. Either way one
|
|
7
|
+
* JSON line goes to the Unix socket the CLI host opened, and the process exits
|
|
8
|
+
* 0 no matter what, so a missing socket can never block the harness.
|
|
9
|
+
*/
|
|
10
|
+
import net from "node:net";
|
|
11
|
+
function readStdin(timeoutMs) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
if (process.stdin.isTTY)
|
|
14
|
+
return resolve("");
|
|
15
|
+
let data = "";
|
|
16
|
+
const timer = setTimeout(() => resolve(data), timeoutMs);
|
|
17
|
+
process.stdin.setEncoding("utf8");
|
|
18
|
+
process.stdin.on("data", (c) => (data += c));
|
|
19
|
+
process.stdin.on("end", () => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
resolve(data);
|
|
22
|
+
});
|
|
23
|
+
process.stdin.on("error", () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
resolve(data);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function parse(json) {
|
|
30
|
+
try {
|
|
31
|
+
const v = JSON.parse(json);
|
|
32
|
+
return typeof v === "object" && v !== null ? v : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function main() {
|
|
39
|
+
const [socketPath, source, ...rest] = process.argv.slice(2);
|
|
40
|
+
if (!socketPath || !source)
|
|
41
|
+
return;
|
|
42
|
+
let event = rest[0] ?? "";
|
|
43
|
+
let payload;
|
|
44
|
+
if (source === "codex") {
|
|
45
|
+
// codex appends its JSON payload after our fixed arguments.
|
|
46
|
+
payload = parse(rest[rest.length - 1] ?? "");
|
|
47
|
+
event = typeof payload?.type === "string" ? payload.type : "agent-turn-complete";
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
payload = parse(await readStdin(1500));
|
|
51
|
+
if (!event && typeof payload?.hook_event_name === "string")
|
|
52
|
+
event = payload.hook_event_name;
|
|
53
|
+
}
|
|
54
|
+
await new Promise((resolve) => {
|
|
55
|
+
const timer = setTimeout(resolve, 1500);
|
|
56
|
+
const conn = net.createConnection(socketPath, () => {
|
|
57
|
+
conn.end(`${JSON.stringify({ source, event, payload })}\n`, () => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
conn.on("error", () => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
main().finally(() => process.exit(0));
|
|
69
|
+
//# sourceMappingURL=hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../../src/remote/hook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { Harness } from "../agents/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Turn boundaries come from the harnesses themselves, not from parsing their
|
|
5
|
+
* screens: Claude Code runs its `UserPromptSubmit` and `Stop` hooks, Codex runs
|
|
6
|
+
* its `notify` command when a turn ends. Both are pointed at a tiny script
|
|
7
|
+
* (dist/remote/hook.js) that writes one JSON line to a Unix socket this
|
|
8
|
+
* process listens on. Nothing in the hook can stall the harness: the script
|
|
9
|
+
* exits 0 whatever happens.
|
|
10
|
+
*/
|
|
11
|
+
export interface HookEvent {
|
|
12
|
+
source: Harness;
|
|
13
|
+
/** Claude: UserPromptSubmit | Stop. Codex: agent-turn-complete (as notify reports it). */
|
|
14
|
+
event: string;
|
|
15
|
+
payload?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface HookServer {
|
|
18
|
+
socketPath: string;
|
|
19
|
+
events: EventEmitter;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** Absolute path of the compiled hook script next to this module. */
|
|
23
|
+
export declare function hookScriptPath(): string;
|
|
24
|
+
export declare function startHookServer(): Promise<HookServer>;
|
|
25
|
+
/** The `--settings <json>` value that makes Claude Code report turn boundaries to `socketPath`. */
|
|
26
|
+
export declare function claudeHookSettings(socketPath: string, script?: string, node?: string): string;
|
|
27
|
+
/** The `-c notify=[...]` override that makes Codex report `agent-turn-complete` to `socketPath`. */
|
|
28
|
+
export declare function codexNotifyOverride(socketPath: string, script?: string, node?: string): string;
|
|
29
|
+
/** Inserts flags where each harness accepts them: after Codex's leading subcommand words (before its positional prompt), anywhere for Claude. */
|
|
30
|
+
export declare function withHookArgs(harness: Harness, args: string[], extra: string[]): string[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
/** Absolute path of the compiled hook script next to this module. */
|
|
9
|
+
export function hookScriptPath() {
|
|
10
|
+
return fileURLToPath(new URL("./hook.js", import.meta.url));
|
|
11
|
+
}
|
|
12
|
+
export async function startHookServer() {
|
|
13
|
+
const socketPath = path.join(os.tmpdir(), `heyditto-hook-${process.pid}-${randomBytes(4).toString("hex")}.sock`);
|
|
14
|
+
const events = new EventEmitter();
|
|
15
|
+
const server = net.createServer((conn) => {
|
|
16
|
+
let buf = "";
|
|
17
|
+
conn.setEncoding("utf8");
|
|
18
|
+
conn.on("data", (chunk) => {
|
|
19
|
+
buf += chunk;
|
|
20
|
+
let nl = buf.indexOf("\n");
|
|
21
|
+
while (nl !== -1) {
|
|
22
|
+
const line = buf.slice(0, nl).trim();
|
|
23
|
+
buf = buf.slice(nl + 1);
|
|
24
|
+
if (line)
|
|
25
|
+
emitLine(events, line);
|
|
26
|
+
nl = buf.indexOf("\n");
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
conn.on("end", () => {
|
|
30
|
+
const line = buf.trim();
|
|
31
|
+
buf = "";
|
|
32
|
+
if (line)
|
|
33
|
+
emitLine(events, line);
|
|
34
|
+
});
|
|
35
|
+
conn.on("error", () => undefined);
|
|
36
|
+
});
|
|
37
|
+
await new Promise((resolve, reject) => {
|
|
38
|
+
server.once("error", reject);
|
|
39
|
+
server.listen(socketPath, () => resolve());
|
|
40
|
+
});
|
|
41
|
+
server.unref();
|
|
42
|
+
return {
|
|
43
|
+
socketPath,
|
|
44
|
+
events,
|
|
45
|
+
close: async () => {
|
|
46
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
47
|
+
await rm(socketPath, { force: true }).catch(() => undefined);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function emitLine(events, line) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(line);
|
|
54
|
+
if (parsed && typeof parsed.event === "string")
|
|
55
|
+
events.emit("hook", parsed);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ignore malformed hook lines */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function shellQuote(s) {
|
|
62
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
63
|
+
}
|
|
64
|
+
/** The `--settings <json>` value that makes Claude Code report turn boundaries to `socketPath`. */
|
|
65
|
+
export function claudeHookSettings(socketPath, script = hookScriptPath(), node = process.execPath) {
|
|
66
|
+
const command = (event) => `${shellQuote(node)} ${shellQuote(script)} ${shellQuote(socketPath)} claude ${event}`;
|
|
67
|
+
const settings = {
|
|
68
|
+
hooks: {
|
|
69
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: command("UserPromptSubmit"), timeout: 5 }] }],
|
|
70
|
+
Stop: [{ hooks: [{ type: "command", command: command("Stop"), timeout: 5 }] }],
|
|
71
|
+
// Permission dialogs arrive as notification_type "permission_prompt".
|
|
72
|
+
Notification: [{ hooks: [{ type: "command", command: command("Notification"), timeout: 5 }] }],
|
|
73
|
+
// AskUserQuestion carries its question and options in tool_input.
|
|
74
|
+
PreToolUse: [{ matcher: "AskUserQuestion", hooks: [{ type: "command", command: command("PreToolUse"), timeout: 5 }] }],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
return JSON.stringify(settings);
|
|
78
|
+
}
|
|
79
|
+
/** The `-c notify=[...]` override that makes Codex report `agent-turn-complete` to `socketPath`. */
|
|
80
|
+
export function codexNotifyOverride(socketPath, script = hookScriptPath(), node = process.execPath) {
|
|
81
|
+
const toml = (s) => JSON.stringify(s);
|
|
82
|
+
return `notify=[${[node, script, socketPath, "codex"].map(toml).join(",")}]`;
|
|
83
|
+
}
|
|
84
|
+
/** Inserts flags where each harness accepts them: after Codex's leading subcommand words (before its positional prompt), anywhere for Claude. */
|
|
85
|
+
export function withHookArgs(harness, args, extra) {
|
|
86
|
+
if (extra.length === 0)
|
|
87
|
+
return args;
|
|
88
|
+
if (harness === "claude")
|
|
89
|
+
return [...args, ...extra];
|
|
90
|
+
let i = 0;
|
|
91
|
+
while (i < args.length && (args[i] === "exec" || args[i] === "resume"))
|
|
92
|
+
i += 1;
|
|
93
|
+
return [...args.slice(0, i), ...extra, ...args.slice(i)];
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/remote/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAyBzC,qEAAqE;AACrE,MAAM,UAAU,cAAc;IAC5B,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,iBAAiB,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjH,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE;QACvC,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YACxB,GAAG,IAAI,KAAK,CAAC;YACb,IAAI,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC3B,OAAO,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBACrC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;gBACxB,IAAI,IAAI;oBAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBACjC,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YAClB,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;YACxB,GAAG,GAAG,EAAE,CAAC;YACT,IAAI,IAAI;gBAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,OAAO;QACL,UAAU;QACV,MAAM;QACN,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACpE,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC/D,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,MAAoB,EAAE,IAAY;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAc,CAAC;QAC7C,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9E,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;IACnC,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AACzC,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,kBAAkB,CAAC,UAAkB,EAAE,MAAM,GAAG,cAAc,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ;IACvG,MAAM,OAAO,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,WAAW,KAAK,EAAE,CAAC;IACzH,MAAM,QAAQ,GAAG;QACf,KAAK,EAAE;YACL,gBAAgB,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YACtG,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YAC9E,sEAAsE;YACtE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YAC9F,kEAAkE;YAClE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;SACvH;KACF,CAAC;IACF,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;AAClC,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,mBAAmB,CAAC,UAAkB,EAAE,MAAM,GAAG,cAAc,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ;IACxG,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC9C,OAAO,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAC/E,CAAC;AAED,iJAAiJ;AACjJ,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,IAAc,EAAE,KAAe;IAC5E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,CAAC,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC;IACrD,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC;QAAE,CAAC,IAAI,CAAC,CAAC;IAC/E,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type CheckpointRequestFrame, type HostCapabilities, type HostFrame, type PromptAnswerFrame, type SessionAnnounceFrame, type SessionCommandsFrame, type SessionStatus, type TurnDeliverFrame } from "./protocol.js";
|
|
2
|
+
/**
|
|
3
|
+
* The host side of the bridge: one WebSocket to the backend that announces
|
|
4
|
+
* the coding sessions this machine runs and receives turns for them.
|
|
5
|
+
*
|
|
6
|
+
* The connection is best effort. A backend without the hosts endpoint, or a
|
|
7
|
+
* flaky network, never blocks the harness: the client keeps retrying in the
|
|
8
|
+
* background with backoff and re-announces every known session when it gets
|
|
9
|
+
* back in, and the harness keeps running locally the whole time.
|
|
10
|
+
*/
|
|
11
|
+
export interface HostHandlers {
|
|
12
|
+
onTurn: (turn: TurnDeliverFrame) => void;
|
|
13
|
+
onInterrupt: (turnId: string) => void;
|
|
14
|
+
onCheckpoint: (request: CheckpointRequestFrame) => void;
|
|
15
|
+
onPromptAnswer?: (answer: PromptAnswerFrame) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface HostClientOptions {
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
capabilities: HostCapabilities;
|
|
21
|
+
handlers: HostHandlers;
|
|
22
|
+
name?: string;
|
|
23
|
+
/** Called for status lines; defaults to silence. */
|
|
24
|
+
log?: (line: string) => void;
|
|
25
|
+
/** Overrides for tests. */
|
|
26
|
+
heartbeatSeconds?: number;
|
|
27
|
+
reconnectBaseMs?: number;
|
|
28
|
+
reconnectMaxMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class HostClient {
|
|
31
|
+
readonly options: HostClientOptions;
|
|
32
|
+
hostId?: string;
|
|
33
|
+
private socket?;
|
|
34
|
+
private connected;
|
|
35
|
+
private closing;
|
|
36
|
+
private attempt;
|
|
37
|
+
private heartbeat?;
|
|
38
|
+
private missedPongs;
|
|
39
|
+
private readonly sessions;
|
|
40
|
+
private readonly catalogs;
|
|
41
|
+
private readonly seenTurns;
|
|
42
|
+
private readonly seenSet;
|
|
43
|
+
private welcomeWaiters;
|
|
44
|
+
constructor(options: HostClientOptions);
|
|
45
|
+
get isConnected(): boolean;
|
|
46
|
+
/** Starts connecting; resolves after the first `welcome` or after `timeoutMs` (still retrying in the background). */
|
|
47
|
+
start(timeoutMs?: number): Promise<boolean>;
|
|
48
|
+
private connectLoop;
|
|
49
|
+
private connectOnce;
|
|
50
|
+
private onWelcome;
|
|
51
|
+
private dispatch;
|
|
52
|
+
private remember;
|
|
53
|
+
private startHeartbeat;
|
|
54
|
+
private stopHeartbeat;
|
|
55
|
+
send(frame: HostFrame): boolean;
|
|
56
|
+
announce(session: Omit<SessionAnnounceFrame, "type">): void;
|
|
57
|
+
/** Announces a session's command catalog; re-sent with the session after a reconnect. */
|
|
58
|
+
commands(frame: Omit<SessionCommandsFrame, "type">): void;
|
|
59
|
+
status(sessionId: string, status: SessionStatus): void;
|
|
60
|
+
closed(sessionId: string): void;
|
|
61
|
+
/** Announces every session closed and stops reconnecting. */
|
|
62
|
+
close(): void;
|
|
63
|
+
}
|