@dynamicagents/plugins 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -79
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +53 -0
- package/dist/claude-code/cards.js +272 -0
- package/dist/claude-code/config.d.ts +73 -31
- package/dist/claude-code/config.js +0 -2
- package/dist/claude-code/credentials.d.ts +21 -9
- package/dist/claude-code/credentials.js +43 -9
- package/dist/claude-code/egress.d.ts +15 -14
- package/dist/claude-code/egress.js +51 -14
- package/dist/claude-code/events.d.ts +153 -9
- package/dist/claude-code/events.js +289 -58
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +24 -80
- package/dist/claude-code/index.js +19 -234
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +342 -0
- package/dist/claude-code/run.d.ts +283 -92
- package/dist/claude-code/run.js +620 -114
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +285 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +19 -0
- package/dist/computer/context.js +222 -0
- package/dist/computer/file-lock.d.ts +5 -0
- package/dist/computer/file-lock.js +40 -0
- package/dist/computer/gate.d.ts +12 -12
- package/dist/computer/gate.js +12 -12
- package/dist/computer/index.d.ts +57 -255
- package/dist/computer/index.js +98 -1039
- package/dist/computer/paths.d.ts +47 -22
- package/dist/computer/paths.js +64 -49
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +27 -38
- package/dist/computer/render.js +32 -65
- package/dist/computer/tools-exec.d.ts +3 -0
- package/dist/computer/tools-exec.js +268 -0
- package/dist/computer/tools-file.d.ts +12 -0
- package/dist/computer/tools-file.js +64 -0
- package/dist/computer/tools-grep.d.ts +3 -0
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +35 -6
- package/dist/repo/checkout.js +64 -6
- package/dist/repo/context.d.ts +135 -0
- package/dist/repo/context.js +582 -0
- package/dist/repo/index.d.ts +79 -57
- package/dist/repo/index.js +118 -1112
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.d.ts +4 -0
- package/dist/repo/tools-clone.js +192 -0
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/tools-pr.js +196 -0
- package/dist/repo/tools-review.d.ts +12 -0
- package/dist/repo/tools-review.js +393 -0
- package/dist/repo/tools-worktree.d.ts +4 -0
- package/dist/repo/tools-worktree.js +305 -0
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +33 -20
- package/dist/{computer → workspace}/advisory.js +32 -7
- package/dist/workspace/ca-trust.d.ts +95 -0
- package/dist/workspace/ca-trust.js +312 -0
- package/dist/workspace/container-deps.d.ts +54 -0
- package/dist/workspace/container-deps.js +224 -0
- package/dist/workspace/container-fault.d.ts +38 -0
- package/dist/workspace/container-fault.js +73 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/workspace/git-host.d.ts +79 -0
- package/dist/workspace/git-host.js +226 -0
- package/dist/workspace/git-identity.d.ts +57 -0
- package/dist/workspace/git-identity.js +147 -0
- package/dist/workspace/index.d.ts +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/workspace/install-job.d.ts +121 -0
- package/dist/workspace/install-job.js +866 -0
- package/dist/{computer → workspace}/install.d.ts +15 -19
- package/dist/{computer → workspace}/install.js +8 -11
- package/dist/workspace/object.d.ts +502 -0
- package/dist/workspace/object.js +1427 -0
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/workspace/shell.d.ts +45 -0
- package/dist/workspace/shell.js +80 -0
- package/dist/workspace/sync.d.ts +111 -0
- package/dist/workspace/sync.js +124 -0
- package/dist/workspace/wake.d.ts +19 -0
- package/dist/workspace/wake.js +10 -0
- package/package.json +28 -42
- package/dist/arc-agi/analysis.d.ts +0 -336
- package/dist/arc-agi/analysis.js +0 -829
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -25
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -42
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -27
- package/dist/arc-agi/main-agent.js +0 -65
- package/dist/arc-agi/recipe.d.ts +0 -51
- package/dist/arc-agi/recipe.js +0 -100
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -40
- package/dist/arc-agi/soul.js +0 -90
- package/dist/arc-agi/store.d.ts +0 -154
- package/dist/arc-agi/store.js +0 -216
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -576
- package/dist/arc-agi/types.d.ts +0 -212
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -137
- package/dist/computer/read.js +0 -203
- package/dist/recall/index.d.ts +0 -99
- package/dist/recall/index.js +0 -171
- package/dist/triage/index.d.ts +0 -355
- package/dist/triage/index.js +0 -302
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import { initCard, resultCard, thinkingCard, toolCallCard, toolResultCard } from "./cards.js";
|
|
2
|
+
/**
|
|
3
|
+
* How far a thinking line's arrival may be from its own `timestamp` for the
|
|
4
|
+
* block to be timed.
|
|
5
|
+
*
|
|
6
|
+
* Both ends of a block are timed by when they arrive, which is only the truth
|
|
7
|
+
* while the drain reads live. A drain that re-attached after a cut is handed
|
|
8
|
+
* what was printed meanwhile in one burst, and its arrival times are the
|
|
9
|
+
* replay's — so a block whose line arrived far from when the CLI stamped it
|
|
10
|
+
* reads "Thought", with no time. Locally a line arrives 5–31 ms after its
|
|
11
|
+
* stamp; a re-attach has been measured at about a second. A container clock
|
|
12
|
+
* this far from the Worker's degrades every block to no time, rather than to a
|
|
13
|
+
* wrong one.
|
|
14
|
+
*/
|
|
15
|
+
export const LIVE_SLACK_MS = 5_000;
|
|
1
16
|
const EMPTY_USAGE = {
|
|
2
17
|
input: 0,
|
|
3
18
|
output: 0,
|
|
@@ -27,29 +42,86 @@ function readUsage(raw) {
|
|
|
27
42
|
};
|
|
28
43
|
}
|
|
29
44
|
/**
|
|
30
|
-
*
|
|
45
|
+
* An `assistant` message as events, one per block, in the order it said them.
|
|
31
46
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
47
|
+
* Per block rather than flattened, because the blocks are different things to
|
|
48
|
+
* a reader: prose, the model's reasoning, and each tool it called. A message
|
|
49
|
+
* that only calls tools is as much progress as one that narrates.
|
|
35
50
|
*/
|
|
36
|
-
function readAssistant(message) {
|
|
51
|
+
function readAssistant(message, thought) {
|
|
52
|
+
const messageId = str(message.id);
|
|
37
53
|
const content = Array.isArray(message.content)
|
|
38
54
|
? message.content
|
|
39
55
|
: [];
|
|
40
|
-
const
|
|
41
|
-
const tools = [];
|
|
56
|
+
const events = [];
|
|
42
57
|
for (const block of content) {
|
|
43
58
|
if (!asRecord(block))
|
|
44
59
|
continue;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
if (block.type === "text") {
|
|
61
|
+
// Trimmed only to ask whether it is empty: indentation is a code block's
|
|
62
|
+
// and trailing spaces a hard break's, so the event keeps it verbatim.
|
|
63
|
+
const text = str(block.text);
|
|
64
|
+
if (text?.trim())
|
|
65
|
+
events.push({ kind: "text", text });
|
|
66
|
+
}
|
|
67
|
+
else if (block.type === "thinking") {
|
|
68
|
+
events.push({
|
|
69
|
+
kind: "thinking",
|
|
70
|
+
...(messageId ? { messageId } : {}),
|
|
71
|
+
...(thought === undefined ? {} : { durationMs: thought })
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else if (block.type === "tool_use") {
|
|
75
|
+
const id = str(block.id);
|
|
76
|
+
const name = str(block.name);
|
|
77
|
+
if (id && name)
|
|
78
|
+
events.push({ kind: "toolUse", id, name, input: block.input });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return events;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A `user` line's tool results. Anything else a user line carries — the
|
|
85
|
+
* prompt echoed back, a hook's note — is not a tool's answer and says nothing.
|
|
86
|
+
*/
|
|
87
|
+
function readToolResults(event, message) {
|
|
88
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
89
|
+
const results = content.filter((block) => asRecord(block)?.type === "tool_result");
|
|
90
|
+
const structured = results.length === 1 ? event.tool_use_result : undefined;
|
|
91
|
+
const events = [];
|
|
92
|
+
for (const block of results) {
|
|
93
|
+
const id = str(block.tool_use_id);
|
|
94
|
+
if (!id)
|
|
95
|
+
continue;
|
|
96
|
+
events.push({
|
|
97
|
+
kind: "toolResult",
|
|
98
|
+
id,
|
|
99
|
+
isError: block.is_error === true,
|
|
100
|
+
content: resultText(block.content),
|
|
101
|
+
...(structured === undefined || structured === null ? {} : { structured })
|
|
102
|
+
});
|
|
51
103
|
}
|
|
52
|
-
return
|
|
104
|
+
return events;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A `tool_result` block's content as text. It is a string, or a list of
|
|
108
|
+
* blocks of which only the text ones are readable here — an image a tool
|
|
109
|
+
* returned is named, not carried.
|
|
110
|
+
*/
|
|
111
|
+
function resultText(content) {
|
|
112
|
+
if (typeof content === "string")
|
|
113
|
+
return content;
|
|
114
|
+
if (!Array.isArray(content))
|
|
115
|
+
return "";
|
|
116
|
+
return content
|
|
117
|
+
.map((block) => {
|
|
118
|
+
const record = asRecord(block);
|
|
119
|
+
if (record?.type === "text")
|
|
120
|
+
return str(record.text) ?? "";
|
|
121
|
+
return record?.type ? `[${String(record.type)}]` : "";
|
|
122
|
+
})
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join("\n");
|
|
53
125
|
}
|
|
54
126
|
/**
|
|
55
127
|
* Read a `system`/`permission_denied` line into something worth reporting.
|
|
@@ -87,8 +159,13 @@ function readDenial(event) {
|
|
|
87
159
|
* the filter.
|
|
88
160
|
*
|
|
89
161
|
* `result` and `init` are never nested and are read unconditionally.
|
|
162
|
+
*
|
|
163
|
+
* `clock` is when this read arrived, and the pending thinking block's start
|
|
164
|
+
* from the previous read — see {@link ParsedStream.thinkingFrom}. Without it no
|
|
165
|
+
* block is timed.
|
|
90
166
|
*/
|
|
91
|
-
export function parseStream(buffer) {
|
|
167
|
+
export function parseStream(buffer, clock) {
|
|
168
|
+
let thinkingFrom = clock?.thinkingFrom;
|
|
92
169
|
const events = [];
|
|
93
170
|
let skipped = 0;
|
|
94
171
|
let nested = 0;
|
|
@@ -131,6 +208,7 @@ export function parseStream(buffer) {
|
|
|
131
208
|
nested++;
|
|
132
209
|
continue;
|
|
133
210
|
}
|
|
211
|
+
const at = readTime(event.timestamp);
|
|
134
212
|
switch (event.type) {
|
|
135
213
|
case "system": {
|
|
136
214
|
if (event.subtype === "init") {
|
|
@@ -140,10 +218,14 @@ export function parseStream(buffer) {
|
|
|
140
218
|
break;
|
|
141
219
|
}
|
|
142
220
|
const model = str(event.model);
|
|
221
|
+
const cwd = str(event.cwd);
|
|
222
|
+
const permissionMode = str(event.permissionMode);
|
|
143
223
|
events.push({
|
|
144
224
|
kind: "init",
|
|
145
225
|
sessionId,
|
|
146
|
-
...(model ? { model } : {})
|
|
226
|
+
...(model ? { model } : {}),
|
|
227
|
+
...(cwd ? { cwd } : {}),
|
|
228
|
+
...(permissionMode ? { permissionMode } : {})
|
|
147
229
|
});
|
|
148
230
|
break;
|
|
149
231
|
}
|
|
@@ -151,6 +233,11 @@ export function parseStream(buffer) {
|
|
|
151
233
|
events.push(readDenial(event));
|
|
152
234
|
break;
|
|
153
235
|
}
|
|
236
|
+
if (event.subtype === "thinking_tokens") {
|
|
237
|
+
if (clock)
|
|
238
|
+
thinkingFrom ??= clock.now;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
154
241
|
if (event.subtype === "api_retry") {
|
|
155
242
|
events.push({
|
|
156
243
|
kind: "retry",
|
|
@@ -176,29 +263,120 @@ export function parseStream(buffer) {
|
|
|
176
263
|
skip(trimmed);
|
|
177
264
|
break;
|
|
178
265
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
266
|
+
// Ended by its own line's arrival, and trusted only on a live read.
|
|
267
|
+
const thought = clock !== undefined &&
|
|
268
|
+
thinkingFrom !== undefined &&
|
|
269
|
+
at !== undefined &&
|
|
270
|
+
Math.abs(clock.now - at) <= LIVE_SLACK_MS
|
|
271
|
+
? clock.now - thinkingFrom
|
|
272
|
+
: undefined;
|
|
273
|
+
events.push(...readAssistant(message, thought));
|
|
183
274
|
break;
|
|
184
275
|
}
|
|
185
276
|
// `user` messages on this stream are tool *results* being fed back to the
|
|
186
|
-
// model, not anything a human said
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
277
|
+
// model, not anything a human said — each one completes the card its call
|
|
278
|
+
// opened.
|
|
279
|
+
case "user": {
|
|
280
|
+
const message = asRecord(event.message);
|
|
281
|
+
if (message)
|
|
282
|
+
events.push(...readToolResults(event, message));
|
|
191
283
|
break;
|
|
284
|
+
}
|
|
192
285
|
case "result": {
|
|
193
286
|
events.push({ kind: "result", result: readResult(event) });
|
|
194
287
|
break;
|
|
195
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* A top-level `type`, not a `system` subtype, so it is handled here and
|
|
291
|
+
* not in the subtype switch above.
|
|
292
|
+
*
|
|
293
|
+
* The placement is load-bearing rather than tidy: everything this switch
|
|
294
|
+
* does not name falls to `default`, which counts it as an unrecognised
|
|
295
|
+
* line and warns. A recognised shape parsed in the wrong place is
|
|
296
|
+
* therefore indistinguishable from a schema that moved — the drain reports
|
|
297
|
+
* a steady `skipped` on every session, and the reading it is dropping is
|
|
298
|
+
* the only free view of the subscription bucket anything here gets.
|
|
299
|
+
*/
|
|
300
|
+
case "rate_limit_event": {
|
|
301
|
+
const info = readRateLimit(event);
|
|
302
|
+
if (!info) {
|
|
303
|
+
skip(trimmed);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
events.push({ kind: "rateLimit", info });
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
196
309
|
default:
|
|
197
310
|
skip(trimmed);
|
|
198
311
|
}
|
|
312
|
+
if (at !== undefined)
|
|
313
|
+
thinkingFrom = undefined;
|
|
199
314
|
}
|
|
200
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
events,
|
|
317
|
+
carry,
|
|
318
|
+
skipped,
|
|
319
|
+
nested,
|
|
320
|
+
...(sample ? { sample } : {}),
|
|
321
|
+
...(thinkingFrom === undefined ? {} : { thinkingFrom })
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Read a `rate_limit_event` line, or nothing when it carries no status.
|
|
326
|
+
*
|
|
327
|
+
* `status` is the only required field: it is what decides whether anything is
|
|
328
|
+
* wrong, and a line without it says nothing this package can use — so it is
|
|
329
|
+
* skipped and sampled like any other unrecognised shape, rather than recorded as
|
|
330
|
+
* a bucket in an unknown state.
|
|
331
|
+
*/
|
|
332
|
+
function readRateLimit(event) {
|
|
333
|
+
const info = asRecord(event.rate_limit_info);
|
|
334
|
+
const status = info && str(info.status);
|
|
335
|
+
if (!info || !status)
|
|
336
|
+
return undefined;
|
|
337
|
+
const resetsAt = info.resetsAt;
|
|
338
|
+
return {
|
|
339
|
+
status,
|
|
340
|
+
// Finite is not enough. `1e308` is finite, and seconds past the `Date` range
|
|
341
|
+
// make `toISOString()` throw — inside `describe`, inside the drain's parse
|
|
342
|
+
// loop, which would take a whole session down over one malformed vendor
|
|
343
|
+
// field. The contract everywhere else here is to drop a bad line, not to
|
|
344
|
+
// raise, so the check is "does this name a moment" rather than "is this a
|
|
345
|
+
// number".
|
|
346
|
+
...(typeof resetsAt === "number" && isRealDate(resetsAt * 1000)
|
|
347
|
+
? { resetsAt }
|
|
348
|
+
: {}),
|
|
349
|
+
...(str(info.rateLimitType)
|
|
350
|
+
? { rateLimitType: str(info.rateLimitType) }
|
|
351
|
+
: {}),
|
|
352
|
+
...(str(info.overageStatus)
|
|
353
|
+
? { overageStatus: str(info.overageStatus) }
|
|
354
|
+
: {}),
|
|
355
|
+
...(str(info.overageDisabledReason)
|
|
356
|
+
? { overageDisabledReason: str(info.overageDisabledReason) }
|
|
357
|
+
: {})
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** A line's `timestamp`, in epoch milliseconds, or `undefined`. */
|
|
361
|
+
function readTime(value) {
|
|
362
|
+
const ms = typeof value === "string" ? Date.parse(value) : NaN;
|
|
363
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
201
364
|
}
|
|
365
|
+
/** Whether a millisecond value is one `Date` can actually represent. */
|
|
366
|
+
function isRealDate(ms) {
|
|
367
|
+
return Number.isFinite(ms) && !Number.isNaN(new Date(ms).getTime());
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The status a healthy bucket reports, and the only one ever observed.
|
|
371
|
+
*
|
|
372
|
+
* **Every other value is unclassified, not a refusal.** Nothing here knows what
|
|
373
|
+
* a non-`allowed` status means, and the one place that decides whether a
|
|
374
|
+
* credential is spent — `readRateLimitEvent` in this package's `credentials.ts`
|
|
375
|
+
* — recognises none of them, deliberately: retiring a working credential on a
|
|
376
|
+
* guess is the expensive half of that trade. What an unrecognised status earns
|
|
377
|
+
* is a note and a log, so somebody can name it.
|
|
378
|
+
*/
|
|
379
|
+
export const RATE_LIMIT_OK = "allowed";
|
|
202
380
|
/** Read a `result` line. Total by construction — a missing field reads as zero. */
|
|
203
381
|
function readResult(event) {
|
|
204
382
|
const apiErrorStatus = event.api_error_status;
|
|
@@ -208,6 +386,7 @@ function readResult(event) {
|
|
|
208
386
|
: 0;
|
|
209
387
|
const sessionId = str(event.session_id);
|
|
210
388
|
const spawned = stats ? num(stats.spawned) : undefined;
|
|
389
|
+
const errors = readErrors(event.errors);
|
|
211
390
|
return {
|
|
212
391
|
subtype: str(event.subtype) ?? "unknown",
|
|
213
392
|
isError: event.is_error === true,
|
|
@@ -219,29 +398,57 @@ function readResult(event) {
|
|
|
219
398
|
costUsd: num(event.total_cost_usd),
|
|
220
399
|
usage: readUsage(event.usage),
|
|
221
400
|
...(spawned === undefined ? {} : { subagentsSpawned: spawned }),
|
|
222
|
-
permissionDenials: denials
|
|
401
|
+
permissionDenials: denials,
|
|
402
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
403
|
+
...(event.structured_output === undefined ||
|
|
404
|
+
event.structured_output === null
|
|
405
|
+
? {}
|
|
406
|
+
: { structured: event.structured_output })
|
|
223
407
|
};
|
|
224
408
|
}
|
|
225
|
-
/**
|
|
226
|
-
|
|
409
|
+
/**
|
|
410
|
+
* A result line's `errors`, as sentences.
|
|
411
|
+
*
|
|
412
|
+
* **Anything that is not a string is stringified rather than dropped**, which is
|
|
413
|
+
* the whole of the care this needs: the field is a vendor's, an entry carrying a
|
|
414
|
+
* code and a message is a perfectly likely shape for it, and on the run this
|
|
415
|
+
* exists to explain — a resume with nowhere to resume from — it is the only
|
|
416
|
+
* account of what happened. Dropping an unexpected entry would report that run
|
|
417
|
+
* as having failed for no stated reason.
|
|
418
|
+
*/
|
|
419
|
+
function readErrors(raw) {
|
|
420
|
+
if (!Array.isArray(raw))
|
|
421
|
+
return [];
|
|
422
|
+
return raw
|
|
423
|
+
.map((entry) => (typeof entry === "string" ? entry : safeJson(entry)))
|
|
424
|
+
.filter((entry) => entry.length > 0);
|
|
425
|
+
}
|
|
426
|
+
/** `value` as JSON, or as whatever `String` makes of it when it has no JSON. */
|
|
427
|
+
function safeJson(value) {
|
|
428
|
+
try {
|
|
429
|
+
return JSON.stringify(value) ?? String(value);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return String(value);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
227
435
|
/**
|
|
228
436
|
* How much of an unrecognised line is kept for the logs.
|
|
229
437
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* whether a schema moved. The discriminating part of a stream-json line is its
|
|
233
|
-
* `type` and `subtype`, both of which are at the front.
|
|
438
|
+
* Read by a person deciding whether a schema moved, and the discriminating part
|
|
439
|
+
* of a stream-json line is its `type` and `subtype`, both at the front.
|
|
234
440
|
*/
|
|
235
441
|
const SAMPLE_MAX_CHARS = 200;
|
|
236
442
|
/**
|
|
237
|
-
* Turn events into progress notes the parent
|
|
443
|
+
* Turn events into progress notes for the parent's transcript.
|
|
238
444
|
*
|
|
239
445
|
* **`from` is the number of notes already emitted for this run, and the key is
|
|
240
446
|
* derived from it.** Not from the note's content, not from a clock, and not from
|
|
241
447
|
* the Claude Code `uuid` — from position alone.
|
|
242
448
|
*
|
|
243
|
-
* The reason is replay. A
|
|
244
|
-
*
|
|
449
|
+
* The reason is replay. A drain that dies mid-stream is resumed from the last
|
|
450
|
+
* stored sequence, whatever arrived after it is read again, and the same
|
|
451
|
+
* assistant turn is parsed twice. A
|
|
245
452
|
* content-derived key would make two identical notes collide *by luck* (and two
|
|
246
453
|
* genuinely repeated notes — "Running tests" twice — collide wrongly). A
|
|
247
454
|
* clock-derived key would never collide, so every replay would re-post
|
|
@@ -252,29 +459,30 @@ export function toProgress(events, from) {
|
|
|
252
459
|
const out = [];
|
|
253
460
|
let n = from;
|
|
254
461
|
for (const event of events) {
|
|
255
|
-
const
|
|
256
|
-
if (!
|
|
462
|
+
const note = describe(event);
|
|
463
|
+
if (!note)
|
|
257
464
|
continue;
|
|
258
|
-
out.push({ key: `claude:${n}`,
|
|
465
|
+
out.push({ key: `claude:${n}`, ...note });
|
|
259
466
|
n++;
|
|
260
467
|
}
|
|
261
468
|
return out;
|
|
262
469
|
}
|
|
263
|
-
/** One
|
|
470
|
+
/** One note, or nothing when the event is not worth one. */
|
|
264
471
|
function describe(event) {
|
|
265
472
|
switch (event.kind) {
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
473
|
+
// Never clipped. A plan or a report cut mid-sentence reads as complete.
|
|
474
|
+
case "text":
|
|
475
|
+
return { text: event.text };
|
|
476
|
+
// Tool activity is a card, and a card never reaches the thread — core's
|
|
477
|
+
// `transcribeNote` keeps it on the transcript — so a session's every Bash,
|
|
478
|
+
// Read and Edit is shown without one message each in front of the person.
|
|
479
|
+
// `./cards.ts` holds how each tool reads.
|
|
480
|
+
case "toolUse":
|
|
481
|
+
return toolCallCard(event.id, event.name, event.input);
|
|
482
|
+
case "toolResult":
|
|
483
|
+
return toolResultCard(event);
|
|
484
|
+
case "thinking":
|
|
485
|
+
return thinkingCard(event.durationMs, event.messageId);
|
|
278
486
|
case "denied":
|
|
279
487
|
// The one progress note that is more useful than the session's own
|
|
280
488
|
// account of itself. A refused tool call is invisible in the transcript —
|
|
@@ -282,19 +490,42 @@ function describe(event) {
|
|
|
282
490
|
// parent sees a subagent being resourceful and never learns it was fenced
|
|
283
491
|
// in. Named tool first, because that is what distinguishes a broken
|
|
284
492
|
// configuration from a deny rule doing its job.
|
|
285
|
-
return
|
|
493
|
+
return {
|
|
494
|
+
text: `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${event.reason}`
|
|
495
|
+
};
|
|
286
496
|
case "retry":
|
|
287
497
|
// Surfaced deliberately. This is what a budget refusal from the egress
|
|
288
498
|
// gateway looks like from inside the container, and a run that ends
|
|
289
499
|
// shortly afterwards is explained by it.
|
|
290
|
-
return `retrying the model call: ${
|
|
291
|
-
|
|
292
|
-
|
|
500
|
+
return { text: `retrying the model call: ${event.detail}` };
|
|
501
|
+
case "rateLimit":
|
|
502
|
+
/**
|
|
503
|
+
* Silent while the bucket is fine, which is every line of a healthy
|
|
504
|
+
* session — one note per `rate_limit_event` would be the noisiest thing on
|
|
505
|
+
* the stream and would say "allowed" each time.
|
|
506
|
+
*
|
|
507
|
+
* The test is inverted deliberately: an unrecognised status produces a
|
|
508
|
+
* note rather than silence. The statuses are a vendor's and only
|
|
509
|
+
* {@link RATE_LIMIT_OK} has been seen, so a new one meaning "nearly out"
|
|
510
|
+
* should surface as prose somebody reads, not be assumed benign.
|
|
511
|
+
*/
|
|
512
|
+
return event.info.status === RATE_LIMIT_OK
|
|
513
|
+
? undefined
|
|
514
|
+
: {
|
|
515
|
+
text: `the ${event.info.rateLimitType ?? "subscription"} limit reports "${event.info.status}"` +
|
|
516
|
+
(event.info.resetsAt === undefined
|
|
517
|
+
? ""
|
|
518
|
+
: ` until ${new Date(event.info.resetsAt * 1000).toISOString()}`)
|
|
519
|
+
};
|
|
520
|
+
// `init`'s session id is a handle as well as news: it reaches the host on
|
|
521
|
+
// the drain's cursor — see `DrainCursor.sessionId` in ./run.ts — where
|
|
522
|
+
// something can record it and resume with it.
|
|
293
523
|
case "init":
|
|
524
|
+
return initCard(event);
|
|
294
525
|
case "result":
|
|
295
|
-
return
|
|
526
|
+
return resultCard(event.result);
|
|
296
527
|
}
|
|
297
528
|
}
|
|
298
|
-
function clip(text, max
|
|
529
|
+
function clip(text, max) {
|
|
299
530
|
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
300
531
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub access for a session, with the token on the Worker side.
|
|
3
|
+
*
|
|
4
|
+
* The session is launched with {@link GITHUB_TOKEN_PLACEHOLDER} as `GH_TOKEN`,
|
|
5
|
+
* and git is given a credential helper that answers with it. The egress gateway swaps
|
|
6
|
+
* the placeholder for the real token on {@link GITHUB_HOSTS} only, so a process
|
|
7
|
+
* in the container can *use* GitHub as the deployment's account and can never
|
|
8
|
+
* *read* the token — the same containment as the Anthropic credential.
|
|
9
|
+
*
|
|
10
|
+
* Keyed on the placeholder, unlike the Anthropic swap: a GitHub request that
|
|
11
|
+
* did not ask for a credential stays anonymous, as it would be anywhere else.
|
|
12
|
+
*/
|
|
13
|
+
/** The hosts the token is ever sent to: git over HTTPS, and the API. */
|
|
14
|
+
export declare const GITHUB_HOSTS: ReadonlySet<string>;
|
|
15
|
+
/**
|
|
16
|
+
* What `GH_TOKEN` and git's credential helper hold. Shaped like a classic
|
|
17
|
+
* token, so nothing that checks the shape refuses it before sending it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const GITHUB_TOKEN_PLACEHOLDER: string;
|
|
20
|
+
/**
|
|
21
|
+
* The session's GitHub environment. A credential helper for `https://github.com`
|
|
22
|
+
* rather than a token in the remote URL, so the checkout's `.git/config` holds
|
|
23
|
+
* nothing and every git the session starts presents the placeholder.
|
|
24
|
+
*/
|
|
25
|
+
export declare function githubEnv(): Record<string, string>;
|
|
26
|
+
/** The keys {@link githubEnv} owns, which a host's `env` may not set beside it. */
|
|
27
|
+
export declare const GITHUB_ENV_KEYS: readonly string[];
|
|
28
|
+
/** How an authorization header carried the placeholder, if it did. */
|
|
29
|
+
type Presented = "bearer" | "basic";
|
|
30
|
+
/**
|
|
31
|
+
* Whether `value` presents the placeholder: `Bearer`/`token` as `gh` sends it,
|
|
32
|
+
* or Basic `user:placeholder` as git does after a `401`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function presentsPlaceholder(value: string | null): Presented | undefined;
|
|
35
|
+
/** The real credential, in the scheme the placeholder came in. */
|
|
36
|
+
export declare function githubAuthorization(presented: Presented, token: string): string;
|
|
37
|
+
/** A refusal in GitHub's error shape, which `gh` and git both print. */
|
|
38
|
+
export declare function githubError(message: string, status: number): Response;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub access for a session, with the token on the Worker side.
|
|
3
|
+
*
|
|
4
|
+
* The session is launched with {@link GITHUB_TOKEN_PLACEHOLDER} as `GH_TOKEN`,
|
|
5
|
+
* and git is given a credential helper that answers with it. The egress gateway swaps
|
|
6
|
+
* the placeholder for the real token on {@link GITHUB_HOSTS} only, so a process
|
|
7
|
+
* in the container can *use* GitHub as the deployment's account and can never
|
|
8
|
+
* *read* the token — the same containment as the Anthropic credential.
|
|
9
|
+
*
|
|
10
|
+
* Keyed on the placeholder, unlike the Anthropic swap: a GitHub request that
|
|
11
|
+
* did not ask for a credential stays anonymous, as it would be anywhere else.
|
|
12
|
+
*/
|
|
13
|
+
/** The hosts the token is ever sent to: git over HTTPS, and the API. */
|
|
14
|
+
export const GITHUB_HOSTS = new Set([
|
|
15
|
+
"github.com",
|
|
16
|
+
"api.github.com"
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* What `GH_TOKEN` and git's credential helper hold. Shaped like a classic
|
|
20
|
+
* token, so nothing that checks the shape refuses it before sending it.
|
|
21
|
+
*/
|
|
22
|
+
export const GITHUB_TOKEN_PLACEHOLDER = "ghp_" + "0".repeat(36);
|
|
23
|
+
/** The username GitHub expects beside a token in git's Basic auth. */
|
|
24
|
+
const GIT_USER = "x-access-token";
|
|
25
|
+
/**
|
|
26
|
+
* The session's GitHub environment. A credential helper for `https://github.com`
|
|
27
|
+
* rather than a token in the remote URL, so the checkout's `.git/config` holds
|
|
28
|
+
* nothing and every git the session starts presents the placeholder.
|
|
29
|
+
*/
|
|
30
|
+
export function githubEnv() {
|
|
31
|
+
return {
|
|
32
|
+
GH_TOKEN: GITHUB_TOKEN_PLACEHOLDER,
|
|
33
|
+
GIT_CONFIG_COUNT: "1",
|
|
34
|
+
GIT_CONFIG_KEY_0: "credential.https://github.com.helper",
|
|
35
|
+
// Answers `get` only; `store` and `erase` are no-ops that still exit 0.
|
|
36
|
+
GIT_CONFIG_VALUE_0: `!f() { if test "$1" = get; then printf 'username=${GIT_USER}\\npassword=%s\\n' "$GH_TOKEN"; fi; }; f`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The keys {@link githubEnv} owns, which a host's `env` may not set beside it. */
|
|
40
|
+
export const GITHUB_ENV_KEYS = Object.keys(githubEnv());
|
|
41
|
+
/**
|
|
42
|
+
* Whether `value` presents the placeholder: `Bearer`/`token` as `gh` sends it,
|
|
43
|
+
* or Basic `user:placeholder` as git does after a `401`.
|
|
44
|
+
*/
|
|
45
|
+
export function presentsPlaceholder(value) {
|
|
46
|
+
if (!value)
|
|
47
|
+
return undefined;
|
|
48
|
+
const [scheme = "", credential = ""] = value.trim().split(/\s+/, 2);
|
|
49
|
+
switch (scheme.toLowerCase()) {
|
|
50
|
+
case "bearer":
|
|
51
|
+
case "token":
|
|
52
|
+
return credential === GITHUB_TOKEN_PLACEHOLDER ? "bearer" : undefined;
|
|
53
|
+
case "basic": {
|
|
54
|
+
let decoded;
|
|
55
|
+
try {
|
|
56
|
+
decoded = atob(credential);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const password = decoded.slice(decoded.indexOf(":") + 1);
|
|
62
|
+
return decoded.includes(":") && password === GITHUB_TOKEN_PLACEHOLDER
|
|
63
|
+
? "basic"
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The real credential, in the scheme the placeholder came in. */
|
|
71
|
+
export function githubAuthorization(presented, token) {
|
|
72
|
+
return presented === "basic"
|
|
73
|
+
? `Basic ${btoa(`${GIT_USER}:${token}`)}`
|
|
74
|
+
: `Bearer ${token}`;
|
|
75
|
+
}
|
|
76
|
+
/** A refusal in GitHub's error shape, which `gh` and git both print. */
|
|
77
|
+
export function githubError(message, status) {
|
|
78
|
+
return new Response(JSON.stringify({ message }), {
|
|
79
|
+
status,
|
|
80
|
+
headers: { "content-type": "application/json" }
|
|
81
|
+
});
|
|
82
|
+
}
|