osborn 0.9.249 → 0.9.251
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/dist/claude-llm.js +13 -0
- package/dist/content/compaction-lens-worker.d.ts +23 -0
- package/dist/content/compaction-lens-worker.js +188 -0
- package/dist/content/compaction-lens.d.ts +106 -0
- package/dist/content/compaction-lens.js +222 -0
- package/dist/content/lens-audio.d.ts +38 -0
- package/dist/content/lens-audio.js +105 -0
- package/dist/content/lens-clip-arc.d.ts +80 -0
- package/dist/content/lens-clip-arc.js +148 -0
- package/dist/content/lens-clip-formats.d.ts +58 -0
- package/dist/content/lens-clip-formats.js +175 -0
- package/dist/content/lens-clip-search.d.ts +51 -0
- package/dist/content/lens-clip-search.js +138 -0
- package/dist/content/lens-clip-window.d.ts +72 -0
- package/dist/content/lens-clip-window.js +257 -0
- package/dist/content/lens-db.d.ts +70 -0
- package/dist/content/lens-db.js +170 -0
- package/dist/content/lens-hn.d.ts +19 -0
- package/dist/content/lens-hn.js +69 -0
- package/dist/content/lens-ingest.d.ts +67 -0
- package/dist/content/lens-ingest.js +155 -0
- package/dist/content/lens-launch.d.ts +46 -0
- package/dist/content/lens-launch.js +111 -0
- package/dist/content/lens-library-index.d.ts +30 -0
- package/dist/content/lens-library-index.js +79 -0
- package/dist/content/lens-library-select.d.ts +59 -0
- package/dist/content/lens-library-select.js +77 -0
- package/dist/content/lens-library.d.ts +92 -0
- package/dist/content/lens-library.js +179 -0
- package/dist/content/lens-model.d.ts +64 -0
- package/dist/content/lens-model.js +193 -0
- package/dist/content/lens-paths.d.ts +43 -0
- package/dist/content/lens-paths.js +166 -0
- package/dist/content/lens-period-block.d.ts +63 -0
- package/dist/content/lens-period-block.js +99 -0
- package/dist/content/lens-period-boundaries.d.ts +13 -0
- package/dist/content/lens-period-boundaries.js +40 -0
- package/dist/content/lens-period-render.d.ts +28 -0
- package/dist/content/lens-period-render.js +70 -0
- package/dist/content/lens-period.d.ts +106 -0
- package/dist/content/lens-period.js +205 -0
- package/dist/content/lens-profile.d.ts +47 -0
- package/dist/content/lens-profile.js +95 -0
- package/dist/content/lens-quotes.d.ts +103 -0
- package/dist/content/lens-quotes.js +186 -0
- package/dist/content/lens-redact.d.ts +61 -0
- package/dist/content/lens-redact.js +187 -0
- package/dist/content/lens-strip.d.ts +33 -0
- package/dist/content/lens-strip.js +132 -0
- package/dist/content/period-run.d.ts +30 -0
- package/dist/content/period-run.js +135 -0
- package/dist/content/render-ws-data.d.ts +8 -0
- package/dist/content/render-ws-data.js +108 -0
- package/dist/content/render-ws-screens.d.ts +25 -0
- package/dist/content/render-ws-screens.js +189 -0
- package/dist/content/render-ws-story.d.ts +1 -0
- package/dist/content/render-ws-story.js +233 -0
- package/dist/content/transcript-sanitizer.d.ts +195 -0
- package/dist/content/transcript-sanitizer.js +515 -0
- package/dist/index.js +34 -0
- package/package.json +1 -1
- package/test-results/.last-run.json +0 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* period-run.ts — run the PERIOD MAP for one session. Stages (each its own module,
|
|
3
|
+
* swappable / rerunnable alone):
|
|
4
|
+
* 1. block loader lens-db.loadConversationRows + lens-period-block.selectBlock
|
|
5
|
+
* 2. strip/redact lens-strip (inside loadConversationRows) + secret/client redaction
|
|
6
|
+
* 3. period map lens-period (one OpenRouter call, quotes verified deterministically)
|
|
7
|
+
* 4. audio (opt.) lens-audio AudioAdapter — offsets only when a recording exists
|
|
8
|
+
* 5. render lens-period-render → review .md (redacted again)
|
|
9
|
+
* OpenRouter only. Hard cap: 1 call, $0.50. Never writes content-profile.md or the HWM.
|
|
10
|
+
*
|
|
11
|
+
* CLI: npx tsx agent/src/content/period-run.ts --session <id> --out <file.md>
|
|
12
|
+
* [--since 24h|ISO] [--until ISO] [--from-row N] [--dry]
|
|
13
|
+
*/
|
|
14
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, resolve } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
import { loadConversationRows, resolveSessionDb } from './lens-db.js';
|
|
18
|
+
import { Budget, chat, getModelInfo, windowTokensFor } from './lens-model.js';
|
|
19
|
+
import { loadClientRedactor } from './lens-redact.js';
|
|
20
|
+
import { redactSecrets } from './transcript-sanitizer.js';
|
|
21
|
+
import { readCompactionBoundaries } from './lens-period-boundaries.js';
|
|
22
|
+
import { localRecordingAdapter } from './lens-audio.js';
|
|
23
|
+
import { refuseOutPath } from './lens-paths.js';
|
|
24
|
+
import { parseSince, selectBlock } from './lens-period-block.js';
|
|
25
|
+
import { attachAudio, periodPrompt, readPeriodMap } from './lens-period.js';
|
|
26
|
+
import { renderPeriodMd } from './lens-period-render.js';
|
|
27
|
+
export const PERIOD_CAP_USD = 0.5;
|
|
28
|
+
const MAX_OUT = 12_000;
|
|
29
|
+
export async function runPeriodMap(o) {
|
|
30
|
+
const log = o.log ?? (() => { });
|
|
31
|
+
const db = resolveSessionDb(o.sessionId);
|
|
32
|
+
if (!db)
|
|
33
|
+
throw new Error(`no session.db for ${o.sessionId}`);
|
|
34
|
+
const projectDir = dirname(dirname(dirname(db)));
|
|
35
|
+
const refused = refuseOutPath(o.outPath, projectDir);
|
|
36
|
+
if (refused)
|
|
37
|
+
throw new Error(refused);
|
|
38
|
+
const key = process.env.OPENROUTER_API_KEY;
|
|
39
|
+
if (!key && !o.dry)
|
|
40
|
+
throw new Error('OPENROUTER_API_KEY not set');
|
|
41
|
+
const info = await getModelInfo(key ?? '');
|
|
42
|
+
if (!info)
|
|
43
|
+
throw new Error('model info unavailable (cannot enforce the cost cap)');
|
|
44
|
+
const capTokens = windowTokensFor(info);
|
|
45
|
+
const red = loadClientRedactor(projectDir);
|
|
46
|
+
// 1+2. block loader (strip + sanitize + secret/client redaction happen inside the load)
|
|
47
|
+
const all = loadConversationRows(db, 0, red.redact);
|
|
48
|
+
const sinceMs = o.since ? parseSince(o.since) : undefined;
|
|
49
|
+
const untilMs = o.until ? parseSince(o.until) : undefined;
|
|
50
|
+
if (sinceMs !== undefined && !Number.isFinite(sinceMs))
|
|
51
|
+
throw new Error(`bad --since ${o.since}`);
|
|
52
|
+
if (untilMs !== undefined && !Number.isFinite(untilMs))
|
|
53
|
+
throw new Error(`bad --until ${o.until}`);
|
|
54
|
+
const block = selectBlock(all.records, { boundaries: readCompactionBoundaries(db), fromRowId: o.fromRowId, toRowId: o.toRowId, sinceMs, untilMs, maxTokens: capTokens });
|
|
55
|
+
// Strip counts for exactly the block's row range (incl. rows dropped inside it).
|
|
56
|
+
const scoped = block ? loadConversationRows(db, block.firstRowId - 1, red.redact, block.lastRowId) : null;
|
|
57
|
+
const { records: _r, text: _t, ...blockMeta } = block ?? {};
|
|
58
|
+
const r = {
|
|
59
|
+
sessionId: o.sessionId, model: info.id, contextLength: info.contextLength, capTokens,
|
|
60
|
+
block: block ? blockMeta : null, stripped: scoped?.stats.stripped ?? {}, rowsRead: scoped?.stats.rowsRead ?? 0,
|
|
61
|
+
rowsKeptInSession: scoped?.records.length ?? 0, map: null, calls: 0, promptTokens: 0, completionTokens: 0, costUsd: 0,
|
|
62
|
+
capUsd: PERIOD_CAP_USD, finish: '', provider: '', audio: 'none (transcript only — row ranges, no clip offsets)', errors: [],
|
|
63
|
+
};
|
|
64
|
+
const finish = () => {
|
|
65
|
+
const md = redactSecrets(red.redact(renderPeriodMd(r)), { assistant: true });
|
|
66
|
+
mkdirSync(dirname(o.outPath), { recursive: true });
|
|
67
|
+
writeFileSync(o.outPath, md, 'utf-8');
|
|
68
|
+
log(`wrote ${o.outPath}`);
|
|
69
|
+
return r;
|
|
70
|
+
};
|
|
71
|
+
if (!block)
|
|
72
|
+
return (r.errors.push('empty block'), finish());
|
|
73
|
+
log(`block ${block.basis}: rows #${block.firstRowId}–#${block.lastRowId} (${block.records.length}), ~${block.estTokens} tok${block.leftOut ? `, left out ${block.leftOut.rows} rows` : ''}`);
|
|
74
|
+
if (o.dry)
|
|
75
|
+
return finish();
|
|
76
|
+
// 3. period map — ONE call, reasoning off (lens-model.chat), instructions after the transcript
|
|
77
|
+
const budget = new Budget(info, { maxCalls: 1, maxCostUsd: PERIOD_CAP_USD });
|
|
78
|
+
const prompt = periodPrompt(block.text, { from: block.from, to: block.to, rows: block.records.length });
|
|
79
|
+
try {
|
|
80
|
+
const res = await chat(budget, prompt, key, MAX_OUT);
|
|
81
|
+
if (!res)
|
|
82
|
+
r.errors.push('refused by the cost cap');
|
|
83
|
+
else {
|
|
84
|
+
Object.assign(r, { promptTokens: res.promptTokens, completionTokens: res.completionTokens, finish: res.finish, provider: res.provider });
|
|
85
|
+
log(`map: ${res.promptTokens}+${res.completionTokens} tok, $${res.costUsd.toFixed(4)}, finish=${res.finish}`);
|
|
86
|
+
if (res.finish === 'length')
|
|
87
|
+
r.errors.push('output cap hit (JSON may be truncated)');
|
|
88
|
+
r.map = readPeriodMap(res.content, block.records);
|
|
89
|
+
if (!r.map) {
|
|
90
|
+
// Keep the (redacted) raw reply next to the review file for debugging.
|
|
91
|
+
const rawPath = o.outPath.replace(/\.md$/, '') + '.raw.txt';
|
|
92
|
+
writeFileSync(rawPath, redactSecrets(red.redact(res.content), { assistant: true }), 'utf-8');
|
|
93
|
+
r.errors.push(`no parseable period map (raw reply: ${rawPath})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
r.errors.push(`model call failed: ${e?.message ?? e}`);
|
|
99
|
+
}
|
|
100
|
+
r.calls = budget.calls;
|
|
101
|
+
r.costUsd = +budget.spentUsd.toFixed(4);
|
|
102
|
+
// 4. optional audio adapter
|
|
103
|
+
const align = (o.audio ?? localRecordingAdapter).align(o.sessionId, projectDir, block.from);
|
|
104
|
+
if (align)
|
|
105
|
+
r.audio = `${align.path} (${align.approximate ? 'approximate' : 'exact'} start: ${align.basis})`;
|
|
106
|
+
if (r.map)
|
|
107
|
+
attachAudio(r.map, align);
|
|
108
|
+
return finish();
|
|
109
|
+
}
|
|
110
|
+
function arg(name) {
|
|
111
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
112
|
+
return i > -1 ? process.argv[i + 1] : undefined;
|
|
113
|
+
}
|
|
114
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
115
|
+
const sessionId = arg('session') ?? '';
|
|
116
|
+
const outPath = arg('out') ?? '';
|
|
117
|
+
const fromRow = arg('from-row');
|
|
118
|
+
runPeriodMap({
|
|
119
|
+
sessionId, outPath, since: arg('since'), until: arg('until'),
|
|
120
|
+
fromRowId: fromRow ? Number(fromRow) : undefined, dry: process.argv.includes('--dry'),
|
|
121
|
+
log: m => console.log(m),
|
|
122
|
+
})
|
|
123
|
+
.then(r => {
|
|
124
|
+
const m = r.map;
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
block: r.block && { rows: r.block.periodRows - (r.block.leftOut?.rows ?? 0), estTokens: r.block.estTokens, leftOut: r.block.leftOut },
|
|
127
|
+
stripped: r.stripped, cost: r.costUsd, calls: r.calls, errors: r.errors,
|
|
128
|
+
map: m && { highLeverage: m.highLeverage.length, arc: m.arc.length, stories: m.stories.length, angles: m.angles.length, quotesKept: m.quotesKept, quoteDrops: m.drops.length, itemsDropped: m.itemsDropped.length },
|
|
129
|
+
}, null, 2));
|
|
130
|
+
})
|
|
131
|
+
.catch(e => {
|
|
132
|
+
console.error(`period map failed: ${e?.message ?? e}`);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const SCRIPT: string[];
|
|
2
|
+
export declare const CAPTIONS: string[];
|
|
3
|
+
/** name -> svg. */
|
|
4
|
+
export declare const SCREENS: Record<string, () => string>;
|
|
5
|
+
/** Per narration line: the screens shown during it (split evenly across the line's duration). */
|
|
6
|
+
export declare const LINE_SCREENS: string[][];
|
|
7
|
+
/** Artifact provenance per screen (for the report / shotlist). */
|
|
8
|
+
export declare const PROVENANCE: Record<string, string>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-ws-data.ts — script, captions and screen definitions for render-ws-story.ts.
|
|
3
|
+
* Narration is TOPIC-ONLY (no first person), per user decision.
|
|
4
|
+
* Code lines: verbatim from agent/src/index.ts (before = git a4388ca^ i.e. 0.9.152; after = current tree).
|
|
5
|
+
* Log lines: verbatim from session recall rows (agent-ad1edf68 L98, agent-a346e43a L36), hostname -> <app>.
|
|
6
|
+
*/
|
|
7
|
+
import { codeScreen, termScreen, pathDiagram, compareDiagram, headerDiagram, editorScreen, lsScreen, endCard } from './render-ws-screens.js';
|
|
8
|
+
// Voice text (numbers spelled for TTS). CAPTIONS below are the on-screen form of the same words.
|
|
9
|
+
export const SCRIPT = [
|
|
10
|
+
'One missing header killed this terminal. The editor loaded, but the terminal and file tree died instantly. Error ten-oh-six.',
|
|
11
|
+
'Browser, Fly edge, a custom reverse proxy, then code-server.',
|
|
12
|
+
'A hand-rolled tunnel got swapped for http-proxy. Still ten-oh-six.',
|
|
13
|
+
'Yet a Cloudflare tunnel to code-server worked. So the proxy was the bug.',
|
|
14
|
+
"code-server checks the browser's Origin against its own host.",
|
|
15
|
+
"The proxy's changeOrigin rewrote Host to a local port, while Origin kept the public domain.",
|
|
16
|
+
'Mismatch: four-oh-three, which the browser shows as ten-oh-six.',
|
|
17
|
+
'code-server reads X-Forwarded-Host first. The proxy never sent it. The fix: set it to the original Host.',
|
|
18
|
+
'Four-oh-three became one-oh-one. The terminal opened, and ls ran.',
|
|
19
|
+
'The twist: tests without an Origin header passed, and fooled the debugging twice.',
|
|
20
|
+
'One missing header. Fixed in osborn zero point nine point one fifty-three.',
|
|
21
|
+
];
|
|
22
|
+
export const CAPTIONS = SCRIPT.map((s) => s.replace(/ten-oh-six/gi, '1006').replace(/four-oh-three/gi, '403').replace(/one-oh-one/gi, '101')
|
|
23
|
+
.replace('zero point nine point one fifty-three', '0.9.153'));
|
|
24
|
+
const BEFORE = [
|
|
25
|
+
{ n: 320, text: 'const wsProxy = httpProxy.createProxyServer({ target: IDE_TARGET, ws: true, changeOrigin: true })' },
|
|
26
|
+
{ n: '', text: '' },
|
|
27
|
+
{ n: 1610, text: "server.on('upgrade', (req: IncomingMessage, socket, head: Buffer) => {" },
|
|
28
|
+
{ n: '…', text: ' // (path + agent-route checks)', hl: 'dim' },
|
|
29
|
+
{ n: 1613, text: ' if (ideProxyEnabled && hasValidIdeCookie(req) && !isAgentRoute) {' },
|
|
30
|
+
{ n: 1614, text: ' ideLastProxiedActivity = Date.now()' },
|
|
31
|
+
{ n: 1615, text: ' wsProxy.ws(req, socket, head, { target: IDE_TARGET })' },
|
|
32
|
+
{ n: 1616, text: ' return' },
|
|
33
|
+
{ n: 1617, text: ' }' },
|
|
34
|
+
{ n: 1618, text: ' socket.destroy()' },
|
|
35
|
+
{ n: 1619, text: '})' },
|
|
36
|
+
];
|
|
37
|
+
const AFTER = (hl) => [
|
|
38
|
+
{ n: 364, text: 'const wsProxy = httpProxy.createProxyServer({ target: IDE_TARGET, ws: true, changeOrigin: true })', hl: hl ? 'warn' : undefined },
|
|
39
|
+
{ n: '', text: '' },
|
|
40
|
+
{ n: 1813, text: ' if (ideProxyEnabled && hasValidIdeCookie(req) && !isAgentRoute) {' },
|
|
41
|
+
{ n: 1814, text: ' ideLastProxiedActivity = Date.now()' },
|
|
42
|
+
{ n: 1815, text: " // code-server's authenticateOrigin() compares Origin against the effective host" },
|
|
43
|
+
{ n: 1816, text: ' // (X-Forwarded-Host first). changeOrigin:true rewrites Host→127.0.0.1:8300 while the' },
|
|
44
|
+
{ n: 1817, text: " // browser's Origin stays the public hostname, causing a 403 → WS 1006. Preserve the" },
|
|
45
|
+
{ n: 1818, text: ' // real public host so the origin check matches.' },
|
|
46
|
+
{ n: 1819, text: " if (!req.headers['x-forwarded-host'] && req.headers.host) {", hl: hl ? 'add' : undefined },
|
|
47
|
+
{ n: 1820, text: " req.headers['x-forwarded-host'] = req.headers.host", hl: hl ? 'add' : undefined },
|
|
48
|
+
{ n: 1821, text: ' }', hl: hl ? 'add' : undefined },
|
|
49
|
+
{ n: 1822, text: ' wsProxy.ws(req, socket, head, { target: IDE_TARGET })' },
|
|
50
|
+
{ n: 1823, text: ' return' },
|
|
51
|
+
];
|
|
52
|
+
const RED = '#ff7b72', GREEN = '#3fb950', AMBER = '#d29922';
|
|
53
|
+
const T_HEAD = { text: '=== WS host/origin matching tests ===', color: '#8b949e' };
|
|
54
|
+
const T_A = [
|
|
55
|
+
{ text: 'Test A: host=127.0.0.1:8300 (what our proxy sends), origin=fly.dev (what browser sends)' },
|
|
56
|
+
{ text: " Host='127.0.0.1:8300', X-Forwarded-Host=None: HTTP/1.1 403 Forbidden", color: RED, hl: 'bad' },
|
|
57
|
+
];
|
|
58
|
+
const T_B = [
|
|
59
|
+
{ text: '' },
|
|
60
|
+
{ text: 'Test B: host=127.0.0.1:8300, X-Forwarded-Host=<app>.fly.dev (proposed fix)' },
|
|
61
|
+
{ text: " Host='127.0.0.1:8300', X-Forwarded-Host='<app>.fly.dev': HTTP/1.1 101 Switching Protocols", color: GREEN, hl: 'add' },
|
|
62
|
+
];
|
|
63
|
+
const T_EDGE = [{ text: '' }, { n: '#', text: 'after deploy, live over the public Fly edge:' }, { text: 'FLY-EDGE HTTP status: 101', color: GREEN, hl: 'add' }];
|
|
64
|
+
const T_D = [
|
|
65
|
+
{ text: '' },
|
|
66
|
+
{ text: 'Test D: no origin (should pass per code-server)' },
|
|
67
|
+
{ text: " Host='127.0.0.1:8300', X-Forwarded-Host=None: HTTP/1.1 101 Switching Protocols", color: AMBER, hl: 'warn' },
|
|
68
|
+
{ text: '' }, { n: '#', text: 'no Origin header → passes → false "it works"' },
|
|
69
|
+
];
|
|
70
|
+
const LOGTAG = 'REAL LOG · hostname redacted';
|
|
71
|
+
/** name -> svg. */
|
|
72
|
+
export const SCREENS = {
|
|
73
|
+
editor: () => editorScreen(false),
|
|
74
|
+
editorErr: () => editorScreen(true),
|
|
75
|
+
path1: () => pathDiagram(1),
|
|
76
|
+
path2: () => pathDiagram(2),
|
|
77
|
+
before: () => codeScreen('agent/src/index.ts', 'before · 0.9.152 (http-proxy swap, still 1006)', BEFORE),
|
|
78
|
+
beforeHl: () => codeScreen('agent/src/index.ts', 'before · 0.9.152 (http-proxy swap, still 1006)', BEFORE.map((l) => (l.n === 1615 ? { ...l, hl: 'bad' } : l.n === 320 ? { ...l, hl: 'warn' } : l))),
|
|
79
|
+
compare: () => compareDiagram(),
|
|
80
|
+
hdr1: () => headerDiagram(1),
|
|
81
|
+
hdr2: () => headerDiagram(2),
|
|
82
|
+
hdr3: () => headerDiagram(3),
|
|
83
|
+
after: () => codeScreen('agent/src/index.ts', 'after · shipped in 0.9.153', AFTER(false)),
|
|
84
|
+
afterHl: () => codeScreen('agent/src/index.ts', 'after · shipped in 0.9.153', AFTER(true)),
|
|
85
|
+
termA: () => termScreen('verification run · node ws client', [T_HEAD, ...T_A], LOGTAG),
|
|
86
|
+
termB: () => termScreen('verification run · node ws client', [T_HEAD, ...T_A, ...T_B], LOGTAG),
|
|
87
|
+
termEdge: () => termScreen('verification run · node ws client', [T_HEAD, ...T_A, ...T_B, ...T_EDGE], LOGTAG),
|
|
88
|
+
ls: () => lsScreen(),
|
|
89
|
+
termD: () => termScreen('verification run · node ws client', [T_HEAD, ...T_A, ...T_D], LOGTAG),
|
|
90
|
+
end: () => endCard(),
|
|
91
|
+
};
|
|
92
|
+
/** Per narration line: the screens shown during it (split evenly across the line's duration). */
|
|
93
|
+
export const LINE_SCREENS = [
|
|
94
|
+
['editor', 'editorErr'], ['path1', 'path2'], ['before', 'beforeHl'], ['compare'], ['hdr1'], ['hdr2'], ['hdr3'],
|
|
95
|
+
['after', 'afterHl', 'afterHl'], ['termA', 'termB', 'termEdge', 'ls'], ['termD'], ['end'],
|
|
96
|
+
];
|
|
97
|
+
/** Artifact provenance per screen (for the report / shotlist). */
|
|
98
|
+
export const PROVENANCE = {
|
|
99
|
+
editor: 'REENACTMENT (abstract workbench)', editorErr: 'REENACTMENT, modal text verbatim from user screenshot (recall agent-ad1edf68 L119)',
|
|
100
|
+
path1: 'diagram of real topology (Fly edge -> agent :8741 -> code-server 127.0.0.1:8300)', path2: 'same',
|
|
101
|
+
before: 'REAL code: index.ts @ a4388ca^ (0.9.152) lines 320, 1610-1619', beforeHl: 'same, highlighted',
|
|
102
|
+
compare: 'diagram; Cloudflare-worked fact from editor-followups-and-origin-fix.md + code comment line 363',
|
|
103
|
+
hdr1: 'diagram of real headers (root cause in recall + note)', hdr2: 'same', hdr3: 'same',
|
|
104
|
+
after: 'REAL code: current index.ts lines 364, 1813-1823 (fix added in a4388ca)', afterHl: 'same, fix lines highlighted',
|
|
105
|
+
termA: 'REAL log: recall agent-ad1edf68 L98 (Test A)', termB: 'REAL log: Test A+B', termEdge: 'REAL log: + recall agent-a346e43a L36 FLY-EDGE 101',
|
|
106
|
+
ls: 'REENACTMENT: `$ ls` prompt only; outcome user-confirmed (editor-followups note), no output fabricated',
|
|
107
|
+
termD: 'REAL log: Test D (no-Origin false pass)', end: 'end card (fix line verbatim from index.ts 1820)',
|
|
108
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const W = 1080, H = 1920, OW = 1160, OH = 2062;
|
|
2
|
+
type Hl = 'add' | 'bad' | 'warn' | 'dim' | undefined;
|
|
3
|
+
export interface Line {
|
|
4
|
+
n?: number | string;
|
|
5
|
+
text: string;
|
|
6
|
+
hl?: Hl;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Editor-style code panel with optional per-line highlight. */
|
|
10
|
+
export declare function codeScreen(file: string, sub: string, lines: Line[], tag?: string): string;
|
|
11
|
+
/** Terminal panel. Lines with `color` are rendered as-is; `label` lines (n='#') are sans annotations outside the log. */
|
|
12
|
+
export declare function termScreen(title: string, lines: Line[], tag: string, tagColor?: string): string;
|
|
13
|
+
/** Request path diagram. step 1 = first two hops, 2 = all four hops. */
|
|
14
|
+
export declare function pathDiagram(step: number): string;
|
|
15
|
+
/** Cloudflare tunnel (worked) vs. the proxy path (1006). */
|
|
16
|
+
export declare function compareDiagram(): string;
|
|
17
|
+
/** Header drop. step 1 = browser headers, 2 = proxy rewrite, 3 = 403. */
|
|
18
|
+
export declare function headerDiagram(step: number): string;
|
|
19
|
+
/** Reenacted code-server workbench; modal text is verbatim from the user's screenshot. */
|
|
20
|
+
export declare function editorScreen(modal: boolean): string;
|
|
21
|
+
/** Reenacted terminal running ls (user-confirmed outcome; no output is shown because none was logged). */
|
|
22
|
+
export declare function lsScreen(): string;
|
|
23
|
+
export declare function endCard(): string;
|
|
24
|
+
export declare const pngPath: (dir: string, name: string) => string;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-ws-screens.ts — builds the screen-style PNGs for render-ws-story.ts.
|
|
3
|
+
* Every code line / log line here is copied from a real artifact:
|
|
4
|
+
* - code: agent/src/index.ts at a4388ca^ (0.9.152, before) and the current tree (after)
|
|
5
|
+
* - logs: session recall rows (WS host/origin matching tests, FLY-EDGE status), hostname redacted
|
|
6
|
+
* Anything not backed by a real artifact carries a visible REENACTMENT corner tag.
|
|
7
|
+
* Requires FONTCONFIG_FILE pointing at a fonts.conf listing JetBrains Mono + Roboto (set by caller).
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
export const W = 1080, H = 1920, OW = 1160, OH = 2062; // render oversize for pan/zoom headroom
|
|
11
|
+
const MONO = 'JetBrains Mono', SANS = 'Roboto';
|
|
12
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
13
|
+
function wrap(text, max) {
|
|
14
|
+
if (text.length <= max)
|
|
15
|
+
return [text];
|
|
16
|
+
const indent = (text.match(/^\s*/)?.[0] || '') + ' ';
|
|
17
|
+
const out = [];
|
|
18
|
+
let rest = text;
|
|
19
|
+
while (rest.length > max) {
|
|
20
|
+
let cut = rest.lastIndexOf(' ', max);
|
|
21
|
+
if (cut <= indent.length)
|
|
22
|
+
cut = max;
|
|
23
|
+
out.push(rest.slice(0, cut));
|
|
24
|
+
rest = indent + rest.slice(cut).trimStart();
|
|
25
|
+
}
|
|
26
|
+
out.push(rest);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
// Minimal TS syntax colouring -> <tspan>s.
|
|
30
|
+
function colorize(src) {
|
|
31
|
+
if (/^\s*\/\//.test(src))
|
|
32
|
+
return `<tspan fill="#6a9955">${esc(src)}</tspan>`;
|
|
33
|
+
const re = /('[^']*'|`[^`]*`|\b(?:const|if|return|true|new)\b|\b\d[\d.]*\b)/g;
|
|
34
|
+
let out = '', last = 0, m;
|
|
35
|
+
while ((m = re.exec(src))) {
|
|
36
|
+
out += esc(src.slice(last, m.index));
|
|
37
|
+
const t = m[0];
|
|
38
|
+
const c = /^['`]/.test(t) ? '#ce9178' : /^\d/.test(t) ? '#b5cea8' : '#569cd6';
|
|
39
|
+
out += `<tspan fill="${c}">${esc(t)}</tspan>`;
|
|
40
|
+
last = m.index + t.length;
|
|
41
|
+
}
|
|
42
|
+
return out + esc(src.slice(last));
|
|
43
|
+
}
|
|
44
|
+
const HL_FILL = {
|
|
45
|
+
add: 'rgba(46,160,67,0.28)', bad: 'rgba(248,81,73,0.26)', warn: 'rgba(210,153,34,0.28)',
|
|
46
|
+
};
|
|
47
|
+
function chrome(body, opts = {}) {
|
|
48
|
+
const tag = opts.tag
|
|
49
|
+
? `<rect x="${W - 40 - opts.tag.length * 17 - 34}" y="150" width="${opts.tag.length * 17 + 34}" height="50" rx="8" fill="${opts.tagColor || '#d29922'}"/>` +
|
|
50
|
+
`<text x="${W - 55}" y="184" text-anchor="end" font-family="${SANS}" font-weight="500" font-size="26" fill="#0d1117">${esc(opts.tag)}</text>`
|
|
51
|
+
: '';
|
|
52
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${OW}" height="${OH}" viewBox="0 0 ${W} ${H}">` +
|
|
53
|
+
`<defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0d1117"/><stop offset="1" stop-color="#05070a"/></linearGradient></defs>` +
|
|
54
|
+
`<rect width="${W}" height="${H}" fill="url(#g)"/>` +
|
|
55
|
+
`<text x="48" y="110" font-family="${SANS}" font-weight="500" font-size="34" fill="#8b949e">${esc(opts.kicker || 'code-server · WebSocket 1006')}</text>` +
|
|
56
|
+
tag + body + `</svg>`;
|
|
57
|
+
}
|
|
58
|
+
/** Editor-style code panel with optional per-line highlight. */
|
|
59
|
+
export function codeScreen(file, sub, lines, tag) {
|
|
60
|
+
const FS = 28, LH = 46, MAX = 50, x0 = 40, top = 240;
|
|
61
|
+
const rows = [];
|
|
62
|
+
for (const l of lines)
|
|
63
|
+
wrap(l.text, MAX).forEach((t, i) => rows.push({ n: i ? '' : String(l.n ?? ''), t, hl: l.hl }));
|
|
64
|
+
const h = 120 + rows.length * LH + 40;
|
|
65
|
+
let body = `<rect x="${x0}" y="${top}" width="${W - 2 * x0}" height="${h}" rx="18" fill="#1e1e1e" stroke="#30363d" stroke-width="2"/>` +
|
|
66
|
+
[0, 1, 2].map((i) => `<circle cx="${x0 + 36 + i * 32}" cy="${top + 38}" r="10" fill="${['#f85149', '#d29922', '#3fb950'][i]}"/>`).join('') +
|
|
67
|
+
`<text x="${x0 + 140}" y="${top + 47}" font-family="${MONO}" font-size="26" fill="#c9d1d9">${esc(file)}</text>` +
|
|
68
|
+
`<text x="${x0 + 30}" y="${top + 98}" font-family="${SANS}" font-weight="500" font-size="28" fill="#8b949e">${esc(sub)}</text>`;
|
|
69
|
+
rows.forEach((r, i) => {
|
|
70
|
+
const y = top + 150 + i * LH;
|
|
71
|
+
if (r.hl && HL_FILL[r.hl])
|
|
72
|
+
body += `<rect x="${x0 + 2}" y="${y - 34}" width="${W - 2 * x0 - 4}" height="${LH}" fill="${HL_FILL[r.hl]}"/>`;
|
|
73
|
+
if (r.hl === 'add' && r.n)
|
|
74
|
+
body += `<text x="${x0 + 14}" y="${y}" font-family="${MONO}" font-size="${FS}" fill="#3fb950">+</text>`;
|
|
75
|
+
body += `<text x="${x0 + 92}" y="${y}" text-anchor="end" font-family="${MONO}" font-size="22" fill="#6e7681">${esc(r.n)}</text>`;
|
|
76
|
+
body += `<text x="${x0 + 108}" y="${y}" font-family="${MONO}" font-size="${FS}" fill="#d4d4d4" xml:space="preserve"${r.hl === 'dim' ? ' opacity="0.45"' : ''}>${colorize(r.t)}</text>`;
|
|
77
|
+
});
|
|
78
|
+
return chrome(body, { tag });
|
|
79
|
+
}
|
|
80
|
+
/** Terminal panel. Lines with `color` are rendered as-is; `label` lines (n='#') are sans annotations outside the log. */
|
|
81
|
+
export function termScreen(title, lines, tag, tagColor) {
|
|
82
|
+
const FS = 28, LH = 46, MAX = 52, x0 = 40, top = 240;
|
|
83
|
+
const rows = [];
|
|
84
|
+
for (const l of lines) {
|
|
85
|
+
const label = l.n === '#';
|
|
86
|
+
(label ? [l.text] : wrap(l.text, MAX)).forEach((t) => rows.push({ t, c: l.color || '#c9d1d9', hl: l.hl, label }));
|
|
87
|
+
}
|
|
88
|
+
const h = 100 + rows.length * LH + 40;
|
|
89
|
+
let body = `<rect x="${x0}" y="${top}" width="${W - 2 * x0}" height="${h}" rx="18" fill="#010409" stroke="#30363d" stroke-width="2"/>` +
|
|
90
|
+
`<text x="${x0 + 30}" y="${top + 52}" font-family="${MONO}" font-size="24" fill="#8b949e">${esc(title)}</text>`;
|
|
91
|
+
rows.forEach((r, i) => {
|
|
92
|
+
const y = top + 120 + i * LH;
|
|
93
|
+
if (r.hl && HL_FILL[r.hl])
|
|
94
|
+
body += `<rect x="${x0 + 2}" y="${y - 34}" width="${W - 2 * x0 - 4}" height="${LH}" fill="${HL_FILL[r.hl]}"/>`;
|
|
95
|
+
body += r.label
|
|
96
|
+
? `<text x="${x0 + 30}" y="${y}" font-family="${MONO}" font-size="26" fill="#d29922">${esc(r.t)}</text>`
|
|
97
|
+
: `<text x="${x0 + 30}" y="${y}" font-family="${MONO}" font-size="${FS}" fill="${r.c}" xml:space="preserve">${esc(r.t)}</text>`;
|
|
98
|
+
});
|
|
99
|
+
return chrome(body, { tag, tagColor });
|
|
100
|
+
}
|
|
101
|
+
function box(x, y, w, h, title, sub, stroke = '#30363d', fill = '#161b22') {
|
|
102
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="18" fill="${fill}" stroke="${stroke}" stroke-width="3"/>` +
|
|
103
|
+
`<text x="${x + w / 2}" y="${y + h / 2 - 4}" text-anchor="middle" font-family="${SANS}" font-weight="500" font-size="40" fill="#e6edf3">${esc(title)}</text>` +
|
|
104
|
+
`<text x="${x + w / 2}" y="${y + h / 2 + 40}" text-anchor="middle" font-family="${MONO}" font-size="24" fill="#8b949e">${esc(sub)}</text>`;
|
|
105
|
+
}
|
|
106
|
+
function arrow(x, y1, y2, color = '#8b949e', label = '') {
|
|
107
|
+
return `<line x1="${x}" y1="${y1}" x2="${x}" y2="${y2 - 18}" stroke="${color}" stroke-width="5"/>` +
|
|
108
|
+
`<polygon points="${x - 16},${y2 - 22} ${x + 16},${y2 - 22} ${x},${y2}" fill="${color}"/>` +
|
|
109
|
+
(label ? `<text x="${x + 34}" y="${(y1 + y2) / 2 + 10}" font-family="${MONO}" font-size="24" fill="${color}">${esc(label)}</text>` : '');
|
|
110
|
+
}
|
|
111
|
+
const hdr = (x, y, s, c = '#c9d1d9', strike = false) => `<text x="${x}" y="${y}" font-family="${MONO}" font-size="27" fill="${c}"${strike ? ' text-decoration="line-through"' : ''}>${esc(s)}</text>`;
|
|
112
|
+
/** Request path diagram. step 1 = first two hops, 2 = all four hops. */
|
|
113
|
+
export function pathDiagram(step) {
|
|
114
|
+
const X = 190, BW = 700, BH = 170, ys = [250, 530, 810, 1090];
|
|
115
|
+
const nodes = [['Browser', 'Origin: https://<app>.fly.dev'], ['Fly edge', 'public TLS'], ['Agent reverse proxy', 'node http-proxy · :8741'], ['code-server', '127.0.0.1:8300']];
|
|
116
|
+
let b = '';
|
|
117
|
+
nodes.forEach(([t, s], i) => {
|
|
118
|
+
if (step === 1 && i > 1)
|
|
119
|
+
return;
|
|
120
|
+
b += box(X, ys[i], BW, BH, t, s);
|
|
121
|
+
if (i < 3 && !(step === 1 && i >= 1))
|
|
122
|
+
b += arrow(W / 2, ys[i] + BH, ys[i + 1], '#58a6ff', i === 2 ? 'WebSocket upgrade' : '');
|
|
123
|
+
});
|
|
124
|
+
return chrome(b, { kicker: 'the request path' });
|
|
125
|
+
}
|
|
126
|
+
/** Cloudflare tunnel (worked) vs. the proxy path (1006). */
|
|
127
|
+
export function compareDiagram() {
|
|
128
|
+
const row = (y, label, hops, ok) => {
|
|
129
|
+
const c = ok ? '#3fb950' : '#f85149';
|
|
130
|
+
let s = `<text x="60" y="${y}" font-family="${SANS}" font-weight="500" font-size="36" fill="${c}">${esc(label)}</text>`;
|
|
131
|
+
hops.forEach((h, i) => { s += box(60, y + 40 + i * 140, 960, 110, h, '', ok ? '#238636' : '#30363d'); });
|
|
132
|
+
const endY = y + 40 + hops.length * 140;
|
|
133
|
+
s += `<rect x="60" y="${endY}" width="960" height="80" rx="14" fill="${ok ? 'rgba(46,160,67,0.2)' : 'rgba(248,81,73,0.2)'}"/>` +
|
|
134
|
+
`<text x="540" y="${endY + 54}" text-anchor="middle" font-family="${MONO}" font-size="34" fill="${c}">${ok ? 'terminal works' : 'WebSocket close 1006'}</text>`;
|
|
135
|
+
return s;
|
|
136
|
+
};
|
|
137
|
+
return chrome(row(250, 'Cloudflare quick tunnel', ['Browser', 'Cloudflare', 'code-server'], true) +
|
|
138
|
+
row(830, 'Own reverse proxy', ['Browser', 'Fly edge', 'Agent proxy', 'code-server'], false), { kicker: 'same code-server, two paths' });
|
|
139
|
+
}
|
|
140
|
+
/** Header drop. step 1 = browser headers, 2 = proxy rewrite, 3 = 403. */
|
|
141
|
+
export function headerDiagram(step) {
|
|
142
|
+
let b = box(60, 240, 960, 110, 'Browser sends', '') +
|
|
143
|
+
hdr(100, 410, 'Origin: https://<app>.fly.dev', '#58a6ff') + hdr(100, 455, 'Host: <app>.fly.dev');
|
|
144
|
+
if (step >= 2) {
|
|
145
|
+
b += arrow(W / 2, 480, 580, '#d29922', 'changeOrigin: true') + box(60, 580, 960, 110, 'Proxy forwards', '', '#d29922') +
|
|
146
|
+
hdr(100, 750, 'Origin: https://<app>.fly.dev', '#58a6ff') + hdr(100, 795, 'Host: 127.0.0.1:8300', '#d29922') +
|
|
147
|
+
hdr(100, 840, 'X-Forwarded-Host: (never sent)', '#f85149', true);
|
|
148
|
+
}
|
|
149
|
+
if (step >= 3) {
|
|
150
|
+
b += arrow(W / 2, 870, 970, '#f85149') + box(60, 970, 960, 140, 'code-server', 'authenticateOrigin()', '#f85149') +
|
|
151
|
+
`<text x="540" y="1200" text-anchor="middle" font-family="${MONO}" font-size="30" fill="#e6edf3"><app>.fly.dev ≠ 127.0.0.1:8300</text>` +
|
|
152
|
+
`<rect x="290" y="1240" width="500" height="110" rx="16" fill="#f85149"/>` +
|
|
153
|
+
`<text x="540" y="1313" text-anchor="middle" font-family="${MONO}" font-weight="700" font-size="54" fill="#0d1117">403 → 1006</text>`;
|
|
154
|
+
}
|
|
155
|
+
return chrome(b, { kicker: 'why the socket dies' });
|
|
156
|
+
}
|
|
157
|
+
/** Reenacted code-server workbench; modal text is verbatim from the user's screenshot. */
|
|
158
|
+
export function editorScreen(modal) {
|
|
159
|
+
let b = `<rect x="40" y="240" width="1000" height="1150" rx="14" fill="#1e1e1e" stroke="#30363d" stroke-width="2"/>` +
|
|
160
|
+
`<rect x="40" y="240" width="70" height="1150" fill="#2c2c2c"/><rect x="110" y="240" width="260" height="1150" fill="#252526"/>` +
|
|
161
|
+
`<text x="130" y="290" font-family="${SANS}" font-size="22" fill="#8b949e">EXPLORER</text>` +
|
|
162
|
+
`<rect x="370" y="1000" width="670" height="390" fill="#181818"/><text x="390" y="1040" font-family="${SANS}" font-size="22" fill="#8b949e">TERMINAL</text>`;
|
|
163
|
+
for (let i = 0; i < 9; i++)
|
|
164
|
+
b += `<rect x="130" y="${320 + i * 46}" width="${120 + ((i * 53) % 110)}" height="18" rx="9" fill="#3a3a3a"/>`;
|
|
165
|
+
for (let i = 0; i < 12; i++)
|
|
166
|
+
b += `<rect x="400" y="${300 + i * 50}" width="${160 + ((i * 97) % 420)}" height="18" rx="9" fill="#333"/>`;
|
|
167
|
+
if (modal) {
|
|
168
|
+
b += `<rect x="40" y="240" width="1000" height="1150" fill="rgba(0,0,0,0.55)"/>` +
|
|
169
|
+
`<rect x="90" y="560" width="900" height="470" rx="16" fill="#252526" stroke="#f85149" stroke-width="3"/>`;
|
|
170
|
+
const t = ['An unexpected error occurred that requires a', 'reload of this page.', '', 'The workbench failed to connect to the server', '(Error: WebSocket close with status code 1006)'];
|
|
171
|
+
t.forEach((s, i) => { b += `<text x="130" y="${640 + i * 50}" font-family="${SANS}" font-size="${i > 2 ? 32 : 30}" fill="${i === 4 ? '#ff7b72' : '#e6edf3'}">${esc(s)}</text>`; });
|
|
172
|
+
b += `<rect x="770" y="930" width="180" height="64" rx="8" fill="#0e639c"/><text x="860" y="972" text-anchor="middle" font-family="${SANS}" font-size="28" fill="#fff">Reload</text>`;
|
|
173
|
+
}
|
|
174
|
+
return chrome(b, { tag: modal ? 'REENACTMENT · error text verbatim' : 'REENACTMENT', kicker: 'browser code editor (code-server)' });
|
|
175
|
+
}
|
|
176
|
+
/** Reenacted terminal running ls (user-confirmed outcome; no output is shown because none was logged). */
|
|
177
|
+
export function lsScreen() {
|
|
178
|
+
return termScreen('code-server terminal', [{ text: '$ ls', color: '#3fb950' }, { text: '_', color: '#c9d1d9' }], 'REENACTMENT · outcome user-confirmed');
|
|
179
|
+
}
|
|
180
|
+
export function endCard() {
|
|
181
|
+
const b = `<text x="540" y="560" text-anchor="middle" font-family="${SANS}" font-weight="500" font-size="88" fill="#e6edf3">One missing header.</text>` +
|
|
182
|
+
`<rect x="60" y="680" width="960" height="150" rx="16" fill="#1e1e1e" stroke="#3fb950" stroke-width="3"/>` +
|
|
183
|
+
`<text x="540" y="745" text-anchor="middle" font-family="${MONO}" font-size="30" fill="#ce9178">req.headers['x-forwarded-host']</text>` +
|
|
184
|
+
`<text x="540" y="795" text-anchor="middle" font-family="${MONO}" font-size="30" fill="#d4d4d4">= req.headers.host</text>` +
|
|
185
|
+
`<text x="540" y="990" text-anchor="middle" font-family="${MONO}" font-weight="700" font-size="96"><tspan fill="#f85149">403</tspan><tspan fill="#8b949e"> → </tspan><tspan fill="#3fb950">101</tspan></text>` +
|
|
186
|
+
`<text x="540" y="1130" text-anchor="middle" font-family="${SANS}" font-weight="500" font-size="40" fill="#8b949e">fixed in osborn 0.9.153</text>`;
|
|
187
|
+
return chrome(b, { kicker: 'code-server behind a reverse proxy' });
|
|
188
|
+
}
|
|
189
|
+
export const pngPath = (dir, name) => path.join(dir, 'screens', `${name}.png`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|