@agentproto/adapter-claude-code-cloud 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE-CODE-CLOUD.md +67 -1
- package/README.md +6 -2
- package/dist/acp-server.mjs +1 -1
- package/dist/{chunk-KA4G4O2X.mjs → chunk-LN5RJ5ZT.mjs} +299 -59
- package/dist/chunk-LN5RJ5ZT.mjs.map +1 -0
- package/dist/index.d.ts +175 -13
- package/dist/index.mjs +4 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-KA4G4O2X.mjs.map +0 -1
package/CLAUDE-CODE-CLOUD.md
CHANGED
|
@@ -12,7 +12,7 @@ relays the vendor's own client + sessions API onto ACP.
|
|
|
12
12
|
|
|
13
13
|
| Method | Behaviour |
|
|
14
14
|
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
-
| `initialize` | Returns `PROTOCOL_VERSION` + `agentCapabilities.loadSession=
|
|
15
|
+
| `initialize` | Returns `PROTOCOL_VERSION` + `agentCapabilities.loadSession=true` (`session/load` adopts the persisted vendor session id, AIP-66 §5). |
|
|
16
16
|
| `authenticate` | No-op. Auth is the user's claude.ai login inside the claude CLI (keychain); preflighted per turn (`claude auth status` — api_key auth and not-logged-in are refused). |
|
|
17
17
|
| `session/new` | Validates `cwd` per AIP-66 §3 BEFORE any vendor side effect: a git checkout, clean working tree, an `origin` remote, and a HEAD contained in some `origin/*` ref. Refuses `mcpServers` (AIP-66 §4). |
|
|
18
18
|
| `session/prompt` | First prompt → `claude --cloud "<prompt>"` under a PTY (start; answers the first-run folder-trust prompt), later prompts → `claude -p "<text>" --cloud <session id>` (send-only). Then polls `GET /v1/code/sessions/{id}/events` (2s, backing off to 10s) until a NEW `result` event arrives. |
|
|
@@ -41,6 +41,34 @@ a timer or on poll silence. Stop reason: `end_turn` for a success result,
|
|
|
41
41
|
failures (HTTP 5xx, network) are retried with the poll backoff — a read
|
|
42
42
|
failure never ends a turn — until 5 consecutive ones fail the prompt.
|
|
43
43
|
|
|
44
|
+
## Event paging & ordering (Appendix)
|
|
45
|
+
|
|
46
|
+
`GET /v1/code/sessions/{id}/events` does NOT page forward. Observed
|
|
47
|
+
2026-10-14 against the live API:
|
|
48
|
+
|
|
49
|
+
- **No `cursor` → the NEWEST page**, up to 50 events, newest first.
|
|
50
|
+
- **`resume_cursor`** (alias `next_cursor`, present on the first/no-cursor
|
|
51
|
+
page only) is the `sequence_num` of the OLDEST event on that page. Passing
|
|
52
|
+
it back as `?cursor=<n>` returns the next **OLDER** page — the cursor
|
|
53
|
+
pages **BACKWARD in time**, not forward. (Real example: the newest page
|
|
54
|
+
held seq 60..11 with `next_cursor` `"11"`; `?cursor=11` returned seq
|
|
55
|
+
10..1 with `resume_cursor` `"1"`.)
|
|
56
|
+
- **`sequence_num`** is a STRING of an integer, strictly increasing within a
|
|
57
|
+
session, and is the ordering key. Parse with `Number()`; skip an event
|
|
58
|
+
whose `sequence_num` is not finite. Every event also carries `event_id`,
|
|
59
|
+
`event_type`, `payload` (the stream-json message), `source`
|
|
60
|
+
(`"worker"`/`"client"`) and `created_at`.
|
|
61
|
+
|
|
62
|
+
The relay therefore tracks progress by `sequence_num`, not by cursor: it
|
|
63
|
+
keeps a per-session `lastSeq` (the highest seq projected or seeded), polls
|
|
64
|
+
the newest page each tick, walks `cursor` backward only as far as needed to
|
|
65
|
+
reach `lastSeq` (capped at 20 pages/tick), then projects the new events in
|
|
66
|
+
ASCENDING seq order. `session/load` seeds `lastSeq` from ONE newest-page GET
|
|
67
|
+
(its max seq is the history high-water — no backward paging needed). Before
|
|
68
|
+
each prompt the relay records the current high-water seq; only a `result`
|
|
69
|
+
with a seq ABOVE that baseline can end the turn, so a prior turn's result
|
|
70
|
+
never ends a new one.
|
|
71
|
+
|
|
44
72
|
## Usage telemetry
|
|
45
73
|
|
|
46
74
|
When the `result` event carries `total_cost_usd` and/or `usage`, it is
|
|
@@ -56,6 +84,44 @@ and kept for every later turn; the host is expected to persist it on the
|
|
|
56
84
|
session descriptor (`meta.cloud = { vendor, sessionId, url }`) and re-attach
|
|
57
85
|
rather than start a new vendor session after a restart.
|
|
58
86
|
|
|
87
|
+
## Re-attach (AIP-66 §5)
|
|
88
|
+
|
|
89
|
+
The vendor id only exists AFTER the first prompt reaches the vendor, so the
|
|
90
|
+
handoff from relay to host is a side file plus a poll, and the re-attach
|
|
91
|
+
itself rides the ordinary ACP `session/load` resume path:
|
|
92
|
+
|
|
93
|
+
- **Meta-file handoff.** At spawn the host sets
|
|
94
|
+
`AGENTPROTO_CLOUD_SESSION_META_FILE` to a path inside the session's
|
|
95
|
+
isolated config dir. Whenever the relay learns the vendor id — after
|
|
96
|
+
`claude --cloud` prints it, or when `session/load` adopts an id handed
|
|
97
|
+
back by the host — it writes exactly the three §5 fields
|
|
98
|
+
`{ vendor, sessionId, url }` there (`src/meta-file.ts`, atomic
|
|
99
|
+
temp-file + rename, best-effort: a failed write never sinks the turn,
|
|
100
|
+
and a re-attach preserves an existing `url` for the same id). The file
|
|
101
|
+
is the ONLY channel: no credential, keychain material, or bearer token
|
|
102
|
+
ever enters it, the descriptor's `meta.cloud`, logs or events (AIP-66
|
|
103
|
+
§8) — it is byte-scoped to those three fields.
|
|
104
|
+
- **The host's watch.** The runtime polls that file every 2s (timers
|
|
105
|
+
unref'd, NO fixed timeout) and stamps the learned mapping onto the
|
|
106
|
+
session's `meta.cloud` the moment it appears; the watch stops on learn
|
|
107
|
+
or when the session ends.
|
|
108
|
+
- **`session/load`.** A restart or re-attach hands the relay the vendor id
|
|
109
|
+
persisted on `meta.cloud.sessionId` through the standard ACP
|
|
110
|
+
`session/load`. The relay validates it against the repo source first
|
|
111
|
+
(an id that isn't `session_`/`cse_`, or whose repo no longer matches,
|
|
112
|
+
fails closed), ADOPTS it — never starting a replacement vendor session —
|
|
113
|
+
pre-marks the vendor's existing events so only the next turn's own
|
|
114
|
+
`result` can end it, and resumes through `sendPrompt`
|
|
115
|
+
(`claude -p --cloud <id>`).
|
|
116
|
+
- **The resume fallback.** The spawn-time watch dies with a daemon
|
|
117
|
+
restart, so every resume point first reads `meta.cloud.sessionId`; when
|
|
118
|
+
the descriptor never learned one, the runtime falls back to reading the
|
|
119
|
+
relay's meta file inside the row's own config dir
|
|
120
|
+
(`cloudSessionMetaFilePath`) and stamps the found mapping back onto the
|
|
121
|
+
descriptor before resuming. Only when both sources are empty does the
|
|
122
|
+
resume fall back to the ordinary ACP uuid, which fails closed at the
|
|
123
|
+
vendor.
|
|
124
|
+
|
|
59
125
|
## Repo source (AIP-66 §3)
|
|
60
126
|
|
|
61
127
|
The cloud runs the user's REPO from its git remote — never an uploaded
|
package/README.md
CHANGED
|
@@ -25,8 +25,12 @@ zero — AIP-66 §7).
|
|
|
25
25
|
from the `Claude Code-credentials` keychain entry (org uuid from
|
|
26
26
|
`~/.claude.json`), or `CLAUDE_CLOUD_OAUTH_TOKEN` when injected. A CLI
|
|
27
27
|
authenticated by API key, or not logged in, is refused with a pointer to
|
|
28
|
-
`claude auth login`.
|
|
29
|
-
|
|
28
|
+
`claude auth login`. When the token is expired (or within five minutes of
|
|
29
|
+
expiry) or the API answers 401, the adapter runs `claude -p "Reply OK"
|
|
30
|
+
--model haiku` once to let the CLI re-mint it, drops its cached credential,
|
|
31
|
+
and retries — a still-failing call reports `run \`claude auth login\``. The
|
|
32
|
+
token is only ever used as a request header — never logged or persisted
|
|
33
|
+
(AIP-66 §8).
|
|
30
34
|
- **A clean, pushed git checkout as the session cwd.** The cloud runs the repo
|
|
31
35
|
from its git remote, never an uploaded working tree, so `session/new`
|
|
32
36
|
refuses (AIP-66 §3) a non-git cwd, a dirty working tree, a missing `origin`
|
package/dist/acp-server.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { ClaudeCloudAcpAgent, MAX_LIST_EVENTS_FAILURES, POLL_INITIAL_MS, POLL_MAX_MS,
|
|
1
|
+
export { ClaudeCloudAcpAgent, MAX_LIST_EVENTS_FAILURES, OBSERVE_PAGES_LIMIT, POLL_INITIAL_MS, POLL_MAX_MS, isTransientListEventsError, main, promptText, resultEventUpdates, resultStopReason, runAcpOverStdio } from './chunk-LN5RJ5ZT.mjs';
|
|
2
2
|
//# sourceMappingURL=acp-server.mjs.map
|
|
3
3
|
//# sourceMappingURL=acp-server.mjs.map
|
|
@@ -3,10 +3,10 @@ import { PROTOCOL_VERSION, ndJsonStream, AgentSideConnection } from '@agentclien
|
|
|
3
3
|
import { sdkMessageToUpdates, promptText } from '@agentproto/adapter-claude-sdk';
|
|
4
4
|
export { promptText } from '@agentproto/adapter-claude-sdk';
|
|
5
5
|
import { execFile, spawn } from 'child_process';
|
|
6
|
+
import { homedir, tmpdir } from 'os';
|
|
6
7
|
import { ANTHROPIC_CORE_SCRUB_ENV } from '@agentproto/provider-presets';
|
|
7
|
-
import { readFile } from 'fs/promises';
|
|
8
|
-
import {
|
|
9
|
-
import { join } from 'path';
|
|
8
|
+
import { readFile, mkdir, writeFile, rename } from 'fs/promises';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
10
|
import { promisify } from 'util';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -15,6 +15,7 @@ import { promisify } from 'util';
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
var execFileAsync = promisify(execFile);
|
|
18
|
+
var EXPIRY_SKEW_MS = 5 * 6e4;
|
|
18
19
|
var KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
19
20
|
function keychainCredentialSource(deps = {}) {
|
|
20
21
|
const execFileImpl = deps.execFileImpl ?? (async (cmd, args) => {
|
|
@@ -52,8 +53,16 @@ function keychainCredentialSource(deps = {}) {
|
|
|
52
53
|
orgUuid = rc.oauthAccount?.organizationUuid;
|
|
53
54
|
} catch {
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
const expiresAt = keychain.claudeAiOauth?.expiresAt;
|
|
57
|
+
cached = {
|
|
58
|
+
accessToken,
|
|
59
|
+
...orgUuid ? { orgUuid } : {},
|
|
60
|
+
...typeof expiresAt === "number" ? { expiresAt } : {}
|
|
61
|
+
};
|
|
56
62
|
return cached;
|
|
63
|
+
},
|
|
64
|
+
invalidate() {
|
|
65
|
+
cached = null;
|
|
57
66
|
}
|
|
58
67
|
};
|
|
59
68
|
}
|
|
@@ -68,9 +77,17 @@ function envCredentialSource(env = process.env) {
|
|
|
68
77
|
}
|
|
69
78
|
const orgUuid = env.CLAUDE_CLOUD_ORG_UUID;
|
|
70
79
|
return { accessToken, ...orgUuid ? { orgUuid } : {} };
|
|
80
|
+
},
|
|
81
|
+
invalidate() {
|
|
71
82
|
}
|
|
72
83
|
};
|
|
73
84
|
}
|
|
85
|
+
function eventSequence(event) {
|
|
86
|
+
const raw = event.sequence_num;
|
|
87
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
88
|
+
const n = Number(raw);
|
|
89
|
+
return Number.isFinite(n) ? n : null;
|
|
90
|
+
}
|
|
74
91
|
function normalizeSessionId(id) {
|
|
75
92
|
const trimmed = id.trim();
|
|
76
93
|
if (trimmed.startsWith("session_")) return `cse_${trimmed.slice("session_".length)}`;
|
|
@@ -84,13 +101,18 @@ function toCliSessionId(id) {
|
|
|
84
101
|
return `session_${normalized.slice("cse_".length)}`;
|
|
85
102
|
}
|
|
86
103
|
var UNAUTHORIZED_HINT = "run `claude auth login`";
|
|
104
|
+
function isCredentialExpiring(credential, now = Date.now()) {
|
|
105
|
+
return credential.expiresAt !== void 0 && credential.expiresAt - now <= EXPIRY_SKEW_MS;
|
|
106
|
+
}
|
|
87
107
|
var SessionsApi = class {
|
|
88
108
|
#credentials;
|
|
109
|
+
#refreshLogin;
|
|
89
110
|
#fetch;
|
|
90
111
|
#baseUrl;
|
|
91
112
|
#credential = null;
|
|
92
113
|
constructor(options) {
|
|
93
114
|
this.#credentials = options.credentials;
|
|
115
|
+
this.#refreshLogin = options.refreshLogin;
|
|
94
116
|
this.#fetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
95
117
|
this.#baseUrl = (options.baseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
|
|
96
118
|
}
|
|
@@ -102,8 +124,9 @@ var SessionsApi = class {
|
|
|
102
124
|
);
|
|
103
125
|
}
|
|
104
126
|
/**
|
|
105
|
-
* GET /v1/code/sessions/{id}/events — newest first.
|
|
106
|
-
* page's `resume_cursor`
|
|
127
|
+
* GET /v1/code/sessions/{id}/events — newest first. With no `cursor` this
|
|
128
|
+
* returns the NEWEST page; pass the page's `resume_cursor` (the oldest
|
|
129
|
+
* event's `sequence_num`) as `cursor` to fetch the next OLDER page.
|
|
107
130
|
*/
|
|
108
131
|
async listEvents(id, opts = {}) {
|
|
109
132
|
const params = new URLSearchParams();
|
|
@@ -127,8 +150,26 @@ var SessionsApi = class {
|
|
|
127
150
|
if (!this.#credential) this.#credential = await this.#credentials.get();
|
|
128
151
|
return this.#credential;
|
|
129
152
|
}
|
|
130
|
-
|
|
153
|
+
/** Refresh the login (when injected), drop the source cache, and re-read.
|
|
154
|
+
* A refresh failure is surfaced to the caller — better a clear "run
|
|
155
|
+
* `claude auth login`" than a request with a known-stale token. */
|
|
156
|
+
async #refreshAndInvalidate() {
|
|
157
|
+
await this.#refreshLogin?.();
|
|
158
|
+
this.#credentials.invalidate();
|
|
159
|
+
this.#credential = null;
|
|
160
|
+
}
|
|
161
|
+
/** The credential for the next request: refresh first when it is expired
|
|
162
|
+
* or expiring. Without an injected refresh the check is a no-op. */
|
|
163
|
+
async #freshCredential() {
|
|
131
164
|
const credential = await this.#credentialNow();
|
|
165
|
+
if (this.#refreshLogin && isCredentialExpiring(credential)) {
|
|
166
|
+
await this.#refreshAndInvalidate();
|
|
167
|
+
return this.#credentialNow();
|
|
168
|
+
}
|
|
169
|
+
return credential;
|
|
170
|
+
}
|
|
171
|
+
async #request(method, path, retriedAuth = false) {
|
|
172
|
+
const credential = retriedAuth ? await this.#credentialNow() : await this.#freshCredential();
|
|
132
173
|
const headers = {
|
|
133
174
|
authorization: `Bearer ${credential.accessToken}`,
|
|
134
175
|
"anthropic-version": "2023-06-01",
|
|
@@ -137,7 +178,7 @@ var SessionsApi = class {
|
|
|
137
178
|
};
|
|
138
179
|
const res = await this.#fetch(`${this.#baseUrl}${path}`, { method, headers });
|
|
139
180
|
if (res.status === 401 && !retriedAuth) {
|
|
140
|
-
this.#
|
|
181
|
+
await this.#refreshAndInvalidate();
|
|
141
182
|
return this.#request(method, path, true);
|
|
142
183
|
}
|
|
143
184
|
if (res.status === 401) {
|
|
@@ -174,8 +215,18 @@ function cloudChildEnv(base = process.env) {
|
|
|
174
215
|
}
|
|
175
216
|
return out;
|
|
176
217
|
}
|
|
218
|
+
var ANSI_ESCAPE_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)|\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b./g;
|
|
219
|
+
function normalizePtyOutput(raw) {
|
|
220
|
+
return raw.replace(ANSI_ESCAPE_RE, " ").replace(/[^\S\n]+/g, " ").replace(/\n[ \t]*(?:\n[ \t]*)+/g, "\n").trim();
|
|
221
|
+
}
|
|
222
|
+
var TRUST_PROMPT_RE = /Yes,\s*I\s*trust\s*this\s*folder/i;
|
|
223
|
+
var PTY_TAIL_CHARS = 300;
|
|
224
|
+
function ptyOutputTail(raw, maxChars = PTY_TAIL_CHARS) {
|
|
225
|
+
const stripped = normalizePtyOutput(raw);
|
|
226
|
+
return stripped.length > maxChars ? stripped.slice(stripped.length - maxChars) : stripped;
|
|
227
|
+
}
|
|
177
228
|
var DEFAULT_RUN_TIMEOUT_MS = 6e4;
|
|
178
|
-
function defaultCloudCliRunner() {
|
|
229
|
+
function defaultCloudCliRunner(deps = {}) {
|
|
179
230
|
return {
|
|
180
231
|
run(cmd, args, opts) {
|
|
181
232
|
return new Promise((resolve, reject) => {
|
|
@@ -216,10 +267,8 @@ function defaultCloudCliRunner() {
|
|
|
216
267
|
});
|
|
217
268
|
let proc = null;
|
|
218
269
|
const pendingWrites = [];
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
if (typeof pty.spawn !== "function") throw new Error("node-pty has no spawn");
|
|
222
|
-
const p = pty.spawn(cmd, args, {
|
|
270
|
+
const attach = (spawnFn) => {
|
|
271
|
+
const p = spawnFn(cmd, args, {
|
|
223
272
|
name: "xterm-256color",
|
|
224
273
|
cols: 120,
|
|
225
274
|
rows: 40,
|
|
@@ -234,7 +283,7 @@ function defaultCloudCliRunner() {
|
|
|
234
283
|
p.onData((chunk) => {
|
|
235
284
|
output += chunk;
|
|
236
285
|
opts.onData?.(chunk);
|
|
237
|
-
if (!answeredTrustPrompt &&
|
|
286
|
+
if (!answeredTrustPrompt && TRUST_PROMPT_RE.test(normalizePtyOutput(output))) {
|
|
238
287
|
answeredTrustPrompt = true;
|
|
239
288
|
p.write("\x1B[B\r");
|
|
240
289
|
}
|
|
@@ -246,7 +295,7 @@ function defaultCloudCliRunner() {
|
|
|
246
295
|
}
|
|
247
296
|
rejectResult(
|
|
248
297
|
new Error(
|
|
249
|
-
`claude code cloud: \`${cmd} ${args.join(" ")}\` timed out after ${opts.timeoutMs ?? 12e4}ms under the PTY`
|
|
298
|
+
`claude code cloud: \`${cmd} ${args.join(" ")}\` timed out after ${opts.timeoutMs ?? 12e4}ms under the PTY; last output: ${ptyOutputTail(output)}`
|
|
250
299
|
)
|
|
251
300
|
);
|
|
252
301
|
}, opts.timeoutMs ?? 12e4);
|
|
@@ -255,13 +304,26 @@ function defaultCloudCliRunner() {
|
|
|
255
304
|
clearTimeout(timer);
|
|
256
305
|
resolveResult({ code: exitCode, output });
|
|
257
306
|
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
307
|
+
};
|
|
308
|
+
if (deps.spawnPty) {
|
|
309
|
+
try {
|
|
310
|
+
attach(deps.spawnPty);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
rejectResult(err);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
void import('node-pty').then((mod) => {
|
|
316
|
+
const pty = mod.default ?? mod;
|
|
317
|
+
if (typeof pty.spawn !== "function") throw new Error("node-pty has no spawn");
|
|
318
|
+
attach(pty.spawn);
|
|
319
|
+
}).catch((err) => {
|
|
320
|
+
rejectResult(
|
|
321
|
+
new Error(
|
|
322
|
+
`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`
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
265
327
|
return {
|
|
266
328
|
write: (data) => {
|
|
267
329
|
if (proc) proc.write(data);
|
|
@@ -272,6 +334,43 @@ function defaultCloudCliRunner() {
|
|
|
272
334
|
}
|
|
273
335
|
};
|
|
274
336
|
}
|
|
337
|
+
function createRefreshLogin(opts = {}) {
|
|
338
|
+
let inFlight = null;
|
|
339
|
+
return () => {
|
|
340
|
+
if (inFlight) return inFlight;
|
|
341
|
+
const run = (async () => {
|
|
342
|
+
const runner = opts.runner ?? defaultCloudCliRunner();
|
|
343
|
+
const env = cloudChildEnv(opts.env ?? process.env);
|
|
344
|
+
let result;
|
|
345
|
+
try {
|
|
346
|
+
result = await runner.run(
|
|
347
|
+
"claude",
|
|
348
|
+
["-p", "Reply OK", "--model", "haiku"],
|
|
349
|
+
{
|
|
350
|
+
cwd: opts.cwd ?? tmpdir(),
|
|
351
|
+
env,
|
|
352
|
+
stdin: "ignore",
|
|
353
|
+
timeoutMs: opts.timeoutMs ?? 6e4
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`claude code cloud: could not refresh the claude.ai login \u2014 is the claude CLI installed and on PATH? (${err instanceof Error ? err.message : String(err)})`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
if (result.code !== 0) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`claude code cloud: could not refresh the claude.ai login (exit ${result.code}) \u2014 run \`claude auth login\``
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
})();
|
|
367
|
+
const wrapped = run.finally(() => {
|
|
368
|
+
if (inFlight === wrapped) inFlight = null;
|
|
369
|
+
});
|
|
370
|
+
inFlight = wrapped;
|
|
371
|
+
return wrapped;
|
|
372
|
+
};
|
|
373
|
+
}
|
|
275
374
|
function parseAuthStatus(stdout) {
|
|
276
375
|
const trimmed = stdout.trim();
|
|
277
376
|
const start = trimmed.indexOf("{");
|
|
@@ -317,12 +416,13 @@ async function preflightAuth(runner, opts) {
|
|
|
317
416
|
return status;
|
|
318
417
|
}
|
|
319
418
|
function parseCloudStartOutput(output) {
|
|
419
|
+
const transcript = normalizePtyOutput(output);
|
|
320
420
|
let sessionId;
|
|
321
421
|
let url;
|
|
322
422
|
let title;
|
|
323
|
-
const resume =
|
|
423
|
+
const resume = transcript.match(/Resume with:\s*claude\s+--teleport\s+(session_[A-Za-z0-9_-]+)/);
|
|
324
424
|
if (resume?.[1]) sessionId = resume[1];
|
|
325
|
-
const view =
|
|
425
|
+
const view = transcript.match(/View:\s*(https?:\/\/\S+)/);
|
|
326
426
|
if (view?.[1]) {
|
|
327
427
|
url = view[1];
|
|
328
428
|
if (!sessionId) {
|
|
@@ -330,7 +430,7 @@ function parseCloudStartOutput(output) {
|
|
|
330
430
|
if (fromUrl?.[1]) sessionId = `session_${fromUrl[1]}`;
|
|
331
431
|
}
|
|
332
432
|
}
|
|
333
|
-
const created =
|
|
433
|
+
const created = transcript.match(/Created cloud session:\s*(.+)/);
|
|
334
434
|
if (created?.[1]) title = created[1].trim();
|
|
335
435
|
if (!sessionId) return null;
|
|
336
436
|
return {
|
|
@@ -383,6 +483,49 @@ async function sendPrompt(sessionId, text, opts) {
|
|
|
383
483
|
}
|
|
384
484
|
return { stdout: result.stdout };
|
|
385
485
|
}
|
|
486
|
+
var CLOUD_SESSION_META_FILE_ENV = "AGENTPROTO_CLOUD_SESSION_META_FILE";
|
|
487
|
+
var CLOUD_VENDOR_SLUG = "anthropic";
|
|
488
|
+
async function readExisting(path) {
|
|
489
|
+
try {
|
|
490
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
491
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
492
|
+
const p = parsed;
|
|
493
|
+
if (typeof p["vendor"] !== "string" || p["vendor"].length === 0) return void 0;
|
|
494
|
+
if (typeof p["sessionId"] !== "string" || p["sessionId"].length === 0) return void 0;
|
|
495
|
+
return {
|
|
496
|
+
vendor: p["vendor"],
|
|
497
|
+
sessionId: p["sessionId"],
|
|
498
|
+
...typeof p["url"] === "string" && p["url"].length > 0 ? { url: p["url"] } : {}
|
|
499
|
+
};
|
|
500
|
+
} catch {
|
|
501
|
+
return void 0;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function writeCloudSessionMeta(meta, opts = {}) {
|
|
505
|
+
const env = opts.env ?? process.env;
|
|
506
|
+
const path = env[CLOUD_SESSION_META_FILE_ENV];
|
|
507
|
+
if (!path) return null;
|
|
508
|
+
try {
|
|
509
|
+
const existing = await readExisting(path);
|
|
510
|
+
const url = meta.url ?? (existing?.sessionId === meta.sessionId ? existing.url : void 0);
|
|
511
|
+
const payload = JSON.stringify({
|
|
512
|
+
vendor: meta.vendor,
|
|
513
|
+
sessionId: meta.sessionId,
|
|
514
|
+
...url !== void 0 ? { url } : {}
|
|
515
|
+
});
|
|
516
|
+
await mkdir(dirname(path), { recursive: true });
|
|
517
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
518
|
+
await writeFile(tmp, payload, "utf8");
|
|
519
|
+
await rename(tmp, path);
|
|
520
|
+
return path;
|
|
521
|
+
} catch (err) {
|
|
522
|
+
process.stderr.write(
|
|
523
|
+
`[claude-code-cloud] could not write the cloud session meta file (${CLOUD_SESSION_META_FILE_ENV}): ${err instanceof Error ? err.message : String(err)}
|
|
524
|
+
`
|
|
525
|
+
);
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
386
529
|
var execFileAsync2 = promisify(execFile);
|
|
387
530
|
var defaultGitRunner = async (args, cwd) => {
|
|
388
531
|
try {
|
|
@@ -439,6 +582,7 @@ async function validateRepoSource(cwd, git = defaultGitRunner) {
|
|
|
439
582
|
var POLL_INITIAL_MS = 2e3;
|
|
440
583
|
var POLL_MAX_MS = 1e4;
|
|
441
584
|
var MAX_LIST_EVENTS_FAILURES = 5;
|
|
585
|
+
var OBSERVE_PAGES_LIMIT = 20;
|
|
442
586
|
var NETWORK_ERROR_CODES = /^(ECONNABORTED|ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|ENOTFOUND|EPIPE|ETIMEDOUT|UND_ERR)/;
|
|
443
587
|
function isTransientListEventsError(err) {
|
|
444
588
|
if (!(err instanceof Error)) return false;
|
|
@@ -452,16 +596,6 @@ function isTransientListEventsError(err) {
|
|
|
452
596
|
}
|
|
453
597
|
return false;
|
|
454
598
|
}
|
|
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
599
|
function defaultSleep(ms) {
|
|
466
600
|
return new Promise((resolve) => {
|
|
467
601
|
const timer = setTimeout(resolve, ms);
|
|
@@ -471,8 +605,9 @@ function defaultSleep(ms) {
|
|
|
471
605
|
function defaultDeps() {
|
|
472
606
|
const runner = defaultCloudCliRunner();
|
|
473
607
|
const credentials = keychainCredentialSource();
|
|
608
|
+
const refreshLogin = createRefreshLogin({ runner, env: process.env });
|
|
474
609
|
return {
|
|
475
|
-
api: new SessionsApi({ credentials }),
|
|
610
|
+
api: new SessionsApi({ credentials, refreshLogin }),
|
|
476
611
|
cli: {
|
|
477
612
|
startCloudSession: (prompt, opts) => startCloudSession(prompt, {
|
|
478
613
|
cwd: opts.cwd,
|
|
@@ -536,9 +671,10 @@ var ClaudeCloudAcpAgent = class {
|
|
|
536
671
|
return {
|
|
537
672
|
protocolVersion: PROTOCOL_VERSION,
|
|
538
673
|
agentCapabilities: {
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
|
|
674
|
+
// Re-attach rides ACP `session/load` with the vendor session id the
|
|
675
|
+
// host persisted on `meta.cloud.sessionId` (AIP-66 §5) — that call
|
|
676
|
+
// ADOPTS the existing cloud session, it never starts a new one.
|
|
677
|
+
loadSession: true
|
|
542
678
|
}
|
|
543
679
|
};
|
|
544
680
|
}
|
|
@@ -557,12 +693,117 @@ var ClaudeCloudAcpAgent = class {
|
|
|
557
693
|
cwd: params.cwd,
|
|
558
694
|
vendorSessionId: null,
|
|
559
695
|
vendorUrl: null,
|
|
560
|
-
|
|
561
|
-
|
|
696
|
+
lastSeq: 0,
|
|
697
|
+
turnBaseline: 0,
|
|
562
698
|
promptInFlight: false
|
|
563
699
|
});
|
|
564
700
|
return { sessionId: id };
|
|
565
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* AIP-66 §5 re-attach: ADOPT the vendor session id the host persisted on
|
|
704
|
+
* `meta.cloud.sessionId` as this ACP session's vendor session. No vendor
|
|
705
|
+
* side effect happens here — the next `session/prompt` goes through
|
|
706
|
+
* `sendPrompt` (`claude -p … --cloud <id>`), never `startCloudSession`,
|
|
707
|
+
* so a resumed relay can never replace the vendor's session with a new
|
|
708
|
+
* one. The adopted id is also reported back to the host's meta file so a
|
|
709
|
+
* re-attach against a fresh config dir still carries the §5 mapping.
|
|
710
|
+
*
|
|
711
|
+
* Fails closed on an id the vendor could never own (e.g. the ACP uuid of
|
|
712
|
+
* a spawn whose vendor id was never learned): no session is started in
|
|
713
|
+
* its place.
|
|
714
|
+
*/
|
|
715
|
+
async loadSession(params) {
|
|
716
|
+
if (params.mcpServers && params.mcpServers.length > 0) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
"claude code cloud: mcpServers is not supported for managed cloud agents (AIP-66 \xA74) \u2014 the vendor owns the harness"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
let vendorSessionId;
|
|
722
|
+
try {
|
|
723
|
+
vendorSessionId = toCliSessionId(params.sessionId);
|
|
724
|
+
} catch {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`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.`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
await this.#validateRepo(params.cwd);
|
|
730
|
+
const session = {
|
|
731
|
+
id: vendorSessionId,
|
|
732
|
+
cwd: params.cwd,
|
|
733
|
+
vendorSessionId,
|
|
734
|
+
vendorUrl: null,
|
|
735
|
+
lastSeq: 0,
|
|
736
|
+
turnBaseline: 0,
|
|
737
|
+
promptInFlight: false
|
|
738
|
+
};
|
|
739
|
+
this.#sessions.set(vendorSessionId, session);
|
|
740
|
+
await this.#seedHistory(session, vendorSessionId);
|
|
741
|
+
await writeCloudSessionMeta({ vendor: CLOUD_VENDOR_SLUG, sessionId: vendorSessionId });
|
|
742
|
+
return {};
|
|
743
|
+
}
|
|
744
|
+
/** Seed `lastSeq` from the vendor's newest events page. Best effort: a
|
|
745
|
+
* failed seed degrades to (at worst) an early end on an older `result` —
|
|
746
|
+
* it must never turn a re-attach into a failed load, which AIP-66 §5
|
|
747
|
+
* forbids replacing with a fresh start anyway.
|
|
748
|
+
*
|
|
749
|
+
* ONE newest-page GET with no cursor: it returns the newest page (newest
|
|
750
|
+
* first), so its max `sequence_num` is the history high-water. No
|
|
751
|
+
* backward paging is needed — `lastSeq` only has to be high enough that
|
|
752
|
+
* the first post-attach poll projects nothing already in the history. */
|
|
753
|
+
async #seedHistory(session, vendorSessionId) {
|
|
754
|
+
try {
|
|
755
|
+
session.lastSeq = await this.#highWaterSeq(vendorSessionId);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
process.stderr.write(
|
|
758
|
+
`[claude-code-cloud] session/load: could not seed the vendor's existing event high-water (${err instanceof Error ? err.message : String(err)})
|
|
759
|
+
`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/** Max `sequence_num` on the session's newest events page (0 when the
|
|
764
|
+
* page is empty). One newest-page GET, no cursor. */
|
|
765
|
+
async #highWaterSeq(vendorSessionId) {
|
|
766
|
+
const res = await this.#api.listEvents(vendorSessionId, {});
|
|
767
|
+
let max = 0;
|
|
768
|
+
for (const event of res.data ?? []) {
|
|
769
|
+
const seq = eventSequence(event);
|
|
770
|
+
if (seq !== null && seq > max) max = seq;
|
|
771
|
+
}
|
|
772
|
+
return max;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Read every event newer than `lastSeq`, in ascending seq order, walking
|
|
776
|
+
* the API's cursor BACKWARD as far as needed. The API serves the NEWEST
|
|
777
|
+
* page with no cursor and pages toward OLDER events via `?cursor=<n>`
|
|
778
|
+
* (sessions-api.ts), so catching up across a >1-page gap means following
|
|
779
|
+
* `resume_cursor` and collecting the seqs above `lastSeq` until a page
|
|
780
|
+
* reaches already-projected history. Capped at {@link OBSERVE_PAGES_LIMIT}
|
|
781
|
+
* pages per call.
|
|
782
|
+
*/
|
|
783
|
+
async #collectNewEvents(vendorSessionId, lastSeq) {
|
|
784
|
+
const collected = [];
|
|
785
|
+
let cursor;
|
|
786
|
+
for (let page = 0; page < OBSERVE_PAGES_LIMIT; page += 1) {
|
|
787
|
+
const res = await this.#api.listEvents(
|
|
788
|
+
vendorSessionId,
|
|
789
|
+
cursor ? { cursor } : {}
|
|
790
|
+
);
|
|
791
|
+
const events = res.data ?? [];
|
|
792
|
+
let oldest = null;
|
|
793
|
+
for (const event of events) {
|
|
794
|
+
const seq = eventSequence(event);
|
|
795
|
+
if (seq === null) continue;
|
|
796
|
+
if (oldest === null || seq < oldest) oldest = seq;
|
|
797
|
+
if (seq > lastSeq) collected.push({ seq, event });
|
|
798
|
+
}
|
|
799
|
+
if (events.length === 0 || oldest === null || oldest <= lastSeq + 1) break;
|
|
800
|
+
const next = res.resume_cursor ?? res.next_cursor;
|
|
801
|
+
if (!next || next === cursor) break;
|
|
802
|
+
cursor = next;
|
|
803
|
+
}
|
|
804
|
+
collected.sort((a, b) => a.seq - b.seq);
|
|
805
|
+
return collected;
|
|
806
|
+
}
|
|
566
807
|
async prompt(params) {
|
|
567
808
|
const session = this.#sessions.get(params.sessionId);
|
|
568
809
|
if (!session) throw new Error(`unknown session ${params.sessionId}`);
|
|
@@ -580,6 +821,11 @@ var ClaudeCloudAcpAgent = class {
|
|
|
580
821
|
});
|
|
581
822
|
session.vendorSessionId = started.sessionId;
|
|
582
823
|
session.vendorUrl = started.url;
|
|
824
|
+
await writeCloudSessionMeta({
|
|
825
|
+
vendor: CLOUD_VENDOR_SLUG,
|
|
826
|
+
sessionId: started.sessionId,
|
|
827
|
+
url: started.url
|
|
828
|
+
});
|
|
583
829
|
await this.#conn.sessionUpdate({
|
|
584
830
|
sessionId: session.id,
|
|
585
831
|
update: {
|
|
@@ -590,7 +836,9 @@ var ClaudeCloudAcpAgent = class {
|
|
|
590
836
|
}
|
|
591
837
|
}
|
|
592
838
|
});
|
|
839
|
+
session.turnBaseline = 0;
|
|
593
840
|
} else {
|
|
841
|
+
session.turnBaseline = await this.#highWaterSeq(session.vendorSessionId);
|
|
594
842
|
await this.#cli.sendPrompt(session.vendorSessionId, text, {
|
|
595
843
|
cwd: session.cwd
|
|
596
844
|
});
|
|
@@ -607,12 +855,9 @@ var ClaudeCloudAcpAgent = class {
|
|
|
607
855
|
let delay = this.#pollInitialMs;
|
|
608
856
|
let failures = 0;
|
|
609
857
|
for (; ; ) {
|
|
610
|
-
let
|
|
858
|
+
let fresh;
|
|
611
859
|
try {
|
|
612
|
-
|
|
613
|
-
vendorId,
|
|
614
|
-
session.pollCursor ? { cursor: session.pollCursor } : {}
|
|
615
|
-
);
|
|
860
|
+
fresh = await this.#collectNewEvents(vendorId, session.lastSeq);
|
|
616
861
|
} catch (err) {
|
|
617
862
|
if (!isTransientListEventsError(err) || failures + 1 >= MAX_LIST_EVENTS_FAILURES) {
|
|
618
863
|
throw err;
|
|
@@ -623,16 +868,9 @@ var ClaudeCloudAcpAgent = class {
|
|
|
623
868
|
continue;
|
|
624
869
|
}
|
|
625
870
|
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
871
|
let stopReason = null;
|
|
634
|
-
for (const event of fresh) {
|
|
635
|
-
|
|
872
|
+
for (const { seq, event } of fresh) {
|
|
873
|
+
session.lastSeq = Math.max(session.lastSeq, seq);
|
|
636
874
|
if (event.event_type === "result") {
|
|
637
875
|
if (resultStopReason(event.payload) === "refusal") {
|
|
638
876
|
const errText = errorResultText(event.payload);
|
|
@@ -646,7 +884,9 @@ var ClaudeCloudAcpAgent = class {
|
|
|
646
884
|
for (const update of resultEventUpdates(event.payload)) {
|
|
647
885
|
await this.#send(session.id, update);
|
|
648
886
|
}
|
|
649
|
-
|
|
887
|
+
if (seq > session.turnBaseline) {
|
|
888
|
+
stopReason = resultStopReason(event.payload);
|
|
889
|
+
}
|
|
650
890
|
} else if (event.event_type === "user") {
|
|
651
891
|
for (const update of sdkMessageToUpdates(
|
|
652
892
|
event.payload
|
|
@@ -704,6 +944,6 @@ function main() {
|
|
|
704
944
|
runAcpOverStdio();
|
|
705
945
|
}
|
|
706
946
|
|
|
707
|
-
export { CLAUDE_CODE_GATEWAY_ENV_UNSET, ClaudeCloudAcpAgent, MAX_LIST_EVENTS_FAILURES, POLL_INITIAL_MS, POLL_MAX_MS,
|
|
708
|
-
//# sourceMappingURL=chunk-
|
|
709
|
-
//# sourceMappingURL=chunk-
|
|
947
|
+
export { CLAUDE_CODE_GATEWAY_ENV_UNSET, CLOUD_SESSION_META_FILE_ENV, CLOUD_VENDOR_SLUG, ClaudeCloudAcpAgent, EXPIRY_SKEW_MS, MAX_LIST_EVENTS_FAILURES, OBSERVE_PAGES_LIMIT, POLL_INITIAL_MS, POLL_MAX_MS, SessionsApi, cloudChildEnv, createRefreshLogin, defaultCloudCliRunner, defaultGitRunner, envCredentialSource, isCredentialExpiring, isTransientListEventsError, keychainCredentialSource, main, normalizePtyOutput, normalizeSessionId, parseAuthStatus, parseCloudStartOutput, preflightAuth, ptyOutputTail, resultEventUpdates, resultStopReason, runAcpOverStdio, sendPrompt, startCloudSession, toCliSessionId, validateRepoSource, writeCloudSessionMeta };
|
|
948
|
+
//# sourceMappingURL=chunk-LN5RJ5ZT.mjs.map
|
|
949
|
+
//# sourceMappingURL=chunk-LN5RJ5ZT.mjs.map
|