@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.
Files changed (134) hide show
  1. package/README.md +59 -79
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +53 -0
  5. package/dist/claude-code/cards.js +272 -0
  6. package/dist/claude-code/config.d.ts +73 -31
  7. package/dist/claude-code/config.js +0 -2
  8. package/dist/claude-code/credentials.d.ts +21 -9
  9. package/dist/claude-code/credentials.js +43 -9
  10. package/dist/claude-code/egress.d.ts +15 -14
  11. package/dist/claude-code/egress.js +51 -14
  12. package/dist/claude-code/events.d.ts +153 -9
  13. package/dist/claude-code/events.js +289 -58
  14. package/dist/claude-code/github.d.ts +39 -0
  15. package/dist/claude-code/github.js +82 -0
  16. package/dist/claude-code/index.d.ts +24 -80
  17. package/dist/claude-code/index.js +19 -234
  18. package/dist/claude-code/model.d.ts +152 -0
  19. package/dist/claude-code/model.js +342 -0
  20. package/dist/claude-code/run.d.ts +283 -92
  21. package/dist/claude-code/run.js +620 -114
  22. package/dist/claude-code/session.d.ts +131 -0
  23. package/dist/claude-code/session.js +285 -0
  24. package/dist/claude-code/spec.d.ts +46 -0
  25. package/dist/claude-code/spec.js +83 -0
  26. package/dist/claude-code/verified.d.ts +15 -0
  27. package/dist/claude-code/verified.js +15 -0
  28. package/dist/computer/context.d.ts +19 -0
  29. package/dist/computer/context.js +222 -0
  30. package/dist/computer/file-lock.d.ts +5 -0
  31. package/dist/computer/file-lock.js +40 -0
  32. package/dist/computer/gate.d.ts +12 -12
  33. package/dist/computer/gate.js +12 -12
  34. package/dist/computer/index.d.ts +57 -255
  35. package/dist/computer/index.js +98 -1039
  36. package/dist/computer/paths.d.ts +47 -22
  37. package/dist/computer/paths.js +64 -49
  38. package/dist/computer/proxy.d.ts +45 -0
  39. package/dist/computer/proxy.js +234 -0
  40. package/dist/computer/render.d.ts +27 -38
  41. package/dist/computer/render.js +32 -65
  42. package/dist/computer/tools-exec.d.ts +3 -0
  43. package/dist/computer/tools-exec.js +268 -0
  44. package/dist/computer/tools-file.d.ts +12 -0
  45. package/dist/computer/tools-file.js +64 -0
  46. package/dist/computer/tools-grep.d.ts +3 -0
  47. package/dist/computer/tools-grep.js +129 -0
  48. package/dist/repo/checkout.d.ts +35 -6
  49. package/dist/repo/checkout.js +64 -6
  50. package/dist/repo/context.d.ts +135 -0
  51. package/dist/repo/context.js +582 -0
  52. package/dist/repo/index.d.ts +79 -57
  53. package/dist/repo/index.js +118 -1112
  54. package/dist/repo/tools-checks.d.ts +3 -0
  55. package/dist/repo/tools-checks.js +112 -0
  56. package/dist/repo/tools-clone.d.ts +4 -0
  57. package/dist/repo/tools-clone.js +192 -0
  58. package/dist/repo/tools-pr.d.ts +14 -0
  59. package/dist/repo/tools-pr.js +196 -0
  60. package/dist/repo/tools-review.d.ts +12 -0
  61. package/dist/repo/tools-review.js +393 -0
  62. package/dist/repo/tools-worktree.d.ts +4 -0
  63. package/dist/repo/tools-worktree.js +305 -0
  64. package/dist/repo/tools-worktrees.d.ts +12 -0
  65. package/dist/repo/tools-worktrees.js +48 -0
  66. package/dist/repo/url.d.ts +2 -2
  67. package/dist/repo/url.js +7 -7
  68. package/dist/scratch/index.d.ts +4 -4
  69. package/dist/scratch/index.js +12 -11
  70. package/dist/{computer → workspace}/advisory.d.ts +33 -20
  71. package/dist/{computer → workspace}/advisory.js +32 -7
  72. package/dist/workspace/ca-trust.d.ts +95 -0
  73. package/dist/workspace/ca-trust.js +312 -0
  74. package/dist/workspace/container-deps.d.ts +54 -0
  75. package/dist/workspace/container-deps.js +224 -0
  76. package/dist/workspace/container-fault.d.ts +38 -0
  77. package/dist/workspace/container-fault.js +73 -0
  78. package/dist/workspace/exec.d.ts +94 -0
  79. package/dist/workspace/exec.js +153 -0
  80. package/dist/workspace/format.d.ts +36 -0
  81. package/dist/workspace/format.js +57 -0
  82. package/dist/workspace/git-host.d.ts +79 -0
  83. package/dist/workspace/git-host.js +226 -0
  84. package/dist/workspace/git-identity.d.ts +57 -0
  85. package/dist/workspace/git-identity.js +147 -0
  86. package/dist/workspace/index.d.ts +43 -47
  87. package/dist/workspace/index.js +52 -112
  88. package/dist/workspace/install-job.d.ts +121 -0
  89. package/dist/workspace/install-job.js +866 -0
  90. package/dist/{computer → workspace}/install.d.ts +15 -19
  91. package/dist/{computer → workspace}/install.js +8 -11
  92. package/dist/workspace/object.d.ts +502 -0
  93. package/dist/workspace/object.js +1427 -0
  94. package/dist/workspace/open.d.ts +95 -0
  95. package/dist/workspace/open.js +63 -0
  96. package/dist/workspace/read.d.ts +16 -0
  97. package/dist/workspace/read.js +22 -0
  98. package/dist/workspace/shell.d.ts +45 -0
  99. package/dist/workspace/shell.js +80 -0
  100. package/dist/workspace/sync.d.ts +111 -0
  101. package/dist/workspace/sync.js +124 -0
  102. package/dist/workspace/wake.d.ts +19 -0
  103. package/dist/workspace/wake.js +10 -0
  104. package/package.json +28 -42
  105. package/dist/arc-agi/analysis.d.ts +0 -336
  106. package/dist/arc-agi/analysis.js +0 -829
  107. package/dist/arc-agi/client.d.ts +0 -82
  108. package/dist/arc-agi/client.js +0 -113
  109. package/dist/arc-agi/game-tools.d.ts +0 -25
  110. package/dist/arc-agi/game-tools.js +0 -26
  111. package/dist/arc-agi/index.d.ts +0 -42
  112. package/dist/arc-agi/index.js +0 -175
  113. package/dist/arc-agi/main-agent.d.ts +0 -27
  114. package/dist/arc-agi/main-agent.js +0 -65
  115. package/dist/arc-agi/recipe.d.ts +0 -51
  116. package/dist/arc-agi/recipe.js +0 -100
  117. package/dist/arc-agi/scorecard.d.ts +0 -118
  118. package/dist/arc-agi/scorecard.js +0 -163
  119. package/dist/arc-agi/soul.d.ts +0 -40
  120. package/dist/arc-agi/soul.js +0 -90
  121. package/dist/arc-agi/store.d.ts +0 -154
  122. package/dist/arc-agi/store.js +0 -216
  123. package/dist/arc-agi/tools.d.ts +0 -36
  124. package/dist/arc-agi/tools.js +0 -576
  125. package/dist/arc-agi/types.d.ts +0 -212
  126. package/dist/arc-agi/types.js +0 -19
  127. package/dist/claude-code/recipe.d.ts +0 -53
  128. package/dist/claude-code/recipe.js +0 -100
  129. package/dist/computer/read.d.ts +0 -137
  130. package/dist/computer/read.js +0 -203
  131. package/dist/recall/index.d.ts +0 -99
  132. package/dist/recall/index.js +0 -171
  133. package/dist/triage/index.d.ts +0 -355
  134. 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
- * Flatten an `assistant` message into a line of prose and the tools it called.
45
+ * An `assistant` message as events, one per block, in the order it said them.
31
46
  *
32
- * `thinking` blocks are dropped. They are the model's private reasoning, they
33
- * are the largest thing in the stream by a wide margin, and the parent agent is
34
- * being shown progress rather than asked to review the subagent's deliberation.
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 text = [];
41
- const tools = [];
56
+ const events = [];
42
57
  for (const block of content) {
43
58
  if (!asRecord(block))
44
59
  continue;
45
- const blockText = str(block.text);
46
- if (block.type === "text" && blockText)
47
- text.push(blockText.trim());
48
- const toolName = str(block.name);
49
- if (block.type === "tool_use" && toolName)
50
- tools.push(toolName);
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 { text: text.join(" ").replace(/\s+/g, " ").trim(), tools };
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
- const { text, tools } = readAssistant(message);
180
- if (!text && tools.length === 0)
181
- break;
182
- events.push({ kind: "assistant", text, tools });
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. They are large, they are already
187
- // summarised by the assistant turn that follows, and the un-nested ones
188
- // belong to the outer run's own tools — so they are deliberately not
189
- // surfaced as progress.
190
- case "user":
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 { events, carry, skipped, nested, ...(sample ? { sample } : {}) };
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
- /** How much of one progress note the parent is shown. */
226
- const PROGRESS_MAX_CHARS = 240;
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
- * Shorter than a progress note and for a different reason: a note is read by a
231
- * model deciding what to do next, whereas this is read by a person deciding
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 can post.
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 chunk that dies mid-drain is retried by the Workflow,
244
- * the re-attach replays the tail, and the same assistant turn is parsed twice. A
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 text = describe(event);
256
- if (!text)
462
+ const note = describe(event);
463
+ if (!note)
257
464
  continue;
258
- out.push({ key: `claude:${n}`, text });
465
+ out.push({ key: `claude:${n}`, ...note });
259
466
  n++;
260
467
  }
261
468
  return out;
262
469
  }
263
- /** One line of progress, or nothing when the event is not worth a note. */
470
+ /** One note, or nothing when the event is not worth one. */
264
471
  function describe(event) {
265
472
  switch (event.kind) {
266
- // Tool calls are not narration. A turn that only called tools said nothing
267
- // the reader can act on, and it cost one Slack message per Bash/Read/Edit —
268
- // nine of them for a one-line README edit. The tool names added nothing to
269
- // the turns that *did* have text either, so the prefix goes with them.
270
- //
271
- // Same policy the in-process agents already apply in the gatekeeper
272
- // (src/agents/shared/loop.ts: "Tool-only steps stay silent in Slack").
273
- // Deliberately narrower than silencing the whole channel: `denied` and
274
- // `retry` below stay, because they are the only evidence of a session that
275
- // is being refused or throttled.
276
- case "assistant":
277
- return event.text ? clip(event.text) : undefined;
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 `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${clip(event.reason)}`;
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: ${clip(event.detail)}`;
291
- // `init` names a session id nobody outside this package can use, and
292
- // `result` is the terminal outcome the caller reports itself.
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 undefined;
526
+ return resultCard(event.result);
296
527
  }
297
528
  }
298
- function clip(text, max = PROGRESS_MAX_CHARS) {
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
+ }