@spunto/build 0.10.0 → 0.12.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 +2 -0
- package/package.json +1 -1
- package/src/agent-stream/agent-stream.ts +44 -0
- package/src/agent-stream/index.ts +1 -0
- package/src/script/index.ts +1 -0
- package/src/script/setup-script.ts +20 -4
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spunto/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Spunto's shared Build engine — the devcontainer image protocol and VS Code extension registry clients, with no database, no HTTP framework and no UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -122,6 +122,37 @@ export function interruptLine(protocol: AgentProtocol, requestId: string): strin
|
|
|
122
122
|
return `${JSON.stringify({ type: "control_request", request_id: requestId, request: { subtype: "interrupt" } })}\n`
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Ask the harness to **name the conversation**, or null for a dialect that cannot be asked.
|
|
127
|
+
*
|
|
128
|
+
* Claude Code used to do this on its own within seconds of the first turn, headless included —
|
|
129
|
+
* that title is what `sessionTitleProbe` reads. Measured on 2.1.296 (October 2026), it no longer
|
|
130
|
+
* does for a session driven over `--input-format stream-json`: the transcript gets `atis-latch`
|
|
131
|
+
* and `last-prompt` lines and never an `ai-title`, so every task kept its placeholder. What it
|
|
132
|
+
* still answers is the SDK's own `generate_session_title` control request — the call its
|
|
133
|
+
* TypeScript SDK makes — with `{"title":…}` in a `control_response` on stdout, and `persist`
|
|
134
|
+
* writes the same `ai-title` line to the transcript as before.
|
|
135
|
+
*
|
|
136
|
+
* Asking is not naming: the harness still picks the words, from the human's prompt (the
|
|
137
|
+
* `description`), exactly as it would have unprompted. Nothing of ours ends up in the title.
|
|
138
|
+
*
|
|
139
|
+
* The request id carries `TITLE_REQUEST_PREFIX` so the answer can be told apart from every other
|
|
140
|
+
* `control_response` (an interrupt's, say) without keeping state between write and read.
|
|
141
|
+
*/
|
|
142
|
+
export function sessionTitleRequestLine(protocol: AgentProtocol, requestId: string, description: string): string | null {
|
|
143
|
+
if (protocol !== "claude-stream") return null
|
|
144
|
+
const text = description.trim()
|
|
145
|
+
if (!text) return null
|
|
146
|
+
return `${JSON.stringify({
|
|
147
|
+
type: "control_request",
|
|
148
|
+
request_id: `${TITLE_REQUEST_PREFIX}${requestId}`,
|
|
149
|
+
request: { subtype: "generate_session_title", description: text, persist: true },
|
|
150
|
+
})}\n`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Marks the `control_request`s that ask for a title, and so the `control_response`s that answer. */
|
|
154
|
+
const TITLE_REQUEST_PREFIX = "spunto-title-"
|
|
155
|
+
|
|
125
156
|
/**
|
|
126
157
|
* A shell that prints the line where the harness wrote **the name it gave the conversation**, or
|
|
127
158
|
* null for a dialect that has no such thing.
|
|
@@ -143,6 +174,9 @@ export function interruptLine(protocol: AgentProtocol, requestId: string): strin
|
|
|
143
174
|
* `--no-session-persistence` writes no transcript, and then this finds nothing — which is a
|
|
144
175
|
* normal answer, not an error.
|
|
145
176
|
*
|
|
177
|
+
* Since Claude Code 2.1.296 an interactive session only writes that line once asked
|
|
178
|
+
* (`sessionTitleRequestLine`); a one-shot `claude-json` run has no stdin to be asked on.
|
|
179
|
+
*
|
|
146
180
|
* `jsonl` gets no probe on purpose: a harness speaking our vocabulary says the name in band,
|
|
147
181
|
* `{"type":"session.title","title":"…"}`, and pays nothing for it.
|
|
148
182
|
*/
|
|
@@ -383,6 +417,16 @@ function adaptClaudeJson(obj: Record<string, unknown>): NormalizedEvent[] {
|
|
|
383
417
|
return events
|
|
384
418
|
}
|
|
385
419
|
|
|
420
|
+
// The answer to `sessionTitleRequestLine`. Only ours, recognised by request id: a
|
|
421
|
+
// `control_response` is also how an interrupt is acknowledged, and that one is no name.
|
|
422
|
+
if (type === "control_response") {
|
|
423
|
+
const response = (obj.response ?? {}) as Record<string, unknown>
|
|
424
|
+
const inner = (response.response ?? {}) as Record<string, unknown>
|
|
425
|
+
const requestId = typeof response.request_id === "string" ? response.request_id : ""
|
|
426
|
+
const title = response.subtype === "success" && requestId.startsWith(TITLE_REQUEST_PREFIX) ? cleanTitle(inner.title) : null
|
|
427
|
+
if (title) return [{ type: "session.title", payload: { title } }]
|
|
428
|
+
}
|
|
429
|
+
|
|
386
430
|
// Known-but-uninteresting (`rate_limit_event`, `system/thinking_tokens`) and anything the CLI
|
|
387
431
|
// grows next land here rather than being dropped: an unknown event is information about the
|
|
388
432
|
// harness, and the UI keeps it behind a toggle.
|
package/src/script/index.ts
CHANGED
|
@@ -1215,10 +1215,25 @@ export type StartScriptParams = {
|
|
|
1215
1215
|
* the image build used. Absent/unusable = Open VSX.
|
|
1216
1216
|
*/
|
|
1217
1217
|
extensionsGallery?: string | null
|
|
1218
|
+
/**
|
|
1219
|
+
* The port code-server listens on, 8080 by default (what every worker and the proxy's `code` host
|
|
1220
|
+
* assume). For a box where 8080 is taken by the host itself — a Blaxel sandbox's own API.
|
|
1221
|
+
*/
|
|
1222
|
+
idePort?: number
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/** code-server's port when the caller doesn't say otherwise. */
|
|
1226
|
+
export const DEFAULT_IDE_PORT = 8080
|
|
1227
|
+
|
|
1228
|
+
function idePortOf(port: number | undefined): number {
|
|
1229
|
+
if (port === undefined) return DEFAULT_IDE_PORT
|
|
1230
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`idePort must be a TCP port, got ${port}`)
|
|
1231
|
+
return port
|
|
1218
1232
|
}
|
|
1219
1233
|
|
|
1220
1234
|
export function buildStartScript(params: StartScriptParams): { script: string; hasDinD: boolean } {
|
|
1221
1235
|
const { project, sshGatewayPublicKey, colorTabsByProject, extensionsGallery } = params
|
|
1236
|
+
const idePort = idePortOf(params.idePort)
|
|
1222
1237
|
const homeDir = "/home/vscode"
|
|
1223
1238
|
const username = "vscode"
|
|
1224
1239
|
|
|
@@ -1338,7 +1353,7 @@ export function buildStartScript(params: StartScriptParams): { script: string; h
|
|
|
1338
1353
|
// ── 4c. Restore the user's PORT secret for code-server's own terminal panel ─
|
|
1339
1354
|
// code-server reads $PORT to override --bind-addr (see § 5 below), so its
|
|
1340
1355
|
// whole process tree — including the integrated terminal it spawns — inherits
|
|
1341
|
-
// PORT
|
|
1356
|
+
// PORT=<idePort> unless corrected. `terminal.integrated.env.linux` lets VS Code
|
|
1342
1357
|
// inject env overrides into terminals it spawns without touching the server
|
|
1343
1358
|
// process itself, so a `PORT` project secret still reaches the user's shell.
|
|
1344
1359
|
// Uses code-server's own bundled node (guaranteed present regardless of the
|
|
@@ -1387,12 +1402,12 @@ export function buildStartScript(params: StartScriptParams): { script: string; h
|
|
|
1387
1402
|
"while true; do",
|
|
1388
1403
|
' VSCODE_PROXY_URI="${PUBLIC_PROTOCOL}://${WORKER_SLUG}-{{port}}.${BASE_DOMAIN}" \\',
|
|
1389
1404
|
// PORT is overridden here (not unset globally) so a project secret named
|
|
1390
|
-
// PORT can't make code-server bind elsewhere. This also leaks PORT
|
|
1405
|
+
// PORT can't make code-server bind elsewhere. This also leaks PORT=<idePort>
|
|
1391
1406
|
// into code-server's own process tree (its integrated terminal included) —
|
|
1392
1407
|
// § 4c above restores the original value for that terminal specifically.
|
|
1393
1408
|
// apps/agent/src/terminal.ts (dashboard's separate terminal feature) is a
|
|
1394
1409
|
// different process tree entirely and was never affected.
|
|
1395
|
-
|
|
1410
|
+
` PORT=${idePort} code-server --bind-addr 0.0.0.0:${idePort} --auth none /workspace 2>&1`,
|
|
1396
1411
|
' echo "[start] code-server exited ($?), restarting in 3s..."',
|
|
1397
1412
|
" sleep 3",
|
|
1398
1413
|
"done",
|
|
@@ -1491,6 +1506,7 @@ export type SetupPhase = {
|
|
|
1491
1506
|
// deleted once WORKER_SETUP_ORCHESTRATOR=agent becomes the default. See RFC 0017 §2.1.
|
|
1492
1507
|
export function buildContainerScript(params: StartScriptParams): { script: string; hasDinD: boolean } {
|
|
1493
1508
|
const { project, sshGatewayPublicKey, colorTabsByProject, extensionsGallery } = params
|
|
1509
|
+
const idePort = idePortOf(params.idePort)
|
|
1494
1510
|
const homeDir = "/home/vscode"
|
|
1495
1511
|
const username = "vscode"
|
|
1496
1512
|
|
|
@@ -1621,7 +1637,7 @@ export function buildContainerScript(params: StartScriptParams): { script: strin
|
|
|
1621
1637
|
...galleryExport(extensionsGallery),
|
|
1622
1638
|
"while true; do",
|
|
1623
1639
|
' VSCODE_PROXY_URI="${PUBLIC_PROTOCOL}://${WORKER_SLUG}-{{port}}.${BASE_DOMAIN}" \\',
|
|
1624
|
-
|
|
1640
|
+
` PORT=${idePort} code-server --bind-addr 0.0.0.0:${idePort} --auth none /workspace 2>&1`,
|
|
1625
1641
|
' echo "[start] code-server exited ($?), restarting in 3s..."',
|
|
1626
1642
|
" sleep 3",
|
|
1627
1643
|
"done",
|