viewable-one 0.0.0-stage → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1460 -0
- package/LICENSE +21 -0
- package/README.md +755 -2
- package/SPAWN.md +2105 -0
- package/glytch.mjs +517 -0
- package/lib/acp.mjs +978 -0
- package/lib/admission.mjs +105 -0
- package/lib/agent-selection.mjs +123 -0
- package/lib/agents.mjs +913 -0
- package/lib/atomic.mjs +96 -0
- package/lib/auth.mjs +1387 -0
- package/lib/authenticatedHttp.mjs +68 -0
- package/lib/boardside.mjs +225 -0
- package/lib/child.mjs +1252 -0
- package/lib/cli-output.mjs +176 -0
- package/lib/cloudflared-release.json +25 -0
- package/lib/cloudflared.mjs +81 -0
- package/lib/codex-rpc.mjs +92 -0
- package/lib/codex.mjs +747 -0
- package/lib/codexApps.mjs +332 -0
- package/lib/codexPlanWindow.mjs +84 -0
- package/lib/codexToolInputs.mjs +139 -0
- package/lib/codingChecks.mjs +55 -0
- package/lib/compose.mjs +76 -0
- package/lib/computerApps.mjs +110 -0
- package/lib/conversationRules.mjs +45 -0
- package/lib/detect.mjs +605 -0
- package/lib/devserver.mjs +659 -0
- package/lib/digest.mjs +290 -0
- package/lib/doctor.mjs +464 -0
- package/lib/exitClass.mjs +64 -0
- package/lib/glytchFolder.mjs +105 -0
- package/lib/hosted.mjs +112 -0
- package/lib/imageAttachments.mjs +31 -0
- package/lib/init-connect.mjs +418 -0
- package/lib/kill.mjs +104 -0
- package/lib/logindaemon.mjs +976 -0
- package/lib/logs.mjs +120 -0
- package/lib/mcpjson.mjs +242 -0
- package/lib/memberPreview.mjs +165 -0
- package/lib/multiwatch.mjs +428 -0
- package/lib/pagePictures.mjs +396 -0
- package/lib/pageRoutes.mjs +67 -0
- package/lib/paths.mjs +9 -0
- package/lib/permissionAsk.mjs +418 -0
- package/lib/picker.mjs +300 -0
- package/lib/platform.mjs +60 -0
- package/lib/previewGateway.mjs +129 -0
- package/lib/previewTunnel.mjs +85 -0
- package/lib/procs.mjs +428 -0
- package/lib/project-mcp.mjs +288 -0
- package/lib/projectPaths.mjs +31 -0
- package/lib/refusal.mjs +70 -0
- package/lib/registry.mjs +204 -0
- package/lib/repomap.mjs +322 -0
- package/lib/restartPass.mjs +151 -0
- package/lib/retired-address.mjs +78 -0
- package/lib/rowsAnswers.mjs +389 -0
- package/lib/rowsEvents.mjs +86 -0
- package/lib/run-engine.mjs +16 -0
- package/lib/runner-health.mjs +57 -0
- package/lib/runner.mjs +1569 -0
- package/lib/runnerAdmission.mjs +35 -0
- package/lib/runnerApi.mjs +312 -0
- package/lib/runnerCompletion.mjs +406 -0
- package/lib/runnerLease.mjs +37 -0
- package/lib/runnerMaintenance.mjs +27 -0
- package/lib/runnerPace.mjs +283 -0
- package/lib/runnerPerformance.mjs +26 -0
- package/lib/runnerRealtime.mjs +340 -0
- package/lib/runnerRegister.mjs +96 -0
- package/lib/runnerSettings.mjs +40 -0
- package/lib/runnerSettlement.mjs +138 -0
- package/lib/runnerWorkspace.mjs +162 -0
- package/lib/serverSweep.mjs +90 -0
- package/lib/sharedPreview.mjs +292 -0
- package/lib/signIn.mjs +47 -0
- package/lib/spawn.mjs +330 -0
- package/lib/sweep.mjs +161 -0
- package/lib/sync.mjs +484 -0
- package/lib/uninstall-project.mjs +588 -0
- package/lib/uninstall.mjs +457 -0
- package/lib/update.mjs +302 -0
- package/lib/usageLimit.mjs +108 -0
- package/lib/windows-command.mjs +85 -0
- package/lib/worktree.mjs +1138 -0
- package/lib/writePolicy.mjs +45 -0
- package/package.json +58 -4
- package/spawn-runner.mjs +167 -0
package/lib/digest.mjs
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Shaping the mirror digest before a session pays for it.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS (2026-08-31, from a spend audit over 201 transcripts and
|
|
4
|
+
// 26,841 API requests): `get_board` returned the whole 48KB digest - 201
|
|
5
|
+
// nodes, roughly 12k tokens - and that text then rode along in context for
|
|
6
|
+
// every remaining request of the session. Measured cost: $0.50 per call,
|
|
7
|
+
// $52-71 over nine days, about 60% of everything the board bridge cost. The
|
|
8
|
+
// call frequency was already fine (107 of 137 board-using sessions called it
|
|
9
|
+
// exactly ONCE), so the fix is not "call it less". It is "return less".
|
|
10
|
+
//
|
|
11
|
+
// The digest's own format is the input here. `src/mirror.ts` writes the file
|
|
12
|
+
// and `src/digest/boardDigest.ts` renders its node lines; this module only
|
|
13
|
+
// reads what they wrote. One kind carries the work: `[idea]`. An idea's open
|
|
14
|
+
// and answered questions are NOTES on it, so they appear twice: as indented
|
|
15
|
+
// `asked:` lines under the idea (kept with `detail:true`) and as one row each
|
|
16
|
+
// in the QUESTIONS block, `- <noteId> open|answered ... on=<ideaId> "..."`,
|
|
17
|
+
// which the slim view always keeps whole (the note id is what check_answer
|
|
18
|
+
// reads, the idea id says where it sits). That makes it a PARSER OF SOMEONE ELSE'S OUTPUT, so every
|
|
19
|
+
// rule below fails toward passing the line through untouched: a format change
|
|
20
|
+
// upstream costs the saving, never the content. If a line does not look like
|
|
21
|
+
// what we expect, it survives whole.
|
|
22
|
+
//
|
|
23
|
+
// What the slim view drops, and why each is safe to drop:
|
|
24
|
+
// - geometry, `(x,y) WxH` on every line. Added 2026-08-25 because a model
|
|
25
|
+
// asked to TIDY the board needs extents to see overlap - true, and that
|
|
26
|
+
// is why `view:"full"` and `geometry:true` still carry it. But the other
|
|
27
|
+
// ~200 sessions were reading the board to find out what was decided, and
|
|
28
|
+
// paying for pixel coordinates to do it.
|
|
29
|
+
// - the body preview after `"Title":`. boardView truncates content to 150
|
|
30
|
+
// chars, and the title is usually the first words of that same content
|
|
31
|
+
// ("Structured data formats" then "Structured data formats - JSON, XML,
|
|
32
|
+
// …"). Near-duplicate text at roughly 2/3 of the node line's length.
|
|
33
|
+
// `get_node` still returns any idea in full.
|
|
34
|
+
// - the AGES block, which repeats every node id a second time to attach a
|
|
35
|
+
// number. Slim folds the age onto the node's own line instead, so the
|
|
36
|
+
// information survives and the second copy of 201 ids does not.
|
|
37
|
+
// - group `members: [...]` lists, a third copy of the same ids - every node
|
|
38
|
+
// line already says `group=`.
|
|
39
|
+
//
|
|
40
|
+
// Nothing is ever dropped silently. Every response ends with a line saying
|
|
41
|
+
// what was omitted and which argument brings it back.
|
|
42
|
+
|
|
43
|
+
/** Section markers, exactly as src/mirror.ts and boardView.ts emit them. */
|
|
44
|
+
const IS_GROUPS = (line) => line === "GROUPS:";
|
|
45
|
+
const IS_NODES = (line) => line === "NODES:";
|
|
46
|
+
const IS_EDGES = (line) => line.startsWith("EDGES: ");
|
|
47
|
+
const IS_QUESTIONS = (line) => line.startsWith("QUESTIONS (");
|
|
48
|
+
// "BOARD FLAGS" since 2026-10-05, when the Subconscious row left and the
|
|
49
|
+
// Board Agent took the scan over; the old spelling still matches, because a
|
|
50
|
+
// digest written before that day is still on disk in linked folders.
|
|
51
|
+
const IS_FLAGS = (line) => line.startsWith("BOARD FLAGS") || line.startsWith("SUBCONSCIOUS FLAGS");
|
|
52
|
+
const IS_AGES = (line) => line.startsWith("AGES (");
|
|
53
|
+
|
|
54
|
+
/** ` (x,y)` and its optional ` WxH`, wherever boardView put them. */
|
|
55
|
+
const GEOMETRY = /\s\(-?\d+,-?\d+\)(?:\s\d+x\d+)?/;
|
|
56
|
+
|
|
57
|
+
/** The trailing facts on a node line that are NOT body text. When the body
|
|
58
|
+
* preview goes, these come back - they are state a session acts on. */
|
|
59
|
+
const META =
|
|
60
|
+
/\s+comments=\d+|\s+notes=\d+|\s+tags=\[[^\]]*\]|\s+hidden\b|\s+(?:result|summary|error)="(?:[^"\\]|\\.)*"|\s+PROPOSED-[A-Z]+\([^)]*\)/g;
|
|
61
|
+
|
|
62
|
+
/** `- <id> [<kind>] …` - the first token of a node or group entry. */
|
|
63
|
+
const ENTRY_ID = /^-\s+(\S+)/;
|
|
64
|
+
const ENTRY_KIND = /^-\s+\S+\s+\[([^\]]+)\]/;
|
|
65
|
+
const ENTRY_DRAWN = /^-\s+\S+\s+\[[^\]]+\] \[drawn\]/;
|
|
66
|
+
const ENTRY_GROUP = /\sgroup=(\S+)/;
|
|
67
|
+
const ENTRY_TAGS = /\stags=\[([^\]]*)\]/;
|
|
68
|
+
|
|
69
|
+
/** Everything after the FIRST `"quoted title": ` is body preview. A line with
|
|
70
|
+
* no such shape (a picture box or a brief line, say) is left exactly as it was. */
|
|
71
|
+
function stripBody(line) {
|
|
72
|
+
const match = line.match(/^(.*?"(?:[^"\\]|\\.)*"):\s/);
|
|
73
|
+
if (!match) return line;
|
|
74
|
+
const tail = line.slice(match[0].length);
|
|
75
|
+
const meta = tail.match(META) ?? [];
|
|
76
|
+
return match[1] + meta.join("");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `id=3d, id=0d, …` from the AGES line into a map. */
|
|
80
|
+
function parseAges(line) {
|
|
81
|
+
const ages = new Map();
|
|
82
|
+
const body = line.slice(line.indexOf("):") + 2);
|
|
83
|
+
for (const pair of body.split(",")) {
|
|
84
|
+
const match = pair.trim().match(/^(\S+)=(\d+)d$/);
|
|
85
|
+
if (match) ages.set(match[1], Number(match[2]));
|
|
86
|
+
}
|
|
87
|
+
return ages;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Split the raw digest into the parts we shape separately. Anything we do
|
|
91
|
+
* not recognise lands in `rest` and is reprinted verbatim, in order. */
|
|
92
|
+
function parse(text) {
|
|
93
|
+
const out = {
|
|
94
|
+
preamble: [],
|
|
95
|
+
groups: [],
|
|
96
|
+
nodes: [],
|
|
97
|
+
edges: [],
|
|
98
|
+
questions: [],
|
|
99
|
+
flags: [],
|
|
100
|
+
ages: null,
|
|
101
|
+
rest: [],
|
|
102
|
+
hadNodes: false,
|
|
103
|
+
};
|
|
104
|
+
let section = "preamble";
|
|
105
|
+
for (const line of text.split("\n")) {
|
|
106
|
+
if (IS_GROUPS(line)) {
|
|
107
|
+
section = "groups";
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (IS_NODES(line)) {
|
|
111
|
+
section = "nodes";
|
|
112
|
+
out.hadNodes = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (IS_EDGES(line)) {
|
|
116
|
+
out.edges.push(line);
|
|
117
|
+
section = "after";
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (IS_QUESTIONS(line)) {
|
|
121
|
+
out.questions.push(line);
|
|
122
|
+
section = "questions";
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (IS_FLAGS(line)) {
|
|
126
|
+
out.flags.push(line);
|
|
127
|
+
section = "flags";
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (IS_AGES(line)) {
|
|
131
|
+
out.ages = line;
|
|
132
|
+
section = "after";
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (section === "groups" || section === "nodes") {
|
|
137
|
+
const list = section === "groups" ? out.groups : out.nodes;
|
|
138
|
+
if (line.startsWith("- ")) {
|
|
139
|
+
list.push({ head: line, extra: [] });
|
|
140
|
+
} else if (line.trim() === "") {
|
|
141
|
+
// Layout. The renderer below puts the blanks back where it wants them.
|
|
142
|
+
} else if (/^\s/.test(line) && list.length > 0) {
|
|
143
|
+
// INDENTED: boardView's multi-line kinds - one-at-a-time items,
|
|
144
|
+
// comparison rows, lesson steps - hang off the entry above them.
|
|
145
|
+
list[list.length - 1].extra.push(line);
|
|
146
|
+
} else {
|
|
147
|
+
// Neither an entry nor an indented continuation, so it is something
|
|
148
|
+
// this parser has never seen: a section the renderer grew after this
|
|
149
|
+
// module was written. Reprint it whole. Guessing at it would cost
|
|
150
|
+
// content; passing it through only costs the saving.
|
|
151
|
+
out.rest.push(line);
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (section === "questions") out.questions.push(line);
|
|
156
|
+
else if (section === "flags") out.flags.push(line);
|
|
157
|
+
else if (section === "preamble") out.preamble.push(line);
|
|
158
|
+
else out.questions.push(line); // trailing blanks after EDGES/AGES
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const entryId = (entry) => entry.head.match(ENTRY_ID)?.[1] ?? "";
|
|
164
|
+
const entryKind = (entry) => entry.head.match(ENTRY_KIND)?.[1] ?? "";
|
|
165
|
+
const entryGroup = (entry) => entry.head.match(ENTRY_GROUP)?.[1] ?? "";
|
|
166
|
+
const entryTags = (entry) =>
|
|
167
|
+
(entry.head.match(ENTRY_TAGS)?.[1] ?? "")
|
|
168
|
+
.split(",")
|
|
169
|
+
.map((t) => t.trim())
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Shape one raw digest for one call.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} text the mirror file, exactly as the app wrote it
|
|
176
|
+
* @param {object} opts
|
|
177
|
+
* view "slim" (default) | "full" - "full" returns `text` untouched
|
|
178
|
+
* except for filters, so the old behaviour is one argument away
|
|
179
|
+
* geometry true to keep (x,y) WxH in the slim view (for tidying work)
|
|
180
|
+
* detail true to keep body previews and multi-line node contents
|
|
181
|
+
* tag keep only nodes carrying this tag
|
|
182
|
+
* group keep only nodes in this group id
|
|
183
|
+
* kind keep only nodes of this kind ("idea", "doc", "note", …)
|
|
184
|
+
* since_days keep only nodes touched within N days
|
|
185
|
+
* drawn keep only nodes carrying a drawing - the writer marks them
|
|
186
|
+
* "[drawn]" after the kind (brief 32 Move 1, 2026-09-12)
|
|
187
|
+
* match keep only nodes whose line contains this (case-insensitive)
|
|
188
|
+
* limit cap the node lines returned
|
|
189
|
+
* @returns {{text: string, stats: object}}
|
|
190
|
+
*/
|
|
191
|
+
export function shapeDigest(text, opts = {}) {
|
|
192
|
+
const full = opts.view === "full";
|
|
193
|
+
const keepGeometry = full || opts.geometry === true;
|
|
194
|
+
const keepDetail = full || opts.detail === true;
|
|
195
|
+
|
|
196
|
+
const parsed = parse(text);
|
|
197
|
+
const ages = parsed.ages ? parseAges(parsed.ages) : new Map();
|
|
198
|
+
const total = parsed.nodes.length;
|
|
199
|
+
|
|
200
|
+
const wanted = [];
|
|
201
|
+
for (const entry of parsed.nodes) {
|
|
202
|
+
const id = entryId(entry);
|
|
203
|
+
if (opts.tag && !entryTags(entry).includes(opts.tag)) continue;
|
|
204
|
+
if (opts.group && entryGroup(entry) !== opts.group) continue;
|
|
205
|
+
if (opts.kind && entryKind(entry) !== opts.kind) continue;
|
|
206
|
+
if (opts.drawn === true && !ENTRY_DRAWN.test(entry.head)) continue;
|
|
207
|
+
if (opts.match && !entry.head.toLowerCase().includes(String(opts.match).toLowerCase())) continue;
|
|
208
|
+
if (typeof opts.since_days === "number") {
|
|
209
|
+
const age = ages.get(id);
|
|
210
|
+
// A node with no age predates the attention log. Age filters are asked
|
|
211
|
+
// for to find RECENT work, so an unknown age is not recent.
|
|
212
|
+
if (age === undefined || age > opts.since_days) continue;
|
|
213
|
+
}
|
|
214
|
+
wanted.push(entry);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const limit = typeof opts.limit === "number" && opts.limit > 0 ? opts.limit : undefined;
|
|
218
|
+
const shown = limit === undefined ? wanted : wanted.slice(0, limit);
|
|
219
|
+
const droppedByLimit = wanted.length - shown.length;
|
|
220
|
+
|
|
221
|
+
const renderNode = (entry) => {
|
|
222
|
+
let head = entry.head;
|
|
223
|
+
if (!keepDetail) head = stripBody(head);
|
|
224
|
+
if (!keepGeometry) head = head.replace(GEOMETRY, "");
|
|
225
|
+
if (!full) {
|
|
226
|
+
const age = ages.get(entryId(entry));
|
|
227
|
+
if (age !== undefined) head += ` ${age}d`;
|
|
228
|
+
}
|
|
229
|
+
return keepDetail ? [head, ...entry.extra] : [head];
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const renderGroup = (entry) => {
|
|
233
|
+
let head = entry.head;
|
|
234
|
+
if (!keepGeometry) head = head.replace(GEOMETRY, "");
|
|
235
|
+
if (!keepDetail) head = head.replace(/\smembers:\s\[[^\]]*\]/, "");
|
|
236
|
+
if (!full) {
|
|
237
|
+
const age = ages.get(entryId(entry));
|
|
238
|
+
if (age !== undefined) head += ` ${age}d`;
|
|
239
|
+
}
|
|
240
|
+
return head;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const omitted = [];
|
|
244
|
+
if (!keepGeometry) omitted.push("positions and sizes (geometry:true)");
|
|
245
|
+
if (!keepDetail) omitted.push("body previews and group member lists (detail:true)");
|
|
246
|
+
if (!full && parsed.ages) omitted.push("the AGES block, folded onto each line as Nd");
|
|
247
|
+
|
|
248
|
+
const lines = [...parsed.preamble];
|
|
249
|
+
if (parsed.groups.length > 0) {
|
|
250
|
+
lines.push("GROUPS:");
|
|
251
|
+
for (const entry of parsed.groups) lines.push(renderGroup(entry));
|
|
252
|
+
}
|
|
253
|
+
// Only if the digest had one. A mirror with no NODES section (an empty
|
|
254
|
+
// board, or a test fixture) must not grow a header it never wrote.
|
|
255
|
+
if (parsed.hadNodes) lines.push("NODES:");
|
|
256
|
+
for (const entry of shown) lines.push(...renderNode(entry));
|
|
257
|
+
lines.push(...parsed.rest);
|
|
258
|
+
lines.push(...parsed.edges);
|
|
259
|
+
if (parsed.questions.length > 0) lines.push("", ...parsed.questions);
|
|
260
|
+
if (parsed.flags.length > 0) lines.push("", ...parsed.flags);
|
|
261
|
+
if (full && parsed.ages) lines.push(parsed.ages);
|
|
262
|
+
|
|
263
|
+
// The footer is not decoration. A filtered board that does not say it is
|
|
264
|
+
// filtered is a board that lies to the next session about what is on it.
|
|
265
|
+
const filters = [];
|
|
266
|
+
if (opts.tag) filters.push(`tag=${opts.tag}`);
|
|
267
|
+
if (opts.group) filters.push(`group=${opts.group}`);
|
|
268
|
+
if (opts.kind) filters.push(`kind=${opts.kind}`);
|
|
269
|
+
if (opts.drawn === true) filters.push("drawn");
|
|
270
|
+
if (typeof opts.since_days === "number") filters.push(`since_days=${opts.since_days}`);
|
|
271
|
+
if (opts.match) filters.push(`match=${JSON.stringify(opts.match)}`);
|
|
272
|
+
if (limit !== undefined) filters.push(`limit=${limit}`);
|
|
273
|
+
|
|
274
|
+
const note = [
|
|
275
|
+
`[${full ? "full" : "slim"} view: ${shown.length} of ${total} nodes`,
|
|
276
|
+
filters.length > 0 ? ` (${filters.join(", ")})` : "",
|
|
277
|
+
droppedByLimit > 0
|
|
278
|
+
? `; ${droppedByLimit} more matched and were NOT shown - raise limit or narrow the filter`
|
|
279
|
+
: "",
|
|
280
|
+
omitted.length > 0 ? `. Omitted: ${omitted.join("; ")}` : "",
|
|
281
|
+
`. get_node(id) returns any idea in full; view:"full" returns the whole digest.]`,
|
|
282
|
+
].join("");
|
|
283
|
+
lines.push("", note);
|
|
284
|
+
|
|
285
|
+
const out = lines.join("\n");
|
|
286
|
+
return {
|
|
287
|
+
text: out,
|
|
288
|
+
stats: { total, shown: shown.length, bytesIn: text.length, bytesOut: out.length },
|
|
289
|
+
};
|
|
290
|
+
}
|