@agentproto/adapter-claude-code-cloud 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,855 @@
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, mkdir, writeFile, rename } from 'fs/promises';
8
+ import { homedir } from 'os';
9
+ import { join, dirname } 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 eventSequence(event) {
75
+ const raw = event.sequence_num;
76
+ if (typeof raw !== "string" || raw.trim() === "") return null;
77
+ const n = Number(raw);
78
+ return Number.isFinite(n) ? n : null;
79
+ }
80
+ function normalizeSessionId(id) {
81
+ const trimmed = id.trim();
82
+ if (trimmed.startsWith("session_")) return `cse_${trimmed.slice("session_".length)}`;
83
+ if (trimmed.startsWith("cse_")) return trimmed;
84
+ throw new Error(
85
+ `claude code cloud: unrecognized session id ${JSON.stringify(id)} \u2014 expected session_<id> or cse_<id>`
86
+ );
87
+ }
88
+ function toCliSessionId(id) {
89
+ const normalized = normalizeSessionId(id);
90
+ return `session_${normalized.slice("cse_".length)}`;
91
+ }
92
+ var UNAUTHORIZED_HINT = "run `claude auth login`";
93
+ var SessionsApi = class {
94
+ #credentials;
95
+ #fetch;
96
+ #baseUrl;
97
+ #credential = null;
98
+ constructor(options) {
99
+ this.#credentials = options.credentials;
100
+ this.#fetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
101
+ this.#baseUrl = (options.baseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
102
+ }
103
+ /** GET /v1/code/sessions/{id} */
104
+ async getSession(id) {
105
+ return this.#request(
106
+ "GET",
107
+ `/v1/code/sessions/${normalizeSessionId(id)}`
108
+ );
109
+ }
110
+ /**
111
+ * GET /v1/code/sessions/{id}/events — newest first. With no `cursor` this
112
+ * returns the NEWEST page; pass the page's `resume_cursor` (the oldest
113
+ * event's `sequence_num`) as `cursor` to fetch the next OLDER page.
114
+ */
115
+ async listEvents(id, opts = {}) {
116
+ const params = new URLSearchParams();
117
+ if (opts.cursor) params.set("cursor", opts.cursor);
118
+ if (opts.limit !== void 0) params.set("limit", String(opts.limit));
119
+ const qs = params.toString();
120
+ return this.#request(
121
+ "GET",
122
+ `/v1/code/sessions/${normalizeSessionId(id)}/events${qs ? `?${qs}` : ""}`
123
+ );
124
+ }
125
+ /** GET /v1/code/sessions?limit=N */
126
+ async listSessions(opts = {}) {
127
+ const qs = opts.limit !== void 0 ? `?limit=${opts.limit}` : "";
128
+ return this.#request(
129
+ "GET",
130
+ `/v1/code/sessions${qs}`
131
+ );
132
+ }
133
+ async #credentialNow() {
134
+ if (!this.#credential) this.#credential = await this.#credentials.get();
135
+ return this.#credential;
136
+ }
137
+ async #request(method, path, retriedAuth = false) {
138
+ const credential = await this.#credentialNow();
139
+ const headers = {
140
+ authorization: `Bearer ${credential.accessToken}`,
141
+ "anthropic-version": "2023-06-01",
142
+ "content-type": "application/json",
143
+ ...credential.orgUuid ? { "x-organization-uuid": credential.orgUuid } : {}
144
+ };
145
+ const res = await this.#fetch(`${this.#baseUrl}${path}`, { method, headers });
146
+ if (res.status === 401 && !retriedAuth) {
147
+ this.#credential = null;
148
+ return this.#request(method, path, true);
149
+ }
150
+ if (res.status === 401) {
151
+ throw new Error(`claude code cloud: unauthorized (401) \u2014 ${UNAUTHORIZED_HINT}`);
152
+ }
153
+ if (!res.ok) {
154
+ throw new Error(
155
+ `claude code cloud: ${method} ${path} failed: HTTP ${res.status}`
156
+ );
157
+ }
158
+ return await res.json();
159
+ }
160
+ };
161
+
162
+ // src/cli.ts
163
+ var CLAUDE_CODE_CLOUD_TOGGLES = [
164
+ "CLAUDE_CODE_USE_BEDROCK",
165
+ "CLAUDE_CODE_USE_VERTEX",
166
+ "CLAUDE_CODE_USE_FOUNDRY",
167
+ "CLAUDE_CODE_USE_ANTHROPIC_AWS",
168
+ "CLAUDE_CODE_USE_MANTLE",
169
+ "CLAUDE_CODE_USE_GATEWAY"
170
+ ];
171
+ var CLAUDE_CODE_GATEWAY_ENV_UNSET = [
172
+ ...ANTHROPIC_CORE_SCRUB_ENV,
173
+ ...CLAUDE_CODE_CLOUD_TOGGLES
174
+ ];
175
+ function cloudChildEnv(base = process.env) {
176
+ const out = {};
177
+ const drop = new Set(CLAUDE_CODE_GATEWAY_ENV_UNSET);
178
+ for (const [key, value] of Object.entries(base)) {
179
+ if (value === void 0 || drop.has(key)) continue;
180
+ out[key] = value;
181
+ }
182
+ return out;
183
+ }
184
+ var DEFAULT_RUN_TIMEOUT_MS = 6e4;
185
+ function defaultCloudCliRunner() {
186
+ return {
187
+ run(cmd, args, opts) {
188
+ return new Promise((resolve, reject) => {
189
+ const child = spawn(cmd, args, {
190
+ cwd: opts.cwd,
191
+ env: opts.env,
192
+ stdio: [opts.stdin === "ignore" ? "ignore" : "inherit", "pipe", "pipe"]
193
+ });
194
+ let stdout = "";
195
+ let stderr = "";
196
+ const timer = setTimeout(() => {
197
+ child.kill("SIGKILL");
198
+ reject(
199
+ new Error(
200
+ `claude code cloud: \`${cmd} ${args.join(" ")}\` timed out after ${opts.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS}ms`
201
+ )
202
+ );
203
+ }, opts.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS);
204
+ timer.unref?.();
205
+ child.stdout?.on("data", (d) => stdout += d.toString());
206
+ child.stderr?.on("data", (d) => stderr += d.toString());
207
+ child.on("error", (err) => {
208
+ clearTimeout(timer);
209
+ reject(err);
210
+ });
211
+ child.on("close", (code) => {
212
+ clearTimeout(timer);
213
+ resolve({ code: code ?? -1, stdout, stderr });
214
+ });
215
+ });
216
+ },
217
+ runPty(cmd, args, opts) {
218
+ let resolveResult;
219
+ let rejectResult;
220
+ const result = new Promise((res, rej) => {
221
+ resolveResult = res;
222
+ rejectResult = rej;
223
+ });
224
+ let proc = null;
225
+ const pendingWrites = [];
226
+ void import('node-pty').then((mod) => {
227
+ const pty = mod.default ?? mod;
228
+ if (typeof pty.spawn !== "function") throw new Error("node-pty has no spawn");
229
+ const p = pty.spawn(cmd, args, {
230
+ name: "xterm-256color",
231
+ cols: 120,
232
+ rows: 40,
233
+ cwd: opts.cwd,
234
+ env: opts.env
235
+ });
236
+ proc = p;
237
+ for (const data of pendingWrites) p.write(data);
238
+ pendingWrites.length = 0;
239
+ let output = "";
240
+ let answeredTrustPrompt = false;
241
+ p.onData((chunk) => {
242
+ output += chunk;
243
+ opts.onData?.(chunk);
244
+ if (!answeredTrustPrompt && /Yes, I trust this folder/.test(output)) {
245
+ answeredTrustPrompt = true;
246
+ p.write("\x1B[B\r");
247
+ }
248
+ });
249
+ const timer = setTimeout(() => {
250
+ try {
251
+ p.kill("SIGKILL");
252
+ } catch {
253
+ }
254
+ rejectResult(
255
+ new Error(
256
+ `claude code cloud: \`${cmd} ${args.join(" ")}\` timed out after ${opts.timeoutMs ?? 12e4}ms under the PTY`
257
+ )
258
+ );
259
+ }, opts.timeoutMs ?? 12e4);
260
+ timer.unref?.();
261
+ p.onExit(({ exitCode }) => {
262
+ clearTimeout(timer);
263
+ resolveResult({ code: exitCode, output });
264
+ });
265
+ }).catch((err) => {
266
+ rejectResult(
267
+ new Error(
268
+ `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`
269
+ )
270
+ );
271
+ });
272
+ return {
273
+ write: (data) => {
274
+ if (proc) proc.write(data);
275
+ else pendingWrites.push(data);
276
+ },
277
+ result
278
+ };
279
+ }
280
+ };
281
+ }
282
+ function parseAuthStatus(stdout) {
283
+ const trimmed = stdout.trim();
284
+ const start = trimmed.indexOf("{");
285
+ const end = trimmed.lastIndexOf("}");
286
+ if (start === -1 || end === -1 || end < start) {
287
+ throw new Error(
288
+ "claude code cloud: could not parse `claude auth status` output \u2014 run `claude auth login`"
289
+ );
290
+ }
291
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
292
+ return { loggedIn: parsed.loggedIn === true, authMethod: parsed.authMethod };
293
+ }
294
+ async function preflightAuth(runner, opts) {
295
+ let result;
296
+ try {
297
+ result = await runner.run("claude", ["auth", "status"], {
298
+ cwd: opts.cwd,
299
+ env: opts.env,
300
+ timeoutMs: 15e3,
301
+ stdin: "ignore"
302
+ });
303
+ } catch (err) {
304
+ throw new Error(
305
+ `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)})`
306
+ );
307
+ }
308
+ if (result.code !== 0 && !result.stdout.trim()) {
309
+ throw new Error(
310
+ "claude code cloud: `claude auth status` failed \u2014 the claude CLI is not usable; run `claude auth login`"
311
+ );
312
+ }
313
+ const status = parseAuthStatus(result.stdout);
314
+ if (!status.loggedIn) {
315
+ throw new Error(
316
+ "claude code cloud: not logged in \u2014 cloud sessions need a claude.ai sign-in; run `claude auth login`"
317
+ );
318
+ }
319
+ if (status.authMethod === "api_key") {
320
+ throw new Error(
321
+ "claude code cloud: authenticated by API key \u2014 cloud sessions need a claude.ai sign-in; run `claude auth login`"
322
+ );
323
+ }
324
+ return status;
325
+ }
326
+ function parseCloudStartOutput(output) {
327
+ let sessionId;
328
+ let url;
329
+ let title;
330
+ const resume = output.match(/Resume with:\s*claude\s+--teleport\s+(session_[A-Za-z0-9_-]+)/);
331
+ if (resume?.[1]) sessionId = resume[1];
332
+ const view = output.match(/View:\s*(https?:\/\/\S+)/);
333
+ if (view?.[1]) {
334
+ url = view[1];
335
+ if (!sessionId) {
336
+ const fromUrl = url.match(/\/session_([A-Za-z0-9_-]+)/);
337
+ if (fromUrl?.[1]) sessionId = `session_${fromUrl[1]}`;
338
+ }
339
+ }
340
+ const created = output.match(/Created cloud session:\s*(.+)/);
341
+ if (created?.[1]) title = created[1].trim();
342
+ if (!sessionId) return null;
343
+ return {
344
+ sessionId,
345
+ url: url ?? `https://claude.ai/code/${sessionId}`,
346
+ ...title ? { title } : {}
347
+ };
348
+ }
349
+ async function startCloudSession(prompt, opts) {
350
+ const runner = opts.runner ?? defaultCloudCliRunner();
351
+ const env = cloudChildEnv(opts.env ?? process.env);
352
+ if (!opts.skipPreflight) await preflightAuth(runner, { cwd: opts.cwd, env });
353
+ const pty = runner.runPty("claude", ["--cloud", prompt], {
354
+ cwd: opts.cwd,
355
+ env,
356
+ timeoutMs: opts.timeoutMs ?? 12e4
357
+ });
358
+ const { code, output } = await pty.result;
359
+ if (code !== 0) {
360
+ throw new Error(
361
+ `claude code cloud: \`claude --cloud\` exited ${code}: ${output.trim().split("\n").slice(-5).join("\n").trim()}`
362
+ );
363
+ }
364
+ const started = parseCloudStartOutput(output);
365
+ if (!started) {
366
+ throw new Error(
367
+ "claude code cloud: could not parse a cloud session id from `claude --cloud` output"
368
+ );
369
+ }
370
+ return started;
371
+ }
372
+ async function sendPrompt(sessionId, text, opts) {
373
+ const runner = opts.runner ?? defaultCloudCliRunner();
374
+ const env = cloudChildEnv(opts.env ?? process.env);
375
+ if (!opts.skipPreflight) await preflightAuth(runner, { cwd: opts.cwd, env });
376
+ const result = await runner.run(
377
+ "claude",
378
+ ["-p", text, "--cloud", toCliSessionId(sessionId)],
379
+ {
380
+ cwd: opts.cwd,
381
+ env,
382
+ stdin: "ignore",
383
+ timeoutMs: opts.timeoutMs ?? 6e4
384
+ }
385
+ );
386
+ if (result.code !== 0) {
387
+ throw new Error(
388
+ `claude code cloud: \`claude -p --cloud\` exited ${result.code}: ${(result.stderr || result.stdout).trim().split("\n").slice(-5).join("\n").trim()}`
389
+ );
390
+ }
391
+ return { stdout: result.stdout };
392
+ }
393
+ var CLOUD_SESSION_META_FILE_ENV = "AGENTPROTO_CLOUD_SESSION_META_FILE";
394
+ var CLOUD_VENDOR_SLUG = "anthropic";
395
+ async function readExisting(path) {
396
+ try {
397
+ const parsed = JSON.parse(await readFile(path, "utf8"));
398
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
399
+ const p = parsed;
400
+ if (typeof p["vendor"] !== "string" || p["vendor"].length === 0) return void 0;
401
+ if (typeof p["sessionId"] !== "string" || p["sessionId"].length === 0) return void 0;
402
+ return {
403
+ vendor: p["vendor"],
404
+ sessionId: p["sessionId"],
405
+ ...typeof p["url"] === "string" && p["url"].length > 0 ? { url: p["url"] } : {}
406
+ };
407
+ } catch {
408
+ return void 0;
409
+ }
410
+ }
411
+ async function writeCloudSessionMeta(meta, opts = {}) {
412
+ const env = opts.env ?? process.env;
413
+ const path = env[CLOUD_SESSION_META_FILE_ENV];
414
+ if (!path) return null;
415
+ try {
416
+ const existing = await readExisting(path);
417
+ const url = meta.url ?? (existing?.sessionId === meta.sessionId ? existing.url : void 0);
418
+ const payload = JSON.stringify({
419
+ vendor: meta.vendor,
420
+ sessionId: meta.sessionId,
421
+ ...url !== void 0 ? { url } : {}
422
+ });
423
+ await mkdir(dirname(path), { recursive: true });
424
+ const tmp = `${path}.${process.pid}.tmp`;
425
+ await writeFile(tmp, payload, "utf8");
426
+ await rename(tmp, path);
427
+ return path;
428
+ } catch (err) {
429
+ process.stderr.write(
430
+ `[claude-code-cloud] could not write the cloud session meta file (${CLOUD_SESSION_META_FILE_ENV}): ${err instanceof Error ? err.message : String(err)}
431
+ `
432
+ );
433
+ return null;
434
+ }
435
+ }
436
+ var execFileAsync2 = promisify(execFile);
437
+ var defaultGitRunner = async (args, cwd) => {
438
+ try {
439
+ const { stdout, stderr } = await execFileAsync2("git", args, { cwd });
440
+ return { code: 0, stdout, stderr };
441
+ } catch (err) {
442
+ const e = err;
443
+ return {
444
+ code: typeof e.code === "number" ? e.code : 1,
445
+ stdout: e.stdout ?? "",
446
+ stderr: e.stderr ?? ""
447
+ };
448
+ }
449
+ };
450
+ async function validateRepoSource(cwd, git = defaultGitRunner) {
451
+ const inside = await git(["rev-parse", "--is-inside-work-tree"], cwd);
452
+ if (inside.code !== 0 || inside.stdout.trim() !== "true") {
453
+ throw new Error(
454
+ `claude code cloud: ${cwd} is not a git checkout \u2014 managed cloud agents run on a git remote and ref (AIP-66 \xA73)`
455
+ );
456
+ }
457
+ const status = await git(["status", "--porcelain"], cwd);
458
+ if (status.code !== 0) {
459
+ throw new Error(
460
+ `claude code cloud: could not read the working tree status in ${cwd} (${status.stderr.trim() || `git status exited ${status.code}`})`
461
+ );
462
+ }
463
+ if (status.stdout.trim().length > 0) {
464
+ throw new Error(
465
+ `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)`
466
+ );
467
+ }
468
+ const remote = await git(["remote", "get-url", "origin"], cwd);
469
+ if (remote.code !== 0 || !remote.stdout.trim()) {
470
+ throw new Error(
471
+ `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)`
472
+ );
473
+ }
474
+ const head = await git(["rev-parse", "HEAD"], cwd);
475
+ if (head.code !== 0 || !head.stdout.trim()) {
476
+ throw new Error(
477
+ `claude code cloud: could not resolve HEAD in ${cwd} (${head.stderr.trim() || `git rev-parse HEAD exited ${head.code}`})`
478
+ );
479
+ }
480
+ const contains = await git(["branch", "-r", "--contains", "HEAD"], cwd);
481
+ if (contains.code !== 0 || !contains.stdout.trim()) {
482
+ throw new Error(
483
+ `claude code cloud: HEAD in ${cwd} is not reachable from origin \u2014 push the commit before starting a cloud session (AIP-66 \xA73)`
484
+ );
485
+ }
486
+ }
487
+
488
+ // src/acp-server.ts
489
+ var POLL_INITIAL_MS = 2e3;
490
+ var POLL_MAX_MS = 1e4;
491
+ var MAX_LIST_EVENTS_FAILURES = 5;
492
+ var OBSERVE_PAGES_LIMIT = 20;
493
+ var NETWORK_ERROR_CODES = /^(ECONNABORTED|ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|ENOTFOUND|EPIPE|ETIMEDOUT|UND_ERR)/;
494
+ function isTransientListEventsError(err) {
495
+ if (!(err instanceof Error)) return false;
496
+ if (/\bHTTP 5\d\d\b/.test(err.message)) return true;
497
+ if (/fetch failed|network error|socket hang up/i.test(err.message)) return true;
498
+ let cause = err;
499
+ for (let depth = 0; depth < 4 && cause; depth += 1) {
500
+ const code = cause.code;
501
+ if (typeof code === "string" && NETWORK_ERROR_CODES.test(code)) return true;
502
+ cause = cause.cause;
503
+ }
504
+ return false;
505
+ }
506
+ function defaultSleep(ms) {
507
+ return new Promise((resolve) => {
508
+ const timer = setTimeout(resolve, ms);
509
+ timer.unref?.();
510
+ });
511
+ }
512
+ function defaultDeps() {
513
+ const runner = defaultCloudCliRunner();
514
+ const credentials = keychainCredentialSource();
515
+ return {
516
+ api: new SessionsApi({ credentials }),
517
+ cli: {
518
+ startCloudSession: (prompt, opts) => startCloudSession(prompt, {
519
+ cwd: opts.cwd,
520
+ runner,
521
+ env: process.env
522
+ }),
523
+ sendPrompt: (id, text, opts) => sendPrompt(id, text, {
524
+ cwd: opts.cwd,
525
+ runner,
526
+ env: process.env
527
+ })
528
+ },
529
+ validateRepo: (cwd) => validateRepoSource(cwd)
530
+ };
531
+ }
532
+ function resultEventUpdates(payload) {
533
+ const p = payload ?? {};
534
+ const hasUsage = p.usage != null || typeof p.total_cost_usd === "number";
535
+ if (!hasUsage) return [];
536
+ const normalized = {
537
+ ...p,
538
+ type: "result",
539
+ modelUsage: p.modelUsage ?? {},
540
+ usage: p.usage ?? { input_tokens: 0, output_tokens: 0 }
541
+ };
542
+ return sdkMessageToUpdates(normalized).filter(
543
+ (u) => u.sessionUpdate === "usage_update"
544
+ );
545
+ }
546
+ function resultStopReason(payload) {
547
+ const p = payload ?? {};
548
+ if (p.is_error === true) return "refusal";
549
+ if (p.subtype !== void 0 && p.subtype !== "success") return "refusal";
550
+ return "end_turn";
551
+ }
552
+ function errorResultText(payload) {
553
+ const p = payload ?? {};
554
+ if (typeof p.result === "string" && p.result.trim()) return p.result;
555
+ return null;
556
+ }
557
+ var ClaudeCloudAcpAgent = class {
558
+ #conn;
559
+ #api;
560
+ #cli;
561
+ #validateRepo;
562
+ #sleep;
563
+ #pollInitialMs;
564
+ #pollMaxMs;
565
+ #sessions = /* @__PURE__ */ new Map();
566
+ constructor(conn, deps = {}) {
567
+ const defaults = defaultDeps();
568
+ this.#conn = conn;
569
+ this.#api = deps.api ?? defaults.api;
570
+ this.#cli = deps.cli ?? defaults.cli;
571
+ this.#validateRepo = deps.validateRepo ?? defaults.validateRepo;
572
+ this.#sleep = deps.sleep ?? defaultSleep;
573
+ this.#pollInitialMs = deps.pollInitialMs ?? POLL_INITIAL_MS;
574
+ this.#pollMaxMs = deps.pollMaxMs ?? POLL_MAX_MS;
575
+ }
576
+ async initialize(_params) {
577
+ return {
578
+ protocolVersion: PROTOCOL_VERSION,
579
+ agentCapabilities: {
580
+ // Re-attach rides ACP `session/load` with the vendor session id the
581
+ // host persisted on `meta.cloud.sessionId` (AIP-66 §5) — that call
582
+ // ADOPTS the existing cloud session, it never starts a new one.
583
+ loadSession: true
584
+ }
585
+ };
586
+ }
587
+ async authenticate() {
588
+ }
589
+ async newSession(params) {
590
+ if (params.mcpServers && params.mcpServers.length > 0) {
591
+ throw new Error(
592
+ "claude code cloud: mcpServers is not supported for managed cloud agents (AIP-66 \xA74) \u2014 the vendor owns the harness"
593
+ );
594
+ }
595
+ await this.#validateRepo(params.cwd);
596
+ const id = crypto.randomUUID();
597
+ this.#sessions.set(id, {
598
+ id,
599
+ cwd: params.cwd,
600
+ vendorSessionId: null,
601
+ vendorUrl: null,
602
+ lastSeq: 0,
603
+ turnBaseline: 0,
604
+ promptInFlight: false
605
+ });
606
+ return { sessionId: id };
607
+ }
608
+ /**
609
+ * AIP-66 §5 re-attach: ADOPT the vendor session id the host persisted on
610
+ * `meta.cloud.sessionId` as this ACP session's vendor session. No vendor
611
+ * side effect happens here — the next `session/prompt` goes through
612
+ * `sendPrompt` (`claude -p … --cloud <id>`), never `startCloudSession`,
613
+ * so a resumed relay can never replace the vendor's session with a new
614
+ * one. The adopted id is also reported back to the host's meta file so a
615
+ * re-attach against a fresh config dir still carries the §5 mapping.
616
+ *
617
+ * Fails closed on an id the vendor could never own (e.g. the ACP uuid of
618
+ * a spawn whose vendor id was never learned): no session is started in
619
+ * its place.
620
+ */
621
+ async loadSession(params) {
622
+ if (params.mcpServers && params.mcpServers.length > 0) {
623
+ throw new Error(
624
+ "claude code cloud: mcpServers is not supported for managed cloud agents (AIP-66 \xA74) \u2014 the vendor owns the harness"
625
+ );
626
+ }
627
+ let vendorSessionId;
628
+ try {
629
+ vendorSessionId = toCliSessionId(params.sessionId);
630
+ } catch {
631
+ throw new Error(
632
+ `claude code cloud: session/load needs the vendor session id, got ${JSON.stringify(params.sessionId)} \u2014 expected session_<id> or cse_<id> (AIP-66 \xA75). That id was never learned onto meta.cloud.sessionId, so there is no vendor session to re-attach to \u2014 and this relay will not start a replacement.`
633
+ );
634
+ }
635
+ await this.#validateRepo(params.cwd);
636
+ const session = {
637
+ id: vendorSessionId,
638
+ cwd: params.cwd,
639
+ vendorSessionId,
640
+ vendorUrl: null,
641
+ lastSeq: 0,
642
+ turnBaseline: 0,
643
+ promptInFlight: false
644
+ };
645
+ this.#sessions.set(vendorSessionId, session);
646
+ await this.#seedHistory(session, vendorSessionId);
647
+ await writeCloudSessionMeta({ vendor: CLOUD_VENDOR_SLUG, sessionId: vendorSessionId });
648
+ return {};
649
+ }
650
+ /** Seed `lastSeq` from the vendor's newest events page. Best effort: a
651
+ * failed seed degrades to (at worst) an early end on an older `result` —
652
+ * it must never turn a re-attach into a failed load, which AIP-66 §5
653
+ * forbids replacing with a fresh start anyway.
654
+ *
655
+ * ONE newest-page GET with no cursor: it returns the newest page (newest
656
+ * first), so its max `sequence_num` is the history high-water. No
657
+ * backward paging is needed — `lastSeq` only has to be high enough that
658
+ * the first post-attach poll projects nothing already in the history. */
659
+ async #seedHistory(session, vendorSessionId) {
660
+ try {
661
+ session.lastSeq = await this.#highWaterSeq(vendorSessionId);
662
+ } catch (err) {
663
+ process.stderr.write(
664
+ `[claude-code-cloud] session/load: could not seed the vendor's existing event high-water (${err instanceof Error ? err.message : String(err)})
665
+ `
666
+ );
667
+ }
668
+ }
669
+ /** Max `sequence_num` on the session's newest events page (0 when the
670
+ * page is empty). One newest-page GET, no cursor. */
671
+ async #highWaterSeq(vendorSessionId) {
672
+ const res = await this.#api.listEvents(vendorSessionId, {});
673
+ let max = 0;
674
+ for (const event of res.data ?? []) {
675
+ const seq = eventSequence(event);
676
+ if (seq !== null && seq > max) max = seq;
677
+ }
678
+ return max;
679
+ }
680
+ /**
681
+ * Read every event newer than `lastSeq`, in ascending seq order, walking
682
+ * the API's cursor BACKWARD as far as needed. The API serves the NEWEST
683
+ * page with no cursor and pages toward OLDER events via `?cursor=<n>`
684
+ * (sessions-api.ts), so catching up across a >1-page gap means following
685
+ * `resume_cursor` and collecting the seqs above `lastSeq` until a page
686
+ * reaches already-projected history. Capped at {@link OBSERVE_PAGES_LIMIT}
687
+ * pages per call.
688
+ */
689
+ async #collectNewEvents(vendorSessionId, lastSeq) {
690
+ const collected = [];
691
+ let cursor;
692
+ for (let page = 0; page < OBSERVE_PAGES_LIMIT; page += 1) {
693
+ const res = await this.#api.listEvents(
694
+ vendorSessionId,
695
+ cursor ? { cursor } : {}
696
+ );
697
+ const events = res.data ?? [];
698
+ let oldest = null;
699
+ for (const event of events) {
700
+ const seq = eventSequence(event);
701
+ if (seq === null) continue;
702
+ if (oldest === null || seq < oldest) oldest = seq;
703
+ if (seq > lastSeq) collected.push({ seq, event });
704
+ }
705
+ if (events.length === 0 || oldest === null || oldest <= lastSeq + 1) break;
706
+ const next = res.resume_cursor ?? res.next_cursor;
707
+ if (!next || next === cursor) break;
708
+ cursor = next;
709
+ }
710
+ collected.sort((a, b) => a.seq - b.seq);
711
+ return collected;
712
+ }
713
+ async prompt(params) {
714
+ const session = this.#sessions.get(params.sessionId);
715
+ if (!session) throw new Error(`unknown session ${params.sessionId}`);
716
+ if (session.promptInFlight) {
717
+ throw new Error(
718
+ "claude code cloud: a turn is already in flight \u2014 session/cancel is unsupported (AIP-66 \xA76)"
719
+ );
720
+ }
721
+ session.promptInFlight = true;
722
+ try {
723
+ const text = promptText(params);
724
+ if (!session.vendorSessionId) {
725
+ const started = await this.#cli.startCloudSession(text, {
726
+ cwd: session.cwd
727
+ });
728
+ session.vendorSessionId = started.sessionId;
729
+ session.vendorUrl = started.url;
730
+ await writeCloudSessionMeta({
731
+ vendor: CLOUD_VENDOR_SLUG,
732
+ sessionId: started.sessionId,
733
+ url: started.url
734
+ });
735
+ await this.#conn.sessionUpdate({
736
+ sessionId: session.id,
737
+ update: {
738
+ sessionUpdate: "agent_message_chunk",
739
+ content: {
740
+ type: "text",
741
+ text: `[claude-code-cloud] ${started.title ? `${started.title} \u2014 ` : ""}${started.url}`
742
+ }
743
+ }
744
+ });
745
+ session.turnBaseline = 0;
746
+ } else {
747
+ session.turnBaseline = await this.#highWaterSeq(session.vendorSessionId);
748
+ await this.#cli.sendPrompt(session.vendorSessionId, text, {
749
+ cwd: session.cwd
750
+ });
751
+ }
752
+ return await this.#observeTurn(session);
753
+ } finally {
754
+ session.promptInFlight = false;
755
+ }
756
+ }
757
+ /** Read events until a NEW `result` event ends the turn (AIP-66 §6). */
758
+ async #observeTurn(session) {
759
+ const vendorId = session.vendorSessionId;
760
+ if (!vendorId) throw new Error("claude code cloud: no vendor session");
761
+ let delay = this.#pollInitialMs;
762
+ let failures = 0;
763
+ for (; ; ) {
764
+ let fresh;
765
+ try {
766
+ fresh = await this.#collectNewEvents(vendorId, session.lastSeq);
767
+ } catch (err) {
768
+ if (!isTransientListEventsError(err) || failures + 1 >= MAX_LIST_EVENTS_FAILURES) {
769
+ throw err;
770
+ }
771
+ failures += 1;
772
+ await this.#sleep(delay);
773
+ delay = Math.min(delay * 2, this.#pollMaxMs);
774
+ continue;
775
+ }
776
+ failures = 0;
777
+ let stopReason = null;
778
+ for (const { seq, event } of fresh) {
779
+ session.lastSeq = Math.max(session.lastSeq, seq);
780
+ if (event.event_type === "result") {
781
+ if (resultStopReason(event.payload) === "refusal") {
782
+ const errText = errorResultText(event.payload);
783
+ if (errText) {
784
+ await this.#send(session.id, {
785
+ sessionUpdate: "agent_message_chunk",
786
+ content: { type: "text", text: errText }
787
+ });
788
+ }
789
+ }
790
+ for (const update of resultEventUpdates(event.payload)) {
791
+ await this.#send(session.id, update);
792
+ }
793
+ if (seq > session.turnBaseline) {
794
+ stopReason = resultStopReason(event.payload);
795
+ }
796
+ } else if (event.event_type === "user") {
797
+ for (const update of sdkMessageToUpdates(
798
+ event.payload
799
+ )) {
800
+ if (update.sessionUpdate === "agent_message_chunk") continue;
801
+ await this.#send(session.id, update);
802
+ }
803
+ } else if (event.event_type === "assistant") {
804
+ for (const update of sdkMessageToUpdates(
805
+ event.payload
806
+ )) {
807
+ await this.#send(session.id, update);
808
+ }
809
+ }
810
+ }
811
+ if (stopReason) return { stopReason };
812
+ await this.#sleep(delay);
813
+ delay = Math.min(delay * 2, this.#pollMaxMs);
814
+ }
815
+ }
816
+ async #send(sessionId, update) {
817
+ await this.#conn.sessionUpdate({ sessionId, update });
818
+ }
819
+ /** Unsupported: the vendor exposes no interrupt verb (AIP-66 §6). Throwing
820
+ * reports it — the in-flight poll loop is NOT aborted (we cannot stop the
821
+ * cloud turn; lying "cancelled" would violate §6's end-on-vendor-event
822
+ * rule). */
823
+ async cancel(_params) {
824
+ throw new Error(
825
+ "claude code cloud: session/cancel is unsupported \u2014 the vendor exposes no interrupt verb for cloud sessions (AIP-66 \xA76)"
826
+ );
827
+ }
828
+ /** No runtime-configurable options (no model menu — the vendor picks the
829
+ * model). Empty set so a host that tries to set one doesn't error. */
830
+ async setSessionConfigOption(_params) {
831
+ return { configOptions: [] };
832
+ }
833
+ /** No agent-specific modes; accept and ignore. */
834
+ async setSessionMode(_params) {
835
+ return {};
836
+ }
837
+ };
838
+ function runAcpOverStdio(deps = {}) {
839
+ const toClient = Writable.toWeb(process.stdout);
840
+ const fromClient = Readable.toWeb(
841
+ process.stdin
842
+ );
843
+ const stream = ndJsonStream(toClient, fromClient);
844
+ return new AgentSideConnection(
845
+ (conn) => new ClaudeCloudAcpAgent(conn, deps),
846
+ stream
847
+ );
848
+ }
849
+ function main() {
850
+ runAcpOverStdio();
851
+ }
852
+
853
+ export { CLAUDE_CODE_GATEWAY_ENV_UNSET, CLOUD_SESSION_META_FILE_ENV, CLOUD_VENDOR_SLUG, ClaudeCloudAcpAgent, MAX_LIST_EVENTS_FAILURES, OBSERVE_PAGES_LIMIT, POLL_INITIAL_MS, POLL_MAX_MS, SessionsApi, cloudChildEnv, defaultCloudCliRunner, defaultGitRunner, envCredentialSource, isTransientListEventsError, keychainCredentialSource, main, normalizeSessionId, parseAuthStatus, parseCloudStartOutput, preflightAuth, resultEventUpdates, resultStopReason, runAcpOverStdio, sendPrompt, startCloudSession, toCliSessionId, validateRepoSource, writeCloudSessionMeta };
854
+ //# sourceMappingURL=chunk-RSQRK7X6.mjs.map
855
+ //# sourceMappingURL=chunk-RSQRK7X6.mjs.map