@agentproto/adapter-claude-code-cloud 0.0.0-stage → 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/CLAUDE-CODE-CLOUD.md +99 -0
- package/LICENSE +202 -0
- package/README.md +52 -2
- package/bin/claude-code-cloud-acp.mjs +9 -0
- package/dist/acp-server.mjs +3 -0
- package/dist/acp-server.mjs.map +1 -0
- package/dist/chunk-KA4G4O2X.mjs +709 -0
- package/dist/chunk-KA4G4O2X.mjs.map +1 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.mjs +114 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -3
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { Writable, Readable } from 'stream';
|
|
2
|
+
import { PROTOCOL_VERSION, ndJsonStream, AgentSideConnection } from '@agentclientprotocol/sdk';
|
|
3
|
+
import { sdkMessageToUpdates, promptText } from '@agentproto/adapter-claude-sdk';
|
|
4
|
+
export { promptText } from '@agentproto/adapter-claude-sdk';
|
|
5
|
+
import { execFile, spawn } from 'child_process';
|
|
6
|
+
import { ANTHROPIC_CORE_SCRUB_ENV } from '@agentproto/provider-presets';
|
|
7
|
+
import { readFile } from 'fs/promises';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @agentproto/adapter-claude-code-cloud v0.1.0
|
|
14
|
+
* AIP-66 managed cloud agent adapter: Claude Code cloud sessions behind ACP.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var execFileAsync = promisify(execFile);
|
|
18
|
+
var KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
19
|
+
function keychainCredentialSource(deps = {}) {
|
|
20
|
+
const execFileImpl = deps.execFileImpl ?? (async (cmd, args) => {
|
|
21
|
+
const { stdout } = await execFileAsync(cmd, args, {
|
|
22
|
+
// `security` writes the secret to stdout only; maxBuffer covers a
|
|
23
|
+
// long JSON blob. Never `echo` it anywhere else.
|
|
24
|
+
maxBuffer: 1 << 20
|
|
25
|
+
});
|
|
26
|
+
return { stdout };
|
|
27
|
+
});
|
|
28
|
+
const readFileSyncImpl = deps.readFileSyncImpl ?? ((path) => readFile(path, "utf8"));
|
|
29
|
+
const home = deps.homedir ?? homedir;
|
|
30
|
+
let cached = null;
|
|
31
|
+
return {
|
|
32
|
+
async get() {
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
const { stdout } = await execFileImpl("security", [
|
|
35
|
+
"find-generic-password",
|
|
36
|
+
"-s",
|
|
37
|
+
KEYCHAIN_SERVICE,
|
|
38
|
+
"-w"
|
|
39
|
+
]);
|
|
40
|
+
const keychain = JSON.parse(stdout);
|
|
41
|
+
const accessToken = keychain.claudeAiOauth?.accessToken;
|
|
42
|
+
if (!accessToken) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"claude code cloud: no claudeAiOauth.accessToken in the Claude Code keychain entry \u2014 run `claude auth login`"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
let orgUuid;
|
|
48
|
+
try {
|
|
49
|
+
const rc = JSON.parse(
|
|
50
|
+
await readFileSyncImpl(join(home(), ".claude.json"))
|
|
51
|
+
);
|
|
52
|
+
orgUuid = rc.oauthAccount?.organizationUuid;
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
cached = { accessToken, ...orgUuid ? { orgUuid } : {} };
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function envCredentialSource(env = process.env) {
|
|
61
|
+
return {
|
|
62
|
+
async get() {
|
|
63
|
+
const accessToken = env.CLAUDE_CLOUD_OAUTH_TOKEN;
|
|
64
|
+
if (!accessToken) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"claude code cloud: CLAUDE_CLOUD_OAUTH_TOKEN is not set \u2014 run `claude auth login` and use the keychain credential source, or inject the token"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const orgUuid = env.CLAUDE_CLOUD_ORG_UUID;
|
|
70
|
+
return { accessToken, ...orgUuid ? { orgUuid } : {} };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function normalizeSessionId(id) {
|
|
75
|
+
const trimmed = id.trim();
|
|
76
|
+
if (trimmed.startsWith("session_")) return `cse_${trimmed.slice("session_".length)}`;
|
|
77
|
+
if (trimmed.startsWith("cse_")) return trimmed;
|
|
78
|
+
throw new Error(
|
|
79
|
+
`claude code cloud: unrecognized session id ${JSON.stringify(id)} \u2014 expected session_<id> or cse_<id>`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
function toCliSessionId(id) {
|
|
83
|
+
const normalized = normalizeSessionId(id);
|
|
84
|
+
return `session_${normalized.slice("cse_".length)}`;
|
|
85
|
+
}
|
|
86
|
+
var UNAUTHORIZED_HINT = "run `claude auth login`";
|
|
87
|
+
var SessionsApi = class {
|
|
88
|
+
#credentials;
|
|
89
|
+
#fetch;
|
|
90
|
+
#baseUrl;
|
|
91
|
+
#credential = null;
|
|
92
|
+
constructor(options) {
|
|
93
|
+
this.#credentials = options.credentials;
|
|
94
|
+
this.#fetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
95
|
+
this.#baseUrl = (options.baseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
|
|
96
|
+
}
|
|
97
|
+
/** GET /v1/code/sessions/{id} */
|
|
98
|
+
async getSession(id) {
|
|
99
|
+
return this.#request(
|
|
100
|
+
"GET",
|
|
101
|
+
`/v1/code/sessions/${normalizeSessionId(id)}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* GET /v1/code/sessions/{id}/events — newest first. Pass the previous
|
|
106
|
+
* page's `resume_cursor` as `cursor` to resume after it.
|
|
107
|
+
*/
|
|
108
|
+
async listEvents(id, opts = {}) {
|
|
109
|
+
const params = new URLSearchParams();
|
|
110
|
+
if (opts.cursor) params.set("cursor", opts.cursor);
|
|
111
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
112
|
+
const qs = params.toString();
|
|
113
|
+
return this.#request(
|
|
114
|
+
"GET",
|
|
115
|
+
`/v1/code/sessions/${normalizeSessionId(id)}/events${qs ? `?${qs}` : ""}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
/** GET /v1/code/sessions?limit=N */
|
|
119
|
+
async listSessions(opts = {}) {
|
|
120
|
+
const qs = opts.limit !== void 0 ? `?limit=${opts.limit}` : "";
|
|
121
|
+
return this.#request(
|
|
122
|
+
"GET",
|
|
123
|
+
`/v1/code/sessions${qs}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
async #credentialNow() {
|
|
127
|
+
if (!this.#credential) this.#credential = await this.#credentials.get();
|
|
128
|
+
return this.#credential;
|
|
129
|
+
}
|
|
130
|
+
async #request(method, path, retriedAuth = false) {
|
|
131
|
+
const credential = await this.#credentialNow();
|
|
132
|
+
const headers = {
|
|
133
|
+
authorization: `Bearer ${credential.accessToken}`,
|
|
134
|
+
"anthropic-version": "2023-06-01",
|
|
135
|
+
"content-type": "application/json",
|
|
136
|
+
...credential.orgUuid ? { "x-organization-uuid": credential.orgUuid } : {}
|
|
137
|
+
};
|
|
138
|
+
const res = await this.#fetch(`${this.#baseUrl}${path}`, { method, headers });
|
|
139
|
+
if (res.status === 401 && !retriedAuth) {
|
|
140
|
+
this.#credential = null;
|
|
141
|
+
return this.#request(method, path, true);
|
|
142
|
+
}
|
|
143
|
+
if (res.status === 401) {
|
|
144
|
+
throw new Error(`claude code cloud: unauthorized (401) \u2014 ${UNAUTHORIZED_HINT}`);
|
|
145
|
+
}
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`claude code cloud: ${method} ${path} failed: HTTP ${res.status}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return await res.json();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// src/cli.ts
|
|
156
|
+
var CLAUDE_CODE_CLOUD_TOGGLES = [
|
|
157
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
158
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
159
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
160
|
+
"CLAUDE_CODE_USE_ANTHROPIC_AWS",
|
|
161
|
+
"CLAUDE_CODE_USE_MANTLE",
|
|
162
|
+
"CLAUDE_CODE_USE_GATEWAY"
|
|
163
|
+
];
|
|
164
|
+
var CLAUDE_CODE_GATEWAY_ENV_UNSET = [
|
|
165
|
+
...ANTHROPIC_CORE_SCRUB_ENV,
|
|
166
|
+
...CLAUDE_CODE_CLOUD_TOGGLES
|
|
167
|
+
];
|
|
168
|
+
function cloudChildEnv(base = process.env) {
|
|
169
|
+
const out = {};
|
|
170
|
+
const drop = new Set(CLAUDE_CODE_GATEWAY_ENV_UNSET);
|
|
171
|
+
for (const [key, value] of Object.entries(base)) {
|
|
172
|
+
if (value === void 0 || drop.has(key)) continue;
|
|
173
|
+
out[key] = value;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
var DEFAULT_RUN_TIMEOUT_MS = 6e4;
|
|
178
|
+
function defaultCloudCliRunner() {
|
|
179
|
+
return {
|
|
180
|
+
run(cmd, args, opts) {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const child = spawn(cmd, args, {
|
|
183
|
+
cwd: opts.cwd,
|
|
184
|
+
env: opts.env,
|
|
185
|
+
stdio: [opts.stdin === "ignore" ? "ignore" : "inherit", "pipe", "pipe"]
|
|
186
|
+
});
|
|
187
|
+
let stdout = "";
|
|
188
|
+
let stderr = "";
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
child.kill("SIGKILL");
|
|
191
|
+
reject(
|
|
192
|
+
new Error(
|
|
193
|
+
`claude code cloud: \`${cmd} ${args.join(" ")}\` timed out after ${opts.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS}ms`
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
}, opts.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS);
|
|
197
|
+
timer.unref?.();
|
|
198
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
199
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
200
|
+
child.on("error", (err) => {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
reject(err);
|
|
203
|
+
});
|
|
204
|
+
child.on("close", (code) => {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
resolve({ code: code ?? -1, stdout, stderr });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
runPty(cmd, args, opts) {
|
|
211
|
+
let resolveResult;
|
|
212
|
+
let rejectResult;
|
|
213
|
+
const result = new Promise((res, rej) => {
|
|
214
|
+
resolveResult = res;
|
|
215
|
+
rejectResult = rej;
|
|
216
|
+
});
|
|
217
|
+
let proc = null;
|
|
218
|
+
const pendingWrites = [];
|
|
219
|
+
void import('node-pty').then((mod) => {
|
|
220
|
+
const pty = mod.default ?? mod;
|
|
221
|
+
if (typeof pty.spawn !== "function") throw new Error("node-pty has no spawn");
|
|
222
|
+
const p = pty.spawn(cmd, args, {
|
|
223
|
+
name: "xterm-256color",
|
|
224
|
+
cols: 120,
|
|
225
|
+
rows: 40,
|
|
226
|
+
cwd: opts.cwd,
|
|
227
|
+
env: opts.env
|
|
228
|
+
});
|
|
229
|
+
proc = p;
|
|
230
|
+
for (const data of pendingWrites) p.write(data);
|
|
231
|
+
pendingWrites.length = 0;
|
|
232
|
+
let output = "";
|
|
233
|
+
let answeredTrustPrompt = false;
|
|
234
|
+
p.onData((chunk) => {
|
|
235
|
+
output += chunk;
|
|
236
|
+
opts.onData?.(chunk);
|
|
237
|
+
if (!answeredTrustPrompt && /Yes, I trust this folder/.test(output)) {
|
|
238
|
+
answeredTrustPrompt = true;
|
|
239
|
+
p.write("\x1B[B\r");
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
const timer = setTimeout(() => {
|
|
243
|
+
try {
|
|
244
|
+
p.kill("SIGKILL");
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
rejectResult(
|
|
248
|
+
new Error(
|
|
249
|
+
`claude code cloud: \`${cmd} ${args.join(" ")}\` timed out after ${opts.timeoutMs ?? 12e4}ms under the PTY`
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}, opts.timeoutMs ?? 12e4);
|
|
253
|
+
timer.unref?.();
|
|
254
|
+
p.onExit(({ exitCode }) => {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
resolveResult({ code: exitCode, output });
|
|
257
|
+
});
|
|
258
|
+
}).catch((err) => {
|
|
259
|
+
rejectResult(
|
|
260
|
+
new Error(
|
|
261
|
+
`claude code cloud: \`claude --cloud\` needs a real PTY and node-pty is not available (${err instanceof Error ? err.message : String(err)}) \u2014 install it (npm i node-pty) to use managed cloud sessions`
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
write: (data) => {
|
|
267
|
+
if (proc) proc.write(data);
|
|
268
|
+
else pendingWrites.push(data);
|
|
269
|
+
},
|
|
270
|
+
result
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function parseAuthStatus(stdout) {
|
|
276
|
+
const trimmed = stdout.trim();
|
|
277
|
+
const start = trimmed.indexOf("{");
|
|
278
|
+
const end = trimmed.lastIndexOf("}");
|
|
279
|
+
if (start === -1 || end === -1 || end < start) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
"claude code cloud: could not parse `claude auth status` output \u2014 run `claude auth login`"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
285
|
+
return { loggedIn: parsed.loggedIn === true, authMethod: parsed.authMethod };
|
|
286
|
+
}
|
|
287
|
+
async function preflightAuth(runner, opts) {
|
|
288
|
+
let result;
|
|
289
|
+
try {
|
|
290
|
+
result = await runner.run("claude", ["auth", "status"], {
|
|
291
|
+
cwd: opts.cwd,
|
|
292
|
+
env: opts.env,
|
|
293
|
+
timeoutMs: 15e3,
|
|
294
|
+
stdin: "ignore"
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`claude code cloud: could not run \`claude auth status\` \u2014 is the claude CLI installed and on PATH? (${err instanceof Error ? err.message : String(err)})`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
"claude code cloud: `claude auth status` failed \u2014 the claude CLI is not usable; run `claude auth login`"
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const status = parseAuthStatus(result.stdout);
|
|
307
|
+
if (!status.loggedIn) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"claude code cloud: not logged in \u2014 cloud sessions need a claude.ai sign-in; run `claude auth login`"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (status.authMethod === "api_key") {
|
|
313
|
+
throw new Error(
|
|
314
|
+
"claude code cloud: authenticated by API key \u2014 cloud sessions need a claude.ai sign-in; run `claude auth login`"
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return status;
|
|
318
|
+
}
|
|
319
|
+
function parseCloudStartOutput(output) {
|
|
320
|
+
let sessionId;
|
|
321
|
+
let url;
|
|
322
|
+
let title;
|
|
323
|
+
const resume = output.match(/Resume with:\s*claude\s+--teleport\s+(session_[A-Za-z0-9_-]+)/);
|
|
324
|
+
if (resume?.[1]) sessionId = resume[1];
|
|
325
|
+
const view = output.match(/View:\s*(https?:\/\/\S+)/);
|
|
326
|
+
if (view?.[1]) {
|
|
327
|
+
url = view[1];
|
|
328
|
+
if (!sessionId) {
|
|
329
|
+
const fromUrl = url.match(/\/session_([A-Za-z0-9_-]+)/);
|
|
330
|
+
if (fromUrl?.[1]) sessionId = `session_${fromUrl[1]}`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const created = output.match(/Created cloud session:\s*(.+)/);
|
|
334
|
+
if (created?.[1]) title = created[1].trim();
|
|
335
|
+
if (!sessionId) return null;
|
|
336
|
+
return {
|
|
337
|
+
sessionId,
|
|
338
|
+
url: url ?? `https://claude.ai/code/${sessionId}`,
|
|
339
|
+
...title ? { title } : {}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async function startCloudSession(prompt, opts) {
|
|
343
|
+
const runner = opts.runner ?? defaultCloudCliRunner();
|
|
344
|
+
const env = cloudChildEnv(opts.env ?? process.env);
|
|
345
|
+
if (!opts.skipPreflight) await preflightAuth(runner, { cwd: opts.cwd, env });
|
|
346
|
+
const pty = runner.runPty("claude", ["--cloud", prompt], {
|
|
347
|
+
cwd: opts.cwd,
|
|
348
|
+
env,
|
|
349
|
+
timeoutMs: opts.timeoutMs ?? 12e4
|
|
350
|
+
});
|
|
351
|
+
const { code, output } = await pty.result;
|
|
352
|
+
if (code !== 0) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`claude code cloud: \`claude --cloud\` exited ${code}: ${output.trim().split("\n").slice(-5).join("\n").trim()}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const started = parseCloudStartOutput(output);
|
|
358
|
+
if (!started) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
"claude code cloud: could not parse a cloud session id from `claude --cloud` output"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return started;
|
|
364
|
+
}
|
|
365
|
+
async function sendPrompt(sessionId, text, opts) {
|
|
366
|
+
const runner = opts.runner ?? defaultCloudCliRunner();
|
|
367
|
+
const env = cloudChildEnv(opts.env ?? process.env);
|
|
368
|
+
if (!opts.skipPreflight) await preflightAuth(runner, { cwd: opts.cwd, env });
|
|
369
|
+
const result = await runner.run(
|
|
370
|
+
"claude",
|
|
371
|
+
["-p", text, "--cloud", toCliSessionId(sessionId)],
|
|
372
|
+
{
|
|
373
|
+
cwd: opts.cwd,
|
|
374
|
+
env,
|
|
375
|
+
stdin: "ignore",
|
|
376
|
+
timeoutMs: opts.timeoutMs ?? 6e4
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
if (result.code !== 0) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`claude code cloud: \`claude -p --cloud\` exited ${result.code}: ${(result.stderr || result.stdout).trim().split("\n").slice(-5).join("\n").trim()}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return { stdout: result.stdout };
|
|
385
|
+
}
|
|
386
|
+
var execFileAsync2 = promisify(execFile);
|
|
387
|
+
var defaultGitRunner = async (args, cwd) => {
|
|
388
|
+
try {
|
|
389
|
+
const { stdout, stderr } = await execFileAsync2("git", args, { cwd });
|
|
390
|
+
return { code: 0, stdout, stderr };
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const e = err;
|
|
393
|
+
return {
|
|
394
|
+
code: typeof e.code === "number" ? e.code : 1,
|
|
395
|
+
stdout: e.stdout ?? "",
|
|
396
|
+
stderr: e.stderr ?? ""
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
async function validateRepoSource(cwd, git = defaultGitRunner) {
|
|
401
|
+
const inside = await git(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
402
|
+
if (inside.code !== 0 || inside.stdout.trim() !== "true") {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`claude code cloud: ${cwd} is not a git checkout \u2014 managed cloud agents run on a git remote and ref (AIP-66 \xA73)`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const status = await git(["status", "--porcelain"], cwd);
|
|
408
|
+
if (status.code !== 0) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`claude code cloud: could not read the working tree status in ${cwd} (${status.stderr.trim() || `git status exited ${status.code}`})`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (status.stdout.trim().length > 0) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`claude code cloud: the working tree in ${cwd} is dirty \u2014 commit or stash first; the vendor refuses to upload uncommitted files (AIP-66 \xA73)`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const remote = await git(["remote", "get-url", "origin"], cwd);
|
|
419
|
+
if (remote.code !== 0 || !remote.stdout.trim()) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`claude code cloud: ${cwd} has no origin remote \u2014 a managed cloud agent runs on the repo's remote, not a local path (AIP-66 \xA73)`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const head = await git(["rev-parse", "HEAD"], cwd);
|
|
425
|
+
if (head.code !== 0 || !head.stdout.trim()) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`claude code cloud: could not resolve HEAD in ${cwd} (${head.stderr.trim() || `git rev-parse HEAD exited ${head.code}`})`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const contains = await git(["branch", "-r", "--contains", "HEAD"], cwd);
|
|
431
|
+
if (contains.code !== 0 || !contains.stdout.trim()) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`claude code cloud: HEAD in ${cwd} is not reachable from origin \u2014 push the commit before starting a cloud session (AIP-66 \xA73)`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/acp-server.ts
|
|
439
|
+
var POLL_INITIAL_MS = 2e3;
|
|
440
|
+
var POLL_MAX_MS = 1e4;
|
|
441
|
+
var MAX_LIST_EVENTS_FAILURES = 5;
|
|
442
|
+
var NETWORK_ERROR_CODES = /^(ECONNABORTED|ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|ENOTFOUND|EPIPE|ETIMEDOUT|UND_ERR)/;
|
|
443
|
+
function isTransientListEventsError(err) {
|
|
444
|
+
if (!(err instanceof Error)) return false;
|
|
445
|
+
if (/\bHTTP 5\d\d\b/.test(err.message)) return true;
|
|
446
|
+
if (/fetch failed|network error|socket hang up/i.test(err.message)) return true;
|
|
447
|
+
let cause = err;
|
|
448
|
+
for (let depth = 0; depth < 4 && cause; depth += 1) {
|
|
449
|
+
const code = cause.code;
|
|
450
|
+
if (typeof code === "string" && NETWORK_ERROR_CODES.test(code)) return true;
|
|
451
|
+
cause = cause.cause;
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
var SEEN_EVENT_IDS_LIMIT = 500;
|
|
456
|
+
function rememberSeenEventId(session, id) {
|
|
457
|
+
const seen = session.seenEventIds;
|
|
458
|
+
seen.add(id);
|
|
459
|
+
while (seen.size > SEEN_EVENT_IDS_LIMIT) {
|
|
460
|
+
const oldest = seen.values().next();
|
|
461
|
+
if (oldest.done) break;
|
|
462
|
+
seen.delete(oldest.value);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function defaultSleep(ms) {
|
|
466
|
+
return new Promise((resolve) => {
|
|
467
|
+
const timer = setTimeout(resolve, ms);
|
|
468
|
+
timer.unref?.();
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function defaultDeps() {
|
|
472
|
+
const runner = defaultCloudCliRunner();
|
|
473
|
+
const credentials = keychainCredentialSource();
|
|
474
|
+
return {
|
|
475
|
+
api: new SessionsApi({ credentials }),
|
|
476
|
+
cli: {
|
|
477
|
+
startCloudSession: (prompt, opts) => startCloudSession(prompt, {
|
|
478
|
+
cwd: opts.cwd,
|
|
479
|
+
runner,
|
|
480
|
+
env: process.env
|
|
481
|
+
}),
|
|
482
|
+
sendPrompt: (id, text, opts) => sendPrompt(id, text, {
|
|
483
|
+
cwd: opts.cwd,
|
|
484
|
+
runner,
|
|
485
|
+
env: process.env
|
|
486
|
+
})
|
|
487
|
+
},
|
|
488
|
+
validateRepo: (cwd) => validateRepoSource(cwd)
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function resultEventUpdates(payload) {
|
|
492
|
+
const p = payload ?? {};
|
|
493
|
+
const hasUsage = p.usage != null || typeof p.total_cost_usd === "number";
|
|
494
|
+
if (!hasUsage) return [];
|
|
495
|
+
const normalized = {
|
|
496
|
+
...p,
|
|
497
|
+
type: "result",
|
|
498
|
+
modelUsage: p.modelUsage ?? {},
|
|
499
|
+
usage: p.usage ?? { input_tokens: 0, output_tokens: 0 }
|
|
500
|
+
};
|
|
501
|
+
return sdkMessageToUpdates(normalized).filter(
|
|
502
|
+
(u) => u.sessionUpdate === "usage_update"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
function resultStopReason(payload) {
|
|
506
|
+
const p = payload ?? {};
|
|
507
|
+
if (p.is_error === true) return "refusal";
|
|
508
|
+
if (p.subtype !== void 0 && p.subtype !== "success") return "refusal";
|
|
509
|
+
return "end_turn";
|
|
510
|
+
}
|
|
511
|
+
function errorResultText(payload) {
|
|
512
|
+
const p = payload ?? {};
|
|
513
|
+
if (typeof p.result === "string" && p.result.trim()) return p.result;
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
var ClaudeCloudAcpAgent = class {
|
|
517
|
+
#conn;
|
|
518
|
+
#api;
|
|
519
|
+
#cli;
|
|
520
|
+
#validateRepo;
|
|
521
|
+
#sleep;
|
|
522
|
+
#pollInitialMs;
|
|
523
|
+
#pollMaxMs;
|
|
524
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
525
|
+
constructor(conn, deps = {}) {
|
|
526
|
+
const defaults = defaultDeps();
|
|
527
|
+
this.#conn = conn;
|
|
528
|
+
this.#api = deps.api ?? defaults.api;
|
|
529
|
+
this.#cli = deps.cli ?? defaults.cli;
|
|
530
|
+
this.#validateRepo = deps.validateRepo ?? defaults.validateRepo;
|
|
531
|
+
this.#sleep = deps.sleep ?? defaultSleep;
|
|
532
|
+
this.#pollInitialMs = deps.pollInitialMs ?? POLL_INITIAL_MS;
|
|
533
|
+
this.#pollMaxMs = deps.pollMaxMs ?? POLL_MAX_MS;
|
|
534
|
+
}
|
|
535
|
+
async initialize(_params) {
|
|
536
|
+
return {
|
|
537
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
538
|
+
agentCapabilities: {
|
|
539
|
+
// Sessions live in the vendor's cloud; re-attach is via the vendor
|
|
540
|
+
// id, not an ACP session/load replay (AIP-66 §5).
|
|
541
|
+
loadSession: false
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async authenticate() {
|
|
546
|
+
}
|
|
547
|
+
async newSession(params) {
|
|
548
|
+
if (params.mcpServers && params.mcpServers.length > 0) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
"claude code cloud: mcpServers is not supported for managed cloud agents (AIP-66 \xA74) \u2014 the vendor owns the harness"
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
await this.#validateRepo(params.cwd);
|
|
554
|
+
const id = crypto.randomUUID();
|
|
555
|
+
this.#sessions.set(id, {
|
|
556
|
+
id,
|
|
557
|
+
cwd: params.cwd,
|
|
558
|
+
vendorSessionId: null,
|
|
559
|
+
vendorUrl: null,
|
|
560
|
+
pollCursor: null,
|
|
561
|
+
seenEventIds: /* @__PURE__ */ new Set(),
|
|
562
|
+
promptInFlight: false
|
|
563
|
+
});
|
|
564
|
+
return { sessionId: id };
|
|
565
|
+
}
|
|
566
|
+
async prompt(params) {
|
|
567
|
+
const session = this.#sessions.get(params.sessionId);
|
|
568
|
+
if (!session) throw new Error(`unknown session ${params.sessionId}`);
|
|
569
|
+
if (session.promptInFlight) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
"claude code cloud: a turn is already in flight \u2014 session/cancel is unsupported (AIP-66 \xA76)"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
session.promptInFlight = true;
|
|
575
|
+
try {
|
|
576
|
+
const text = promptText(params);
|
|
577
|
+
if (!session.vendorSessionId) {
|
|
578
|
+
const started = await this.#cli.startCloudSession(text, {
|
|
579
|
+
cwd: session.cwd
|
|
580
|
+
});
|
|
581
|
+
session.vendorSessionId = started.sessionId;
|
|
582
|
+
session.vendorUrl = started.url;
|
|
583
|
+
await this.#conn.sessionUpdate({
|
|
584
|
+
sessionId: session.id,
|
|
585
|
+
update: {
|
|
586
|
+
sessionUpdate: "agent_message_chunk",
|
|
587
|
+
content: {
|
|
588
|
+
type: "text",
|
|
589
|
+
text: `[claude-code-cloud] ${started.title ? `${started.title} \u2014 ` : ""}${started.url}`
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
} else {
|
|
594
|
+
await this.#cli.sendPrompt(session.vendorSessionId, text, {
|
|
595
|
+
cwd: session.cwd
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return await this.#observeTurn(session);
|
|
599
|
+
} finally {
|
|
600
|
+
session.promptInFlight = false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Read events until a NEW `result` event ends the turn (AIP-66 §6). */
|
|
604
|
+
async #observeTurn(session) {
|
|
605
|
+
const vendorId = session.vendorSessionId;
|
|
606
|
+
if (!vendorId) throw new Error("claude code cloud: no vendor session");
|
|
607
|
+
let delay = this.#pollInitialMs;
|
|
608
|
+
let failures = 0;
|
|
609
|
+
for (; ; ) {
|
|
610
|
+
let page;
|
|
611
|
+
try {
|
|
612
|
+
page = await this.#api.listEvents(
|
|
613
|
+
vendorId,
|
|
614
|
+
session.pollCursor ? { cursor: session.pollCursor } : {}
|
|
615
|
+
);
|
|
616
|
+
} catch (err) {
|
|
617
|
+
if (!isTransientListEventsError(err) || failures + 1 >= MAX_LIST_EVENTS_FAILURES) {
|
|
618
|
+
throw err;
|
|
619
|
+
}
|
|
620
|
+
failures += 1;
|
|
621
|
+
await this.#sleep(delay);
|
|
622
|
+
delay = Math.min(delay * 2, this.#pollMaxMs);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
failures = 0;
|
|
626
|
+
if (page.resume_cursor) session.pollCursor = page.resume_cursor;
|
|
627
|
+
const fresh = (page.data ?? []).filter((event) => !session.seenEventIds.has(event.event_id)).sort((a, b) => {
|
|
628
|
+
if (a.created_at !== b.created_at) {
|
|
629
|
+
return a.created_at < b.created_at ? -1 : 1;
|
|
630
|
+
}
|
|
631
|
+
return a.event_id < b.event_id ? -1 : 1;
|
|
632
|
+
});
|
|
633
|
+
let stopReason = null;
|
|
634
|
+
for (const event of fresh) {
|
|
635
|
+
rememberSeenEventId(session, event.event_id);
|
|
636
|
+
if (event.event_type === "result") {
|
|
637
|
+
if (resultStopReason(event.payload) === "refusal") {
|
|
638
|
+
const errText = errorResultText(event.payload);
|
|
639
|
+
if (errText) {
|
|
640
|
+
await this.#send(session.id, {
|
|
641
|
+
sessionUpdate: "agent_message_chunk",
|
|
642
|
+
content: { type: "text", text: errText }
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
for (const update of resultEventUpdates(event.payload)) {
|
|
647
|
+
await this.#send(session.id, update);
|
|
648
|
+
}
|
|
649
|
+
stopReason = resultStopReason(event.payload);
|
|
650
|
+
} else if (event.event_type === "user") {
|
|
651
|
+
for (const update of sdkMessageToUpdates(
|
|
652
|
+
event.payload
|
|
653
|
+
)) {
|
|
654
|
+
if (update.sessionUpdate === "agent_message_chunk") continue;
|
|
655
|
+
await this.#send(session.id, update);
|
|
656
|
+
}
|
|
657
|
+
} else if (event.event_type === "assistant") {
|
|
658
|
+
for (const update of sdkMessageToUpdates(
|
|
659
|
+
event.payload
|
|
660
|
+
)) {
|
|
661
|
+
await this.#send(session.id, update);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (stopReason) return { stopReason };
|
|
666
|
+
await this.#sleep(delay);
|
|
667
|
+
delay = Math.min(delay * 2, this.#pollMaxMs);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async #send(sessionId, update) {
|
|
671
|
+
await this.#conn.sessionUpdate({ sessionId, update });
|
|
672
|
+
}
|
|
673
|
+
/** Unsupported: the vendor exposes no interrupt verb (AIP-66 §6). Throwing
|
|
674
|
+
* reports it — the in-flight poll loop is NOT aborted (we cannot stop the
|
|
675
|
+
* cloud turn; lying "cancelled" would violate §6's end-on-vendor-event
|
|
676
|
+
* rule). */
|
|
677
|
+
async cancel(_params) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
"claude code cloud: session/cancel is unsupported \u2014 the vendor exposes no interrupt verb for cloud sessions (AIP-66 \xA76)"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
/** No runtime-configurable options (no model menu — the vendor picks the
|
|
683
|
+
* model). Empty set so a host that tries to set one doesn't error. */
|
|
684
|
+
async setSessionConfigOption(_params) {
|
|
685
|
+
return { configOptions: [] };
|
|
686
|
+
}
|
|
687
|
+
/** No agent-specific modes; accept and ignore. */
|
|
688
|
+
async setSessionMode(_params) {
|
|
689
|
+
return {};
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
function runAcpOverStdio(deps = {}) {
|
|
693
|
+
const toClient = Writable.toWeb(process.stdout);
|
|
694
|
+
const fromClient = Readable.toWeb(
|
|
695
|
+
process.stdin
|
|
696
|
+
);
|
|
697
|
+
const stream = ndJsonStream(toClient, fromClient);
|
|
698
|
+
return new AgentSideConnection(
|
|
699
|
+
(conn) => new ClaudeCloudAcpAgent(conn, deps),
|
|
700
|
+
stream
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
function main() {
|
|
704
|
+
runAcpOverStdio();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export { CLAUDE_CODE_GATEWAY_ENV_UNSET, ClaudeCloudAcpAgent, MAX_LIST_EVENTS_FAILURES, POLL_INITIAL_MS, POLL_MAX_MS, SEEN_EVENT_IDS_LIMIT, SessionsApi, cloudChildEnv, defaultCloudCliRunner, defaultGitRunner, envCredentialSource, isTransientListEventsError, keychainCredentialSource, main, normalizeSessionId, parseAuthStatus, parseCloudStartOutput, preflightAuth, resultEventUpdates, resultStopReason, runAcpOverStdio, sendPrompt, startCloudSession, toCliSessionId, validateRepoSource };
|
|
708
|
+
//# sourceMappingURL=chunk-KA4G4O2X.mjs.map
|
|
709
|
+
//# sourceMappingURL=chunk-KA4G4O2X.mjs.map
|