geneseed 3.1.3 → 3.3.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/CHANGELOG.md +180 -0
- package/README.md +24 -14
- package/SETUP.md +38 -7
- package/SHIPPED.md +6 -5
- package/adapters/bob/README.md +40 -10
- package/adapters/claude-code/README.md +5 -0
- package/adapters/claude-code/settings.json +8 -1
- package/adapters/copilot/README.md +50 -24
- package/adapters/opencode/README.md +5 -1
- package/adapters/opencode/plugins/geneseed-guard.js +40 -6
- package/bin/build-driver.mjs +143 -89
- package/bin/geneseed-cli.mjs +63 -106
- package/bin/geneseed-hook.mjs +26 -16
- package/docs/candidate-hosts.md +74 -0
- package/docs/extending.md +6 -5
- package/docs/token-footprint.md +32 -21
- package/docs/web-ui.md +5 -13
- package/harness.config.json +1 -1
- package/js/README.md +5 -15
- package/js/build/emit-claude.mjs +26 -12
- package/js/build/emit-common.mjs +63 -114
- package/js/build/emit-opencode.mjs +74 -66
- package/js/build/generate.mjs +4 -6
- package/js/build/render.mjs +50 -32
- package/js/build/source.mjs +29 -8
- package/js/build/stubs.mjs +25 -33
- package/js/build/themes.mjs +1 -1
- package/js/build/version.mjs +1 -3
- package/js/cli-table.json +84 -0
- package/js/hosts/hooks.mjs +243 -81
- package/js/hosts/hosts.mjs +57 -5
- package/js/hosts/installs.mjs +35 -28
- package/js/hosts/link.mjs +2 -2
- package/js/hosts/mcp.mjs +3 -1
- package/js/hosts/native.mjs +54 -19
- package/js/hosts/opencode.mjs +39 -28
- package/js/hosts/settings.mjs +206 -115
- package/js/inspect/checks-authoring.mjs +5 -4
- package/js/inspect/checks-build.mjs +1 -1
- package/js/inspect/diff.mjs +1 -3
- package/js/inspect/doctor.mjs +59 -44
- package/js/inspect/excludes.mjs +5 -11
- package/js/inspect/inventory.mjs +38 -56
- package/js/inspect/registry.mjs +4 -7
- package/js/inspect/scan.mjs +8 -15
- package/js/inspect/status.mjs +51 -3
- package/js/lib/fs.mjs +34 -0
- package/js/lib/json.mjs +28 -1
- package/js/lib/paths.mjs +0 -10
- package/js/lib/udiff.mjs +11 -16
- package/js/maintain/memory.mjs +2 -4
- package/js/maintain/migrate.mjs +3 -3
- package/js/maintain/uninstall.mjs +127 -88
- package/js/maintain/update.mjs +4 -4
- package/js/ui/anim.mjs +1 -1
- package/js/ui/menu.mjs +1 -1
- package/js/ui/tui.mjs +3 -20
- package/js/web/actions.mjs +230 -217
- package/js/web/activity.mjs +71 -89
- package/js/web/api.mjs +130 -226
- package/js/web/daemon.mjs +92 -110
- package/js/web/docs.mjs +59 -126
- package/js/web/handler.mjs +94 -123
- package/js/web/jobs.mjs +116 -178
- package/js/web/routes.mjs +47 -192
- package/js/web/server.mjs +47 -82
- package/package.json +1 -1
- package/registry.json +14 -0
- package/src/AGENT.md.tmpl +55 -16
- package/src/agents/explorer.md +2 -1
- package/src/doctrines/README.md +3 -3
- package/src/doctrines/process.md +4 -2
- package/src/doctrines/rigor.md +13 -0
- package/src/laws/universal.md +133 -43
- package/src/ontology/universal.md +38 -13
- package/src/skills/_template.md +5 -1
- package/src/skills/bruno-collection-generator.md +63 -0
- package/src/skills/bruno-test-writer.md +62 -0
- package/src/skills/clarify.md +1 -1
- package/src/skills/crash-course.md +1 -0
- package/src/skills/decode.md +1 -0
- package/src/skills/drill.md +1 -0
- package/src/skills/feynman.md +1 -0
- package/src/skills/forge-mcp.md +2 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/herdr.md +1 -0
- package/src/skills/ingest.md +2 -1
- package/src/skills/learning-path.md +1 -0
- package/src/skills/opencode-theme.md +1 -0
- package/src/skills/parallel-agents.md +1 -1
- package/src/skills/ponytail.md +2 -2
- package/src/skills/research.md +4 -2
- package/themes/_TEMPLATE.json +5 -0
- package/themes/biker.json +5 -0
- package/themes/commentator.json +5 -0
- package/themes/cyberpunk.json +5 -0
- package/themes/gamer.json +5 -0
- package/themes/imperial.json +5 -0
- package/themes/joker.json +5 -0
- package/themes/marvin.json +5 -0
- package/themes/mean.json +5 -0
- package/themes/military.json +5 -0
- package/themes/neutral.json +5 -0
- package/themes/pirate.json +5 -0
- package/themes/sports.json +5 -0
- package/themes/verstappen.json +5 -0
- package/themes/wizard.json +5 -0
- package/web/dist/assets/Activity-BfVcLrda.js +1 -0
- package/web/dist/assets/ActivityDetail-CQXccGPl.js +1 -0
- package/web/dist/assets/CatalogRow-DALEQImZ.js +1 -0
- package/web/dist/assets/Diff-BIjxXx46.js +5 -0
- package/web/dist/assets/Doctor-CJLbgsrw.js +1 -0
- package/web/dist/assets/FilterInput-CkUCk_lY.js +1 -0
- package/web/dist/assets/GreenhouseView-DfC5IILJ.js +2 -0
- package/web/dist/assets/Harness-Dm5CCU-v.js +1 -0
- package/web/dist/assets/JournalView-Cpu2eI4s.js +1 -0
- package/web/dist/assets/Laws-DDDVFuQz.js +3 -0
- package/web/dist/assets/Library-Dao3T9na.js +1 -0
- package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-C__nuWX5.js} +15 -15
- package/web/dist/assets/OperatorHudView-DEpCtAjP.js +2 -0
- package/web/dist/assets/Profile-BlXtUlh2.js +1 -0
- package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-DIre6gxR.js} +1 -1
- package/web/dist/assets/Skills-rY13ZPhM.js +1 -0
- package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-wW_RS_my.js} +1 -1
- package/web/dist/assets/{activity-Cc0n2jwx.js → activity-DpYPKNrA.js} +1 -1
- package/web/dist/assets/index-BEZvPvpF.js +1 -0
- package/web/dist/assets/index-DNQE5ydy.js +10 -0
- package/web/dist/assets/index-DzdQjvM1.css +1 -0
- package/web/dist/assets/index-PmQGRS1X.js +2 -0
- package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
- package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +119 -130
- package/js/web/graph.mjs +0 -0
- package/web/dist/assets/Activity-B49fE_ug.js +0 -1
- package/web/dist/assets/ActivityDetail-abt7hzD2.js +0 -1
- package/web/dist/assets/Diff-D3_bVkih.js +0 -5
- package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
- package/web/dist/assets/Graph-DvYMtlvl.js +0 -1
- package/web/dist/assets/Harness-BIWei7N5.js +0 -1
- package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
- package/web/dist/assets/Library-MjbOkHIW.js +0 -1
- package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
- package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
- package/web/dist/assets/Skills-DArA6IHS.js +0 -1
- package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
- package/web/dist/assets/index-DC3-GlCY.js +0 -1
- package/web/dist/assets/index-DP035wPa.js +0 -12
- package/web/dist/assets/index-dHi-7M-F.js +0 -2
- package/web/dist/assets/roman-DkYN0LX9.js +0 -1
package/js/web/api.mjs
CHANGED
|
@@ -1,55 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The web console's read endpoints —
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
2
|
+
* The web console's read endpoints — `overview`, `themes`, `doctor`, `diff`, `installs`,
|
|
3
|
+
* `excludes`, `setup`, `profile`, `catalog`, `item` and `wiki_item` — and the `WebState`
|
|
4
|
+
* all eleven read from.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* reshaped. `apiExcludes` is `excludesSnapshot()`, unmodified. `apiInstalls` is a row per
|
|
12
|
-
* `installTargets()` entry through five detectors P5d and P5f ported. `apiDoctor` is
|
|
13
|
-
* `doctorCollect()` — with the one parameter it did not have. Only `apiThemes` and
|
|
14
|
-
* `apiOverview` compute anything of their own, and most of what `apiOverview` computes is
|
|
15
|
-
* counting.
|
|
16
|
-
*
|
|
17
|
-
* THE ONE PARAMETER: `doctorCollect({groups})`. `js/inspect/doctor.mjs` carried a `ran(check,
|
|
18
|
-
* label, probs)` that returned its third argument and threw the first two away, with a
|
|
19
|
-
* docblock saying the label stays "because it is the one place each check is NAMED, and
|
|
20
|
-
* what P6's accumulator will key on". This is that phase. `on_progress=` — the TUI's — is
|
|
21
|
-
* still absent, still P7's: nothing here passes it and adding it would be a claim no cell
|
|
22
|
-
* can check.
|
|
23
|
-
*
|
|
24
|
-
* THE INVENTORY IS THE FULL RECORD SINCE P6c, AND THERE IS STILL ONE CLASSIFIER. P6b
|
|
25
|
-
* shipped `inventoryFor` as three integers, because `apiOverview` reads three `len()`s off
|
|
26
|
-
* `state.inventory` and the badges and taxonomy classes belonged to a phase that had not
|
|
27
|
-
* arrived. `api_catalog` consumes exactly those fields, so this is the phase that would
|
|
28
|
-
* have made the counting half and the reading half two classifiers. It did not:
|
|
29
|
-
* `tuiInventory` in `js/inspect/inventory.mjs` is the one walk, `js/inspect/status.mjs`'s `inventoryCounts`
|
|
30
|
-
* is three `length`s off it, and `specEntries` grew out of `specNames` by adding the READ
|
|
31
|
-
* rather than by being written beside it.
|
|
32
|
-
*
|
|
33
|
-
* THE TAXONOMY WAS ALREADY PORTED, which the measurement found and the handoff did not.
|
|
34
|
-
* `js/inspect/status.mjs` said flatly that "the ~111 LOC of TUI taxonomy" was P7's; `LAW_CLASS`,
|
|
35
|
-
* `SKILL_CLASS`, `LAW_CLASSES` and `ENTITY_STATUSES` had in fact crossed in P5g inside
|
|
36
|
-
* `js/inspect/checks-authoring.mjs`, because doctor's authoring gates are what validate them. P6c moved the
|
|
37
|
-
* four to `js/inspect/inventory.mjs` — where Python keeps them — rather than copying them, and
|
|
38
|
-
* doctor imports them back. Only `parse_laws`, `load_registry` and `entity_status` were
|
|
39
|
-
* genuinely new.
|
|
40
|
-
*
|
|
41
|
-
* THE SNAPSHOT REPORTS NO INTERPRETER VERSION, and the absence is deliberate. The server
|
|
42
|
-
* this one replaced ran on an interpreter and published its version under a key named for
|
|
43
|
-
* it; nothing here answers that question, and answering it with this runtime's version
|
|
44
|
-
* under that name would have been a lie the UI printed. So the field is gone rather than
|
|
45
|
-
* present-and-empty. The recorded web corpus was taken from the old server and still
|
|
46
|
-
* carries the key, which is why its replay harness erases it before comparing.
|
|
47
|
-
*
|
|
48
|
-
* P6c's ROUTES ARE THE FIRST THAT PARSE A PATH, which brings three things no earlier
|
|
49
|
-
* endpoint could reach: `percentDecode` (NOT `decodeURIComponent`, which throws where the
|
|
50
|
-
* reference leaves a stray `%` alone), the `NotFound` → 404 convention, and `flatName`'s
|
|
51
|
-
* traversal refusal — a GET carries no token, so before that check the item route was an
|
|
52
|
-
* arbitrary-file read.
|
|
6
|
+
* `PREFIX_ROUTES` below are the first routes that parse a path, which brings three things
|
|
7
|
+
* no state-keyed route needs: `percentDecode` (NOT `decodeURIComponent`, which throws on a
|
|
8
|
+
* `%` that is not an escape, where this shell instead answers a 404 naming the literal
|
|
9
|
+
* text), the `NotFound` → 404 convention, and `flatName`'s traversal refusal — a GET
|
|
10
|
+
* carries no token, so before that check the item route was an arbitrary-file read.
|
|
53
11
|
*/
|
|
54
12
|
import { createHash } from 'node:crypto';
|
|
55
13
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -73,35 +31,30 @@ import {
|
|
|
73
31
|
import { firstBlockquote } from '../hosts/native.mjs';
|
|
74
32
|
import { EMIT_OPTIONS, themeOptions } from '../maintain/setup.mjs';
|
|
75
33
|
import { accentFor, statusData } from '../inspect/status.mjs';
|
|
76
|
-
import { readText, withDiscardableStderr } from '../lib/fs.mjs';
|
|
34
|
+
import { readText, withDiscardableStderr, isDir, isFile } from '../lib/fs.mjs';
|
|
77
35
|
import { comparePaths, normcase, within } from '../lib/paths.mjs';
|
|
78
36
|
import { stripWhitespace, percentDecode } from '../lib/text.mjs';
|
|
79
37
|
import { readJsonc } from '../hosts/settings.mjs';
|
|
80
38
|
import { apiActivity, apiActivityDetail } from './activity.mjs';
|
|
81
39
|
import { apiMcp, apiRules } from './actions.mjs';
|
|
82
|
-
import { apiGraph } from './graph.mjs';
|
|
83
40
|
|
|
84
|
-
/**
|
|
41
|
+
/** A requested catalog section or item that does not exist. */
|
|
85
42
|
export class NotFound extends Error {}
|
|
86
43
|
|
|
87
|
-
|
|
88
|
-
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
89
|
-
|
|
90
|
-
/** Two resolved paths, compared as `PurePath.__eq__` does — case-folded on Windows. */
|
|
44
|
+
/** Two resolved paths, compared case-folded on Windows. */
|
|
91
45
|
const samePath = (a, b) => normcase(resolvePath(a)) === normcase(resolvePath(b));
|
|
92
46
|
|
|
93
|
-
/**
|
|
47
|
+
/** `??` would also swallow an explicit `null` the file declared; this does not. */
|
|
94
48
|
const dget = (obj, key, dflt) => (Object.hasOwn(obj, key) ? obj[key] : dflt);
|
|
95
49
|
|
|
96
|
-
/**
|
|
50
|
+
/** SHA-256 hex, first 16 chars — `""` for an empty file. */
|
|
97
51
|
export function fingerprint(text) {
|
|
98
52
|
return text ? createHash('sha256').update(text, 'utf-8').digest('hex').slice(0, 16) : '';
|
|
99
53
|
}
|
|
100
54
|
|
|
101
55
|
/**
|
|
102
|
-
* `
|
|
103
|
-
*
|
|
104
|
-
* every timezone but one.
|
|
56
|
+
* `YYYY-MM-DD HH:MM`, LOCAL time — `toISOString` would be UTC, which reads wrong in every
|
|
57
|
+
* timezone but one for a user-facing timestamp.
|
|
105
58
|
*/
|
|
106
59
|
export function stampMinute(ms) {
|
|
107
60
|
const d = new Date(ms);
|
|
@@ -113,19 +66,19 @@ export function stampMinute(ms) {
|
|
|
113
66
|
// ---- WebState ---------------------------------------------------------------------------
|
|
114
67
|
|
|
115
68
|
/**
|
|
116
|
-
*
|
|
69
|
+
* The resolved view of the deployed harness a request reads from.
|
|
117
70
|
*
|
|
118
71
|
* A factory returning a plain object rather than a class, because the two cached
|
|
119
72
|
* properties are the only behaviour and a getter pair expresses them without ceremony.
|
|
120
73
|
* THE CACHING IS BEHAVIOUR, NOT AN OPTIMISATION: `doctorCollect` renders a theme per call
|
|
121
74
|
* and the dashboard GETs `/api/overview` on every navigation, so an uncached `doctor`
|
|
122
75
|
* would put a full build on the request path. `refresh()` is what a mutation calls to
|
|
123
|
-
* drop both
|
|
76
|
+
* drop both.
|
|
124
77
|
*/
|
|
125
78
|
export function webState(theme = null, target = null) {
|
|
126
79
|
const st = {
|
|
127
|
-
// `
|
|
128
|
-
//
|
|
80
|
+
// NOT resolved: `target` is printed by three endpoints, so resolving it here would
|
|
81
|
+
// change what they answer.
|
|
129
82
|
target: target || opencodeConfigDir(),
|
|
130
83
|
root: null,
|
|
131
84
|
theme: null,
|
|
@@ -164,14 +117,12 @@ export function webState(theme = null, target = null) {
|
|
|
164
117
|
st._doctor = { ok: !problems.length, problems, checked_at: stampMinute(Date.now()) };
|
|
165
118
|
};
|
|
166
119
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
120
|
+
* The CURRENT install's mode, from its `.geneseed-emit` marker: the ROOT first (where
|
|
121
|
+
* every emit writes it), then the data dir.
|
|
169
122
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* `
|
|
173
|
-
* self.emit` line was missing here, which would have left `emit` stale after any write
|
|
174
|
-
* that re-themed or re-pointed the install. Restored with its caller.
|
|
123
|
+
* `refresh()` below MUST call this: without it, `emit` goes stale after any write that
|
|
124
|
+
* re-themes or re-points the install, since the constructor only reads it once via
|
|
125
|
+
* `installedDefaults()`.
|
|
175
126
|
*/
|
|
176
127
|
st.detectEmit = () => {
|
|
177
128
|
for (const d of [st.root, st.target]) {
|
|
@@ -181,12 +132,12 @@ export function webState(theme = null, target = null) {
|
|
|
181
132
|
const v = stripWhitespace(readText(em));
|
|
182
133
|
if (v) return v;
|
|
183
134
|
}
|
|
184
|
-
} catch { /*
|
|
135
|
+
} catch { /* unreadable marker: try the next candidate */ }
|
|
185
136
|
}
|
|
186
137
|
return existsSync(path.join(st.root, 'CLAUDE.md')) ? 'claude-global' : 'opencode-global';
|
|
187
138
|
};
|
|
188
139
|
/**
|
|
189
|
-
*
|
|
140
|
+
* Re-point the console at another detected install's data dir.
|
|
190
141
|
*
|
|
191
142
|
* `root` is the install ROOT the markers and sigils live at. It defaults to `target` and
|
|
192
143
|
* differs only for claude/bob/copilot PROJECT installs, where the data sits under
|
|
@@ -217,13 +168,12 @@ export function webState(theme = null, target = null) {
|
|
|
217
168
|
return st;
|
|
218
169
|
}
|
|
219
170
|
|
|
220
|
-
/** `_web_core._deployed`. */
|
|
221
171
|
export function deployed(state) {
|
|
222
172
|
return existsSync(path.join(state.target, GLOBAL_MANIFEST));
|
|
223
173
|
}
|
|
224
174
|
|
|
225
175
|
/**
|
|
226
|
-
*
|
|
176
|
+
* A deployed spec's one-line purpose.
|
|
227
177
|
*
|
|
228
178
|
* The `> blockquote` convention every rendered skill and agent carries, then the
|
|
229
179
|
* frontmatter `description`, then the first prose paragraph. The fallbacks are for the
|
|
@@ -243,13 +193,13 @@ export function specDesc(fm, body) {
|
|
|
243
193
|
}
|
|
244
194
|
|
|
245
195
|
/**
|
|
246
|
-
*
|
|
196
|
+
* Agent/skill specs read straight off a DEPLOYED harness dir.
|
|
247
197
|
*
|
|
248
198
|
* Agents are flat `<root>/<name>.md` (skipping `_*` templates); skills use OpenCode's
|
|
249
|
-
* folder layout `<root>/<name>/SKILL.md`.
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
199
|
+
* folder layout `<root>/<name>/SKILL.md`. Matched case-INSENSITIVE on Windows via
|
|
200
|
+
* `normcase`, not a bare `endsWith`. The frontmatter is stripped because it is host
|
|
201
|
+
* plumbing rather than prose, which is what makes a deployed entry the same shape as a
|
|
202
|
+
* source-rendered one and every consumer indifferent to the origin.
|
|
253
203
|
*/
|
|
254
204
|
export function specEntries(root, nested) {
|
|
255
205
|
const out = [];
|
|
@@ -272,8 +222,7 @@ export function specEntries(root, nested) {
|
|
|
272
222
|
}
|
|
273
223
|
|
|
274
224
|
/**
|
|
275
|
-
*
|
|
276
|
-
* `state.target`, not a fresh render of `src/`.
|
|
225
|
+
* The agents and skills actually installed at `state.target`, not a fresh render of `src/`.
|
|
277
226
|
*
|
|
278
227
|
* All three constitutional tiers still come from the render: once deployed they live inside
|
|
279
228
|
* AGENT.md rather than as separate files, so the deployed arm replaces the two ENTITY rosters
|
|
@@ -309,28 +258,25 @@ export function deployedInventory(state) {
|
|
|
309
258
|
theme: state.theme };
|
|
310
259
|
}
|
|
311
260
|
|
|
312
|
-
/**
|
|
261
|
+
/** The deployed record when there is one, else the source render. */
|
|
313
262
|
export function inventoryFor(state) {
|
|
314
263
|
return deployed(state) ? deployedInventory(state) : tuiInventory(state.theme);
|
|
315
264
|
}
|
|
316
265
|
|
|
317
266
|
// ---- the catalog stores overview counts ---------------------------------------------------
|
|
318
267
|
|
|
319
|
-
/**
|
|
268
|
+
/** Always `<target>/memory` — never the CWD-scanning resolver. */
|
|
320
269
|
const memoryDir = (state) => path.join(state.target, 'memory');
|
|
321
270
|
const notebookDir = (state) => path.join(state.target, 'notebook');
|
|
322
271
|
|
|
323
272
|
/**
|
|
324
|
-
*
|
|
273
|
+
* Matched case-insensitively on Windows.
|
|
325
274
|
*
|
|
326
|
-
* `comparePaths`
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* P6b and P6c was lower-case, which is why nothing said so for three phases. The `normcase`
|
|
332
|
-
* in the filter beside it was already here for exactly this reason — the sort was the half
|
|
333
|
-
* that got missed.
|
|
275
|
+
* `comparePaths` and NOT a bare `.sort()`: on Windows, filenames sort case-insensitively, so
|
|
276
|
+
* `MEMORY.md` sorts under `m` and lands after `a-fact.md`; JS's default comparator is UTF-16
|
|
277
|
+
* code units, so `M` (0x4D) sorts before `a` (0x61) and the whole catalog comes back in a
|
|
278
|
+
* different order. The `normcase` in the filter beside it only fixes matching, not
|
|
279
|
+
* ordering — this is the other half.
|
|
334
280
|
*/
|
|
335
281
|
function globMd(dir) {
|
|
336
282
|
if (!isDir(dir)) return [];
|
|
@@ -340,9 +286,9 @@ function globMd(dir) {
|
|
|
340
286
|
.sort(comparePaths);
|
|
341
287
|
}
|
|
342
288
|
|
|
343
|
-
|
|
289
|
+
/** Filename without its extension — single owner, imported by `js/web/activity.mjs` too. */
|
|
290
|
+
export const stemOf = (name) => name.slice(0, name.length - path.extname(name).length);
|
|
344
291
|
|
|
345
|
-
/** `_web_catalog._memory_items`. */
|
|
346
292
|
export function memoryItems(state) {
|
|
347
293
|
const d = memoryDir(state);
|
|
348
294
|
if (!isDir(d)) return [];
|
|
@@ -359,7 +305,6 @@ export function memoryItems(state) {
|
|
|
359
305
|
});
|
|
360
306
|
}
|
|
361
307
|
|
|
362
|
-
/** `_web_catalog._notebook_items`. */
|
|
363
308
|
export function notebookItems(state) {
|
|
364
309
|
const d = notebookDir(state);
|
|
365
310
|
if (!isDir(d)) return [];
|
|
@@ -368,13 +313,12 @@ export function notebookItems(state) {
|
|
|
368
313
|
}));
|
|
369
314
|
}
|
|
370
315
|
|
|
371
|
-
/**
|
|
316
|
+
/** The two setup manifests, in listing order. */
|
|
372
317
|
const CONFIG_META = {
|
|
373
318
|
'context.json': ['Project context', 'what the agent loads for this project'],
|
|
374
319
|
'wiki.jsonc': ['Wiki manifest', 'your machine-wide knowledge base(s)'],
|
|
375
320
|
};
|
|
376
321
|
|
|
377
|
-
/** `_web_catalog._config_items`. */
|
|
378
322
|
export function configItems(state) {
|
|
379
323
|
const out = [];
|
|
380
324
|
for (const fname of ['context.json', 'wiki.jsonc']) {
|
|
@@ -395,23 +339,18 @@ export const WIKI_FILE_CAP = 5000;
|
|
|
395
339
|
* The three wiki sites below, and `apiDeployCmd` in `js/web/actions.mjs`, are the
|
|
396
340
|
* request-path callers of `expanduser`, and it REFUSES a `~user` form by printing and throwing
|
|
397
341
|
* (`js/hosts/hosts.mjs`'s docblock). None of them may let that escape: `js/web/handler.mjs`
|
|
398
|
-
* wraps the whole GET in a blanket `catch` → a JSON
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
* three degrades for a `~user` path, from the other direction: `ntpath.expanduser` expands
|
|
404
|
-
* `~nosuchuser` to `C:\Users\nosuchuser\…`, which does not exist, so `is_file()`/`is_dir()`
|
|
405
|
-
* answer False. `_wiki_path` in `_web_catalog.py` is the guard that makes POSIX agree, where
|
|
406
|
-
* the same call raises RuntimeError instead.
|
|
342
|
+
* wraps the whole GET in a blanket `catch` → a JSON 500, so an unguarded refusal would turn
|
|
343
|
+
* ONE bad line in a file the user hand-edits into a dead Knowledge section. That is the same
|
|
344
|
+
* argument `sovereignBypass` makes in `js/hosts/hosts.mjs` — contain the refusal per entry
|
|
345
|
+
* and take the site's OWN degrade — and each of the three already has one: `[]`, `continue`,
|
|
346
|
+
* `continue`-into-404.
|
|
407
347
|
*
|
|
408
|
-
* `withDiscardableStderr` because `expanduser`
|
|
409
|
-
* caller
|
|
410
|
-
* bytes by `tests/web_golden.py`, so replaying it would be a divergence on every cell.
|
|
348
|
+
* `withDiscardableStderr` because `expanduser` prints its refusal at the RAISE SITE, and
|
|
349
|
+
* this caller wants that swallowed along with the exception rather than logged.
|
|
411
350
|
*
|
|
412
|
-
* DELIBERATE
|
|
413
|
-
*
|
|
414
|
-
*
|
|
351
|
+
* DELIBERATE PRODUCT DECISION: a `~otherUser` path is refused rather than resolved, even
|
|
352
|
+
* where the account happens to exist — reading another account's files through a
|
|
353
|
+
* hand-edited wiki manifest is not a feature.
|
|
415
354
|
*/
|
|
416
355
|
function wikiPath(p) {
|
|
417
356
|
try {
|
|
@@ -422,12 +361,12 @@ function wikiPath(p) {
|
|
|
422
361
|
}
|
|
423
362
|
|
|
424
363
|
/**
|
|
425
|
-
*
|
|
426
|
-
*
|
|
364
|
+
* `$GENESEED_WIKI` first, else `wiki.jsonc` beside the deployed bundle, read with the
|
|
365
|
+
* harness's generic JSONC loader.
|
|
427
366
|
*
|
|
428
|
-
* `resolvePath
|
|
429
|
-
*
|
|
430
|
-
*
|
|
367
|
+
* `resolvePath`, not merely `expanduser`: harmless here because `p` is consumed by `isFile`
|
|
368
|
+
* and `mcpLoad` and never reaches a response body, and both of those follow symlinks and
|
|
369
|
+
* resolve a relative path against the same cwd anyway.
|
|
431
370
|
*/
|
|
432
371
|
function wikiManifest(state) {
|
|
433
372
|
const cand = process.env.GENESEED_WIKI;
|
|
@@ -439,16 +378,14 @@ function wikiManifest(state) {
|
|
|
439
378
|
}
|
|
440
379
|
|
|
441
380
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
381
|
+
* The COMMENT-TOLERANT dict loader, `{}` for a file that is missing, unreadable,
|
|
382
|
+
* unparseable, or not an object.
|
|
444
383
|
*
|
|
445
|
-
* Comment-tolerant is the whole point and
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* `tests/web_golden.py` are plain JSON, so no cell had one — `catalog/…-tolerates-comments`
|
|
451
|
-
* is the cell that does now.
|
|
384
|
+
* Comment-tolerant is the whole point, and getting this wrong is silent: the two files it
|
|
385
|
+
* reads are `wiki.jsonc` and `context.json`, hand-maintained, so a `//` line is expected.
|
|
386
|
+
* Plain `JSON.parse` would throw on it and this loader would then answer `{}` — the wiki
|
|
387
|
+
* section listing nothing and the config item's `manifest` coming back empty, with no
|
|
388
|
+
* error surfaced anywhere.
|
|
452
389
|
*/
|
|
453
390
|
function mcpLoad(p) {
|
|
454
391
|
if (!isFile(p)) return {};
|
|
@@ -458,29 +395,28 @@ function mcpLoad(p) {
|
|
|
458
395
|
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
459
396
|
}
|
|
460
397
|
|
|
461
|
-
/**
|
|
462
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Every `.md` under `dir`, recursively.
|
|
400
|
+
*
|
|
401
|
+
* `recursive: true` replaces a hand-rolled walk; its one caller re-sorts the result with
|
|
402
|
+
* `comparePaths` before using it, so the order this returns is never observed.
|
|
403
|
+
*/
|
|
404
|
+
function rglobMd(dir) {
|
|
463
405
|
let entries;
|
|
464
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
else if (normcase(e.name).endsWith('.md')) out.push(full);
|
|
469
|
-
}
|
|
470
|
-
return out;
|
|
406
|
+
try { entries = readdirSync(dir, { recursive: true, withFileTypes: true }); } catch { return []; }
|
|
407
|
+
return entries
|
|
408
|
+
.filter((e) => e.isFile() && normcase(e.name).endsWith('.md'))
|
|
409
|
+
.map((e) => path.join(e.parentPath, e.name));
|
|
471
410
|
}
|
|
472
411
|
|
|
473
|
-
/** `_web_catalog._wiki_items`. */
|
|
474
412
|
export function wikiItems(state) {
|
|
475
413
|
const items = [];
|
|
476
414
|
const seen = new Set();
|
|
477
415
|
for (const w of wikiManifest(state)) {
|
|
478
416
|
if (!w || typeof w !== 'object' || Array.isArray(w)) continue;
|
|
479
417
|
const wname = String(dget(w, 'name', null) || 'wiki');
|
|
480
|
-
// PER ENTRY, and the loop continues — one unusable `path` in the manifest must not
|
|
481
|
-
// every OTHER vault in it.
|
|
482
|
-
// unobservable: `root` is only ever joined against or used as the base of a `relative()`
|
|
483
|
-
// whose other operand was built FROM it, and `source` is `.resolve()`d on both sides.
|
|
418
|
+
// PER ENTRY, and the loop continues — one unusable `path` in the manifest must not
|
|
419
|
+
// blank every OTHER vault in it.
|
|
484
420
|
const root = wikiPath(String(dget(w, 'path', null) || ''));
|
|
485
421
|
if (!root || !isDir(root)) continue;
|
|
486
422
|
const entries = (dget(w, 'entries', null) || [])
|
|
@@ -496,7 +432,7 @@ export function wikiItems(state) {
|
|
|
496
432
|
const fp = path.join(root, rel);
|
|
497
433
|
let mds;
|
|
498
434
|
if (isFile(fp) && path.extname(fp) === '.md') mds = [fp];
|
|
499
|
-
//
|
|
435
|
+
// Capped and sorted via `comparePaths`, for the reason `globMd` carries.
|
|
500
436
|
else if (isDir(fp)) mds = rglobMd(fp).sort(comparePaths).slice(0, WIKI_FILE_CAP);
|
|
501
437
|
else continue;
|
|
502
438
|
for (const md of mds) {
|
|
@@ -513,7 +449,7 @@ export function wikiItems(state) {
|
|
|
513
449
|
return items;
|
|
514
450
|
}
|
|
515
451
|
|
|
516
|
-
/**
|
|
452
|
+
/** One page by `<wiki>:<relpath>`, never outside the vault. */
|
|
517
453
|
export function apiWikiItem(state, name) {
|
|
518
454
|
const at = name.indexOf(':');
|
|
519
455
|
const wname = at < 0 ? name : name.slice(0, at);
|
|
@@ -522,9 +458,8 @@ export function apiWikiItem(state, name) {
|
|
|
522
458
|
for (const w of wikiManifest(state)) {
|
|
523
459
|
if (!w || typeof w !== 'object' || Array.isArray(w)) continue;
|
|
524
460
|
if (String(dget(w, 'name', null) || 'wiki') !== wname) continue;
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
// where the reference lands when the expanded root turns out not to exist.
|
|
461
|
+
// An unresolvable or refused root falls through to the `NotFound` below, the same as
|
|
462
|
+
// one that simply does not exist.
|
|
528
463
|
const root = wikiPath(String(dget(w, 'path', null) || ''));
|
|
529
464
|
if (!root) continue;
|
|
530
465
|
const p = resolvePath(path.join(root, rel));
|
|
@@ -538,19 +473,18 @@ export function apiWikiItem(state, name) {
|
|
|
538
473
|
}
|
|
539
474
|
|
|
540
475
|
/**
|
|
541
|
-
*
|
|
476
|
+
* Re-exported from `js/lib/paths.mjs`, where the lib split put it.
|
|
542
477
|
*
|
|
543
478
|
* ⚠ A SECOND, INDEPENDENT COPY lives in `js/inspect/scan.mjs`, written for the doctor's tree
|
|
544
479
|
* walks. A change to the containment rule has to land on both, and nothing compares them.
|
|
545
480
|
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
* re-export keeps `js/web/actions.mjs`'s import path unchanged.
|
|
481
|
+
* Re-exported here (rather than imported directly by callers) so `js/web/actions.mjs`'s
|
|
482
|
+
* import path stays unchanged.
|
|
549
483
|
*/
|
|
550
484
|
export { within };
|
|
551
485
|
|
|
552
486
|
/**
|
|
553
|
-
*
|
|
487
|
+
* Catalog names are flat basenames.
|
|
554
488
|
*
|
|
555
489
|
* A separator, a `..` or a drive colon in the URL segment is someone steering the join
|
|
556
490
|
* outside the catalog dir. A GET carries no token, so before this check the endpoint was
|
|
@@ -563,16 +497,13 @@ function flatName(name) {
|
|
|
563
497
|
}
|
|
564
498
|
}
|
|
565
499
|
|
|
566
|
-
/** `_web_core.WIKILINK_RE`. Exported since P6e — `js/web/graph.mjs` is its second reader. */
|
|
567
500
|
export const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
568
501
|
|
|
569
502
|
/**
|
|
570
|
-
* `
|
|
503
|
+
* `[[name]]` matched against known agent and skill names.
|
|
571
504
|
*
|
|
572
505
|
* AGENTS FIRST, SKILLS SECOND, into ONE map — so a name that is both resolves as a SKILL.
|
|
573
|
-
*
|
|
574
|
-
* `item/an-agent-carries-its-body-and-its-resolved-links` is the cell that would catch the
|
|
575
|
-
* other order.
|
|
506
|
+
* This ordering is deliberate and must not be swapped.
|
|
576
507
|
*/
|
|
577
508
|
export function resolveLinks(state, body) {
|
|
578
509
|
const inv = state.inventory;
|
|
@@ -582,8 +513,8 @@ export function resolveLinks(state, body) {
|
|
|
582
513
|
const links = [];
|
|
583
514
|
const seen = new Set();
|
|
584
515
|
for (const m of body.matchAll(WIKILINK_RE)) {
|
|
585
|
-
// `stripWhitespace`, not `trim()` —
|
|
586
|
-
//
|
|
516
|
+
// `stripWhitespace`, not `trim()` — the two whitespace classes differ, and both call
|
|
517
|
+
// sites on this regex must answer with the same rule.
|
|
587
518
|
const label = stripWhitespace(m[1]);
|
|
588
519
|
if (known.has(label) && !seen.has(label)) {
|
|
589
520
|
seen.add(label);
|
|
@@ -594,8 +525,8 @@ export function resolveLinks(state, body) {
|
|
|
594
525
|
}
|
|
595
526
|
|
|
596
527
|
/**
|
|
597
|
-
*
|
|
598
|
-
*
|
|
528
|
+
* A setup file parsed into the shape the detail pane renders as cards, or `null` so the
|
|
529
|
+
* caller falls back to the raw body.
|
|
599
530
|
*/
|
|
600
531
|
function configManifest(name, p) {
|
|
601
532
|
const cfg = mcpLoad(p);
|
|
@@ -611,7 +542,7 @@ function configManifest(name, p) {
|
|
|
611
542
|
}
|
|
612
543
|
|
|
613
544
|
/**
|
|
614
|
-
*
|
|
545
|
+
* A closed list; anything else is a 404.
|
|
615
546
|
*
|
|
616
547
|
* ⚠ `laws` STAYS ONE SECTION FOR ALL THREE TIERS, and that is a decision rather than an
|
|
617
548
|
* oversight. The console has ONE Constitution entry, not three, so a reader keeps the "read it
|
|
@@ -659,7 +590,6 @@ function constitutionItems(inv) {
|
|
|
659
590
|
];
|
|
660
591
|
}
|
|
661
592
|
|
|
662
|
-
/** `_web_catalog.api_catalog`. */
|
|
663
593
|
export function apiCatalog(state, section) {
|
|
664
594
|
if (!SECTIONS.includes(section)) throw new NotFound(section);
|
|
665
595
|
const inv = state.inventory;
|
|
@@ -684,7 +614,6 @@ export function apiCatalog(state, section) {
|
|
|
684
614
|
return { section, items };
|
|
685
615
|
}
|
|
686
616
|
|
|
687
|
-
/** `_web_catalog.api_item`. */
|
|
688
617
|
export function apiItem(state, type, name) {
|
|
689
618
|
const inv = state.inventory;
|
|
690
619
|
if (type === 'agent' || type === 'skill') {
|
|
@@ -737,7 +666,7 @@ export function apiItem(state, type, name) {
|
|
|
737
666
|
|
|
738
667
|
// ---- the eight endpoints -------------------------------------------------------------
|
|
739
668
|
|
|
740
|
-
/** `
|
|
669
|
+
/** `buildOverride` in `actions.mjs` is a third reader of this. */
|
|
741
670
|
export function themeChoices() {
|
|
742
671
|
return themeOptions().map(([name, blurb]) => {
|
|
743
672
|
const data = readJsonMaybe(path.join(THEMES, `${name}.json`));
|
|
@@ -747,16 +676,19 @@ export function themeChoices() {
|
|
|
747
676
|
});
|
|
748
677
|
}
|
|
749
678
|
|
|
750
|
-
/** `_web_actions._emit_choices`. */
|
|
751
679
|
export const emitChoices = () => EMIT_OPTIONS.map(([name, desc]) => ({ name, desc }));
|
|
752
680
|
|
|
753
|
-
/** `_web_actions.api_themes`. */
|
|
754
681
|
export function apiThemes(state) {
|
|
755
682
|
return { themes: themeChoices(), emits: emitChoices(),
|
|
756
683
|
current: { theme: state.theme, emit: state.emit } };
|
|
757
684
|
}
|
|
758
685
|
|
|
759
|
-
/**
|
|
686
|
+
/**
|
|
687
|
+
* No interpreter/runtime version field, deliberately: this server has no interpreter
|
|
688
|
+
* version worth reporting under that name, and reporting this runtime's own version there
|
|
689
|
+
* would misrepresent what actually changed. The field is absent rather than
|
|
690
|
+
* present-and-empty.
|
|
691
|
+
*/
|
|
760
692
|
export function apiSetup(state) {
|
|
761
693
|
return {
|
|
762
694
|
...statusData(),
|
|
@@ -766,7 +698,7 @@ export function apiSetup(state) {
|
|
|
766
698
|
};
|
|
767
699
|
}
|
|
768
700
|
|
|
769
|
-
/**
|
|
701
|
+
/** The same engine as the `doctor` verb, grouped per check. */
|
|
770
702
|
export function apiDoctor(state) {
|
|
771
703
|
const groups = [];
|
|
772
704
|
const [themes, problems] = doctorCollect({ theme: state.theme, groups });
|
|
@@ -775,19 +707,18 @@ export function apiDoctor(state) {
|
|
|
775
707
|
checked_at: state.doctor.checked_at };
|
|
776
708
|
}
|
|
777
709
|
|
|
778
|
-
/** `_web_actions.api_diff`. */
|
|
779
710
|
export function apiDiff(state) {
|
|
780
711
|
const { target, theme, files } = diffCollect({
|
|
781
712
|
target: state.target, theme: state.theme, emit: state.emit });
|
|
782
713
|
return { deployed: files !== null, target, theme, files: files || [] };
|
|
783
714
|
}
|
|
784
715
|
|
|
785
|
-
/**
|
|
716
|
+
/** The union across every global install, verbatim. */
|
|
786
717
|
export const apiExcludes = () => excludesSnapshot();
|
|
787
718
|
|
|
788
719
|
/**
|
|
789
|
-
*
|
|
790
|
-
*
|
|
720
|
+
* The data dir an install's inventory/memory/diff is read from. Host-driven, so a new
|
|
721
|
+
* nested-marker host cannot silently read the bare root.
|
|
791
722
|
*/
|
|
792
723
|
export function viewCfg(host, scope, root) {
|
|
793
724
|
if (scope === 'project' && ['claude', 'bob', 'copilot'].includes(host)) {
|
|
@@ -796,7 +727,6 @@ export function viewCfg(host, scope, root) {
|
|
|
796
727
|
return root;
|
|
797
728
|
}
|
|
798
729
|
|
|
799
|
-
/** `_web_actions.api_installs`. */
|
|
800
730
|
export function apiInstalls(state) {
|
|
801
731
|
const out = [];
|
|
802
732
|
for (const [host, scope, root] of installTargets()) {
|
|
@@ -814,11 +744,10 @@ export function apiInstalls(state) {
|
|
|
814
744
|
modes: discoverNames('modes', 'direct') };
|
|
815
745
|
}
|
|
816
746
|
|
|
817
|
-
/**
|
|
747
|
+
/** Beside the deployed AGENT.md. */
|
|
818
748
|
export const PROFILE_FILE = 'PROFILE.md';
|
|
819
749
|
export const profilePath = (state) => path.join(state.target, PROFILE_FILE);
|
|
820
750
|
|
|
821
|
-
/** `_web_catalog.api_profile`. */
|
|
822
751
|
export function apiProfile(state) {
|
|
823
752
|
const p = profilePath(state);
|
|
824
753
|
if (!isFile(p)) return { exists: false, path: p, text: '', fingerprint: '' };
|
|
@@ -826,7 +755,7 @@ export function apiProfile(state) {
|
|
|
826
755
|
return { exists: true, path: p, text, fingerprint: fingerprint(text) };
|
|
827
756
|
}
|
|
828
757
|
|
|
829
|
-
/**
|
|
758
|
+
/** The dashboard aggregate. */
|
|
830
759
|
export function apiOverview(state) {
|
|
831
760
|
const inv = state.inventory;
|
|
832
761
|
let diff = null;
|
|
@@ -855,8 +784,7 @@ export function apiOverview(state) {
|
|
|
855
784
|
buildEpoch = Math.floor(ms / 1000);
|
|
856
785
|
}
|
|
857
786
|
// Which detected install the current view points at, so the dashboard's footprint hero
|
|
858
|
-
// can re-emit exactly it. Mirrors `viewCfg`'s rule, spelled out
|
|
859
|
-
// it out — the two are separate code in the Python too.
|
|
787
|
+
// can re-emit exactly it. Mirrors `viewCfg`'s rule, spelled out separately here.
|
|
860
788
|
let install = null;
|
|
861
789
|
for (const [host, scope, root] of installTargets()) {
|
|
862
790
|
const data = (scope === 'project' && ['claude', 'bob', 'copilot'].includes(host))
|
|
@@ -866,7 +794,7 @@ export function apiOverview(state) {
|
|
|
866
794
|
install = { host, scope, path: root, footprint: footprintOfDir(root) };
|
|
867
795
|
break;
|
|
868
796
|
}
|
|
869
|
-
} catch { /*
|
|
797
|
+
} catch { /* an unreadable candidate just isn't a match */ }
|
|
870
798
|
}
|
|
871
799
|
return {
|
|
872
800
|
theme: state.theme,
|
|
@@ -944,9 +872,9 @@ const RECENT_LIMIT = 8;
|
|
|
944
872
|
/**
|
|
945
873
|
* THE NEWEST FILE-BACKED ENTRIES ACROSS THE HARNESS — the "freshly grown" card's whole payload.
|
|
946
874
|
*
|
|
947
|
-
*
|
|
948
|
-
* because NOTHING else in this API carries a per-entry date: every
|
|
949
|
-
* `{name, title, desc, source}` and the only two timestamps on the whole surface are
|
|
875
|
+
* The first GET added after the Python→Node port froze its reference surface. It exists
|
|
876
|
+
* because NOTHING else in this API carries a per-entry date: every
|
|
877
|
+
* catalog row is `{name, title, desc, source}` and the only two timestamps on the whole surface are
|
|
950
878
|
* `overview.build_time` and `doctor.checked_at`. A client cannot stat a path, so "what changed
|
|
951
879
|
* lately" was unanswerable without a server that looks.
|
|
952
880
|
*
|
|
@@ -1001,57 +929,34 @@ function releaseLabel() {
|
|
|
1001
929
|
}
|
|
1002
930
|
|
|
1003
931
|
/**
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
* A literal path to `api_X(state)`, exactly as the reference's table is — and the reason
|
|
1007
|
-
* it is a table on this side too is `tests/unit/web_server.test.mjs`'s cross-check, which
|
|
1008
|
-
* requires this table plus `PREFIX_ROUTES`, `PORTED_INLINE` and `NOT_PORTED` to equal
|
|
1009
|
-
* `REF_GET` — a FROZEN LITERAL of what the reference daemon answered, since the source it
|
|
1010
|
-
* was once read from with `ast` is deleted. On the second instance of anything, the gate
|
|
1011
|
-
* becomes a table cross-checked against a declaration.
|
|
1012
|
-
*/
|
|
1013
|
-
/**
|
|
1014
|
-
* `_web_server.do_GET`'s prefix routes, the ported ones — path in, response out.
|
|
932
|
+
* The prefix routes — path in, response out.
|
|
1015
933
|
*
|
|
1016
934
|
* `percentDecode` and not `decodeURIComponent`: the JS builtin throws a `URIError` on a `%`
|
|
1017
|
-
* that is not an escape,
|
|
1018
|
-
* a 404 naming the literal text.
|
|
935
|
+
* that is not an escape, where this shell instead answers a 404 naming the literal text.
|
|
1019
936
|
*/
|
|
1020
937
|
export const PREFIX_ROUTES = [
|
|
1021
938
|
['/api/catalog/', (state, p) => apiCatalog(state, p.split('/').pop())],
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
// and a 500 two frames down.
|
|
939
|
+
// /api/item/<type>/<name> — TYPE has no slash, the NAME keeps its slashes so a wiki
|
|
940
|
+
// page's relpath survives. A missing name is a 404 here rather than a 500 two frames down.
|
|
1025
941
|
['/api/item/', (state, p) => {
|
|
1026
|
-
const
|
|
1027
|
-
if (
|
|
1028
|
-
return apiItem(state,
|
|
942
|
+
const m = /^\/api\/item\/([^/]+)\/(.+)$/.exec(p);
|
|
943
|
+
if (!m) throw new NotFound(p);
|
|
944
|
+
return apiItem(state, m[1], percentDecode(m[2]));
|
|
1029
945
|
}],
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
// lookup safe, rather than a check out here.
|
|
946
|
+
// The sid is passed through unquoted — `apiActivityDetail`'s safe-name scheme is what
|
|
947
|
+
// makes the lookup safe, rather than a check out here.
|
|
1033
948
|
['/api/activity/', (state, p) => apiActivityDetail(
|
|
1034
949
|
state, percentDecode(p.slice('/api/activity/'.length)))],
|
|
1035
950
|
];
|
|
1036
951
|
|
|
1037
|
-
/**
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (at < 0) break;
|
|
1044
|
-
out.push(rest.slice(0, at));
|
|
1045
|
-
rest = rest.slice(at + sep.length);
|
|
1046
|
-
}
|
|
1047
|
-
out.push(rest);
|
|
1048
|
-
return out;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
952
|
+
/**
|
|
953
|
+
* A literal path to `api_X(state)`, table-keyed rather than a chain of `if`s — the reason
|
|
954
|
+
* it is a table is `tests/unit/web_server.test.mjs`'s cross-check, which requires this
|
|
955
|
+
* table plus `PREFIX_ROUTES` and `GET_INLINE` to equal a surface list written out in the
|
|
956
|
+
* test, so a route cannot appear or vanish unenumerated.
|
|
957
|
+
*/
|
|
1051
958
|
export const STATE_ROUTES = {
|
|
1052
959
|
'/api/overview': apiOverview,
|
|
1053
|
-
// Beyond the reference — declared in `GET_BEYOND_REF`, which is what keeps the partition
|
|
1054
|
-
// gate's set equality honest instead of forging `REF_GET`.
|
|
1055
960
|
'/api/recent': apiRecent,
|
|
1056
961
|
'/api/themes': apiThemes,
|
|
1057
962
|
'/api/setup': apiSetup,
|
|
@@ -1060,11 +965,10 @@ export const STATE_ROUTES = {
|
|
|
1060
965
|
'/api/excludes': apiExcludes,
|
|
1061
966
|
'/api/profile': apiProfile,
|
|
1062
967
|
'/api/diff': apiDiff,
|
|
1063
|
-
'/api/graph': apiGraph,
|
|
1064
968
|
'/api/activity': apiActivity,
|
|
1065
|
-
//
|
|
1066
|
-
// indices, and the fingerprint every mutation must send back is what this GET
|
|
1067
|
-
//
|
|
969
|
+
// `/api/rules` must exist as a PAIR with its POST: `apiRulesMutate` splices `parseRules`'
|
|
970
|
+
// line indices, and the fingerprint every mutation must send back is what this GET
|
|
971
|
+
// answers — a daemon whose POST worked and whose GET 501'd would have no way to obtain one.
|
|
1068
972
|
'/api/rules': apiRules,
|
|
1069
973
|
'/api/mcp': apiMcp,
|
|
1070
974
|
};
|