@north-light/crouter 0.3.182 → 0.3.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/build-root.js +10 -3
  2. package/dist/builtin-memory/internal/memory-loading.md +3 -1
  3. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  4. package/dist/clients/attach/__tests__/titled-editor-paste.test.d.ts +1 -0
  5. package/dist/clients/attach/__tests__/titled-editor-paste.test.js +114 -0
  6. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +4 -1
  7. package/dist/clients/attach/input/titled-editor.d.ts +18 -0
  8. package/dist/clients/attach/input/titled-editor.js +62 -0
  9. package/dist/clients/attach/render/chat-view.d.ts +3 -0
  10. package/dist/clients/attach/render/chat-view.js +15 -6
  11. package/dist/clients/attach/render/context-message.js +2 -2
  12. package/dist/clients/attach/render/group-activity.d.ts +18 -1
  13. package/dist/clients/attach/render/group-activity.js +54 -6
  14. package/dist/clients/attach/render/group-recap.js +13 -2
  15. package/dist/clients/attach/render/{markdown-headings.d.ts → markdown-source.d.ts} +4 -4
  16. package/dist/clients/attach/render/markdown-source.js +165 -0
  17. package/dist/clients/attach/session/bindings.js +5 -2
  18. package/dist/clients/attach/viewer.js +396 -394
  19. package/dist/clients/inbox/review/keys.js +2 -9
  20. package/dist/clients/inbox/tui/keys.js +2 -9
  21. package/dist/commands/cron.js +1 -1
  22. package/dist/commands/memory/delete.js +1 -1
  23. package/dist/commands/memory/find.js +1 -1
  24. package/dist/commands/memory/lint.js +28 -3
  25. package/dist/commands/memory/list.js +1 -1
  26. package/dist/commands/memory/origin.js +1 -1
  27. package/dist/commands/memory/read.js +4 -4
  28. package/dist/commands/memory/write.js +1 -1
  29. package/dist/commands/sys/settings-shell.d.ts +0 -3
  30. package/dist/commands/sys/settings-shell.js +1 -38
  31. package/dist/commands/sys/settings.js +2 -3
  32. package/dist/commands/sys/setup-core.d.ts +2 -17
  33. package/dist/commands/sys/setup-core.js +9 -33
  34. package/dist/commands/sys/setup-wizard.d.ts +1 -3
  35. package/dist/commands/sys/setup-wizard.js +9 -39
  36. package/dist/commands/sys/setup.js +6 -6
  37. package/dist/commands/sys/sync-project-guidance.js +62 -12
  38. package/dist/core/__tests__/fault-classifier.test.js +20 -0
  39. package/dist/core/__tests__/nested-store-discovery.test.d.ts +1 -0
  40. package/dist/core/__tests__/nested-store-discovery.test.js +89 -0
  41. package/dist/core/__tests__/on-read-nested-store.test.d.ts +1 -0
  42. package/dist/core/__tests__/on-read-nested-store.test.js +117 -0
  43. package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.d.ts +1 -0
  44. package/dist/core/__tests__/plugin-link-symlinked-scope-root.test.js +38 -0
  45. package/dist/core/__tests__/serial/tmux-surface.test.js +8 -5
  46. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +34 -1
  47. package/dist/core/command.js +5 -3
  48. package/dist/core/fault-classifier.d.ts +1 -1
  49. package/dist/core/fault-classifier.js +7 -0
  50. package/dist/core/fs-utils.js +6 -1
  51. package/dist/core/keybindings/index.d.ts +1 -1
  52. package/dist/core/keybindings/index.js +1 -1
  53. package/dist/core/keybindings/match.d.ts +11 -0
  54. package/dist/core/keybindings/match.js +22 -0
  55. package/dist/core/memory/doc-link-grammar.js +3 -7
  56. package/dist/core/memory/inline-ref-guidance.js +1 -1
  57. package/dist/core/memory-resolver.d.ts +7 -1
  58. package/dist/core/memory-resolver.js +20 -7
  59. package/dist/core/nested-stores.d.ts +12 -0
  60. package/dist/core/nested-stores.js +146 -0
  61. package/dist/core/preview-registry.js +4 -2
  62. package/dist/core/runtime/bearings.d.ts +1 -1
  63. package/dist/core/runtime/bearings.js +2 -2
  64. package/dist/core/runtime/broker/fault-retry.js +23 -4
  65. package/dist/core/runtime/broker-extension-render.d.ts +1 -1
  66. package/dist/core/runtime/broker-extension-render.js +2 -2
  67. package/dist/core/runtime/broker-protocol.d.ts +1 -1
  68. package/dist/core/runtime/deliver-live.js +11 -1
  69. package/dist/core/runtime/fault.js +1 -1
  70. package/dist/core/runtime/tmux-bindings.js +42 -20
  71. package/dist/core/runtime/tool-group-summary.js +2 -6
  72. package/dist/core/substrate/injected-store.d.ts +6 -0
  73. package/dist/core/substrate/injected-store.js +19 -0
  74. package/dist/core/substrate/on-read-node.d.ts +4 -2
  75. package/dist/core/substrate/on-read-node.js +5 -3
  76. package/dist/core/substrate/on-read.d.ts +4 -2
  77. package/dist/core/substrate/on-read.js +27 -5
  78. package/dist/pi-extensions/canvas-context-intro.d.ts +1 -1
  79. package/dist/pi-extensions/canvas-context-intro.js +14 -3
  80. package/dist/pi-extensions/canvas-doc-substrate.js +13 -2
  81. package/dist/shared/generated-context.d.ts +4 -0
  82. package/dist/shared/generated-context.js +20 -1
  83. package/dist/shared/tool-groups.d.ts +3 -3
  84. package/dist/shared/tool-groups.js +1 -4
  85. package/package.json +1 -1
  86. package/runtime.lock.json +2 -2
  87. package/scripts/postinstall.mjs +1 -1
  88. package/dist/clients/attach/render/markdown-headings.js +0 -93
  89. package/dist/commands/search/answer.d.ts +0 -1
  90. package/dist/commands/search/answer.js +0 -50
  91. package/dist/commands/search/contents.d.ts +0 -1
  92. package/dist/commands/search/contents.js +0 -96
  93. package/dist/commands/search/exa.d.ts +0 -53
  94. package/dist/commands/search/exa.js +0 -157
  95. package/dist/commands/search/puremd.d.ts +0 -11
  96. package/dist/commands/search/puremd.js +0 -53
  97. package/dist/commands/search/web.d.ts +0 -1
  98. package/dist/commands/search/web.js +0 -65
  99. package/dist/commands/search.d.ts +0 -2
  100. package/dist/commands/search.js +0 -24
  101. package/dist/commands/sys/panels/keys-panel.d.ts +0 -16
  102. package/dist/commands/sys/panels/keys-panel.js +0 -60
@@ -1,12 +1,5 @@
1
1
  import { Key, decodeKittyPrintable, matchesKey, parseKey } from '@earendil-works/pi-tui';
2
- /** pi-tui parses legacy ESC+lowercase as Alt+letter, but does not normalize
3
- * the shifted uppercase form. Convert that complete two-byte form to the
4
- * equivalent CSI-u key so all semantic matching stays in pi-tui. */
5
- function normalizeLegacyShiftedMeta(data) {
6
- if (data.length !== 2 || data[0] !== '\x1b' || !/[A-Z]/.test(data[1]))
7
- return data;
8
- return `\x1b[${data.charCodeAt(1)};4u`;
9
- }
2
+ import { normalizeShiftedMeta } from '../../../core/keybindings/index.js';
10
3
  /** Return text that compose mode inserts. Semantic textual commands deliberately
11
4
  * retain their payload so compose can preserve the old branch order. */
12
5
  export function reviewKeyText(key) {
@@ -30,7 +23,7 @@ export function reviewKeyText(key) {
30
23
  /** Match one complete pi-tui input chunk. pi-tui owns ESC/meta disambiguation;
31
24
  * this function has neither terminal state nor a fallback raw-sequence parser. */
32
25
  export function matchReviewKey(data) {
33
- const normalized = normalizeLegacyShiftedMeta(data);
26
+ const normalized = normalizeShiftedMeta(data);
34
27
  // Compose-only chords precede their unmodified forms. The surface applies
35
28
  // these only while composing, preserving the terminal-review mode ladder.
36
29
  if (matchesKey(normalized, Key.ctrl('j')) || matchesKey(normalized, Key.alt('enter')))
@@ -1,4 +1,5 @@
1
1
  import { decodeKittyPrintable, matchesKey, parseKey } from '@earendil-works/pi-tui';
2
+ import { normalizeShiftedMeta } from '../../../core/keybindings/index.js';
2
3
  function emptyKey() {
3
4
  return {
4
5
  upArrow: false,
@@ -30,14 +31,6 @@ function withFlag(...flags) {
30
31
  function isPlainText(data) {
31
32
  return data.length > 0 && !data.includes('\x1b') && data.charCodeAt(0) >= 0x20;
32
33
  }
33
- /** pi-tui parses legacy ESC+lowercase as Alt+letter, but does not normalize
34
- * the shifted uppercase form. Convert that complete two-byte form to the
35
- * equivalent CSI-u key so all semantic matching stays in pi-tui. */
36
- function normalizeLegacyShiftedMeta(data) {
37
- if (data.length !== 2 || data[0] !== '\x1b' || !/[A-Z]/.test(data[1]))
38
- return data;
39
- return `\x1b[${data.charCodeAt(1)};4u`;
40
- }
41
34
  const BRACKETED_PASTE_START = '\x1b[200~';
42
35
  const BRACKETED_PASTE_END = '\x1b[201~';
43
36
  function bracketedPaste(data) {
@@ -49,7 +42,7 @@ export function decodeInboxKey(data) {
49
42
  const pasted = bracketedPaste(data);
50
43
  if (pasted !== undefined)
51
44
  return { input: pasted, key: emptyKey() };
52
- const normalized = normalizeLegacyShiftedMeta(data);
45
+ const normalized = normalizeShiftedMeta(data);
53
46
  const parsed = parseKey(normalized);
54
47
  if (matchesKey(normalized, 'up'))
55
48
  return { input: '', key: withFlag('upArrow') };
@@ -131,7 +131,7 @@ const addLeaf = defineLeaf({
131
131
  name: 'cron add',
132
132
  summary: 'arm a scheduled command',
133
133
  params: [
134
- { kind: 'stdin', name: 'command', required: true, constraint: "Arbitrary bash the daemon runs at each fire. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Runs via `bash -c` in this cwd with $CRTR_CRON_ID, $CRTR_CRON_NAME, and $CRTR_RUN_ID in the env ($CRTR_ANCHOR too when anchored). The scheduler does not interpret it as a node action; a bash predicate can decide whether this fire acts, not arm a native predicate trigger." },
134
+ { kind: 'stdin', name: 'command', required: true, constraint: "Arbitrary bash the daemon runs at each fire. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Runs via `bash -c` in this cwd with $CRTR_CRON_ID, $CRTR_CRON_NAME, and $CRTR_RUN_ID in the env ($CRTR_ANCHOR too when anchored). The scheduler does not interpret it as a node action; a bash predicate can decide whether this fire acts, not arm a native predicate trigger. A run that must branch on canvas state should call `crtr --json <cmd>` and read the leaf's declared outputs, rather than parsing the prose output." },
135
135
  { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Agent-legible label — how the cron shows in `cron list`.' },
136
136
  { kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'One-shot: run the stored bash once at <when> — a duration ("90s","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", interpreted in --tz else host zone). Exactly one of --at / --every is required. The row is deleted after its run unless the run\'s disposition pauses it.' },
137
137
  { kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'Recurring: run the stored bash at each cadence — a fixed interval ("6h" — first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required. For a gated cron (a command that exits 75 while ineligible), pick a LONG cadence: each natural slot is only the backstop re-check, and the daemon poke supplies the latency — "6h" plus poke beats "15m" polling on both latency and cost.' },
@@ -47,7 +47,7 @@ export const deleteLeaf = defineLeaf({
47
47
  const scopeArg = input['scope'];
48
48
  let doc;
49
49
  try {
50
- doc = resolveMemoryDoc(nameRaw, scopeArg !== undefined ? { scope: scopeArg } : {});
50
+ doc = resolveMemoryDoc(nameRaw, { includeDescendants: true, ...(scopeArg !== undefined ? { scope: scopeArg } : {}) });
51
51
  }
52
52
  catch (e) {
53
53
  if (e instanceof CrtrError && e.code === 'not_found') {
@@ -34,7 +34,7 @@ function candidates(kindFilter, scopeFilter) {
34
34
  units.push(u);
35
35
  };
36
36
  const units = [];
37
- for (const doc of listAllMemoryDocs(scopeFilter)) {
37
+ for (const doc of listAllMemoryDocs(scopeFilter, false, true)) {
38
38
  const sub = parseSubstrateDoc(doc);
39
39
  if (sub === null)
40
40
  continue;
@@ -17,6 +17,7 @@ import { isDocKind, normalizeDocName, resolveDocName, RUNGS } from '../../core/s
17
17
  import { displayName } from '../../core/substrate/ceiling.js';
18
18
  import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
19
19
  import { listAllMemoryDocs } from '../../core/memory-resolver.js';
20
+ import { descendantStoreRoots } from '../../core/nested-stores.js';
20
21
  const VALID_RUNGS = [...RUNGS];
21
22
  const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
22
23
  /** Rung-scaled body-length caps, measured in WORDS (frontmatter excluded).
@@ -157,7 +158,7 @@ export function lintSubstrateSchema(fm) {
157
158
  // Every authored file-read surface needs an explicit event boundary. Runtime
158
159
  // parsing stays tolerant so one bad doc cannot break an agent's context.
159
160
  if (fm['file-read-visibility'] !== 'none' && appliesToGlobs === undefined) {
160
- return `missing applies-to: file-read-visibility is \`${fm['file-read-visibility']}\`, so this memory must declare when it surfaces. Use \`applies-to: "."\` for context loaded when cwd or a selected profile mounts this project store, use a non-empty file glob (or YAML list) for context loaded only after a matching file is read, or set \`file-read-visibility: none\` when this memory has no file-context route. File globs match the read file's absolute path, basename, or path relative to the memory's owning project root. \`read-when\` matches file metadata but does not replace the required workspace/file boundary. Run \`crtr memory write -h\` before changing routing frontmatter.`;
161
+ return `missing applies-to: file-read-visibility is \`${fm['file-read-visibility']}\`, so this memory must declare when it surfaces. Use \`applies-to: "."\` for context loaded when cwd or a selected profile mounts this project store and on any file read beneath the store's owning dir, use a non-empty file glob (or YAML list) for context loaded only after a matching file is read, or set \`file-read-visibility: none\` when this memory has no file-context route. File globs match the read file's absolute path, basename, or path relative to the memory's owning project root. \`read-when\` matches file metadata but does not replace the required workspace/file boundary. Run \`crtr memory write -h\` before changing routing frontmatter.`;
161
162
  }
162
163
  // A dead on-read trigger: an explicit applies-to/read-when with nothing to
163
164
  // surface (file-read-visibility none) can never fire — flag it loudly rather
@@ -256,7 +257,7 @@ export const lintLeaf = defineLeaf({
256
257
  // Exact canonical names in the current resolvable corpus. INDEX docs also
257
258
  // expose their folded bare-directory name (`taste`, not `taste/INDEX`).
258
259
  const corpusNames = new Set();
259
- for (const doc of listAllMemoryDocs(undefined, true)) {
260
+ for (const doc of listAllMemoryDocs(undefined, true, true)) {
260
261
  const canonical = displayName(doc.name);
261
262
  if (canonical !== '')
262
263
  corpusNames.add(canonical);
@@ -280,6 +281,30 @@ export const lintLeaf = defineLeaf({
280
281
  lintDir(pluginMemoryDir(plugin));
281
282
  }
282
283
  }
284
+ // Nested descendant stores: same schema gate, plus a boot-rung warning —
285
+ // the boot catalog deliberately never includes nested stores, so a
286
+ // system-prompt-visibility above `none` there is an inert rung.
287
+ for (const root of descendantStoreRoots(projectScopeRoots())) {
288
+ const dir = join(root, 'memory');
289
+ lintDir(dir);
290
+ if (!pathExists(dir))
291
+ continue;
292
+ for (const file of walkFiles(dir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
293
+ if (basename(file) === 'MEMORY.md')
294
+ continue;
295
+ let fm;
296
+ try {
297
+ fm = parseFrontmatterGeneric(readText(file)).data;
298
+ }
299
+ catch {
300
+ continue; // invalid YAML is already a finding from lintDir
301
+ }
302
+ const sys = fm?.['system-prompt-visibility'];
303
+ if (sys !== undefined && sys !== 'none') {
304
+ warnings.push(`${file}: nested-store docs never ride the boot catalog, so this \`system-prompt-visibility\` rung is inert — set \`none\``);
305
+ }
306
+ }
307
+ }
283
308
  for (const scope of ['user', 'builtin']) {
284
309
  lintDir(scopeMemoryDir(scope));
285
310
  for (const plugin of listInstalledPlugins(scope)) {
@@ -363,7 +388,7 @@ export const lintLeaf = defineLeaf({
363
388
  checked,
364
389
  findings: findings.map((f) => ({ path: f.path, error: f.error })),
365
390
  warnings,
366
- next: 'Fix each doc (quote YAML values containing `: `; use a valid kind/rung/gate; route every non-`none` file-read rung with `applies-to: "."` for workspace-open context or a non-empty file glob for matching reads; otherwise use file-read-visibility `none`; retarget or drop dangling [[links]]; split an over-length body into [[linked]] `none`-visibility reference docs); make every profile-managed project root INDEX match the front-door contract; then re-run `crtr memory lint`.',
391
+ next: 'Fix each doc (quote YAML values containing `: `; use a valid kind/rung/gate; route every non-`none` file-read rung with `applies-to: "."` for workspace-open context plus reads beneath the store\'s owning dir, or a non-empty file glob for matching reads; otherwise use file-read-visibility `none`; retarget or drop dangling [[links]]; split an over-length body into [[linked]] `none`-visibility reference docs); make every profile-managed project root INDEX match the front-door contract; then re-run `crtr memory lint`.',
367
392
  });
368
393
  }
369
394
  return {
@@ -4,7 +4,7 @@ import { parseSubstrateDoc } from '../../core/substrate/schema.js';
4
4
  import { isIndexName, indexDirOf } from '../../core/substrate/ceiling.js';
5
5
  import { paginate } from '../../core/pagination.js';
6
6
  import { MEMORY_KINDS, MEMORY_SCOPES, scopeRank } from './shared.js';
7
- export function listMemoryDocs(kindFilter, scopeFilter, quiet = false, docs = listAllMemoryDocs(scopeFilter, quiet)) {
7
+ export function listMemoryDocs(kindFilter, scopeFilter, quiet = false, docs = listAllMemoryDocs(scopeFilter, quiet, true)) {
8
8
  const seen = new Set(); // "scope/name" → first wins
9
9
  const items = [];
10
10
  const addItem = (item) => {
@@ -37,7 +37,7 @@ export const originLeaf = defineLeaf({
37
37
  const nameRaw = input['name'];
38
38
  let doc;
39
39
  try {
40
- doc = resolveMemoryDoc(nameRaw);
40
+ doc = resolveMemoryDoc(nameRaw, { includeDescendants: true });
41
41
  }
42
42
  catch (e) {
43
43
  if (!(e instanceof CrtrError && e.code === 'not_found'))
@@ -42,7 +42,7 @@ function indexRoutes(doc) {
42
42
  const canonicalName = `${dir}/INDEX`;
43
43
  let corpus;
44
44
  try {
45
- corpus = listAllMemoryDocs(undefined, true);
45
+ corpus = listAllMemoryDocs(undefined, true, true);
46
46
  }
47
47
  catch {
48
48
  return [];
@@ -83,7 +83,7 @@ function indexRoutes(doc) {
83
83
  for (const linkName of docLinkNames(doc.body)) {
84
84
  let linked;
85
85
  try {
86
- linked = resolveMemoryDoc(linkName);
86
+ linked = resolveMemoryDoc(linkName, { includeDescendants: true });
87
87
  }
88
88
  catch {
89
89
  continue; // dangling link — lint's finding, not read's
@@ -142,7 +142,7 @@ export const readLeaf = defineLeaf({
142
142
  // under <pluginName>/), so this resolves every name.
143
143
  let doc;
144
144
  try {
145
- doc = resolveMemoryDoc(nameRaw, { kind: kindFilter });
145
+ doc = resolveMemoryDoc(nameRaw, { kind: kindFilter, includeDescendants: true });
146
146
  }
147
147
  catch (e) {
148
148
  if (!(e instanceof CrtrError && e.code === 'not_found'))
@@ -169,7 +169,7 @@ export const readLeaf = defineLeaf({
169
169
  // `resolveMemoryDoc` permits leaf-name fallback for interactive reads;
170
170
  // a stored graph edge does not. Compare the resolved canonical name
171
171
  // so an old shorthand never masquerades as a first-class doc link.
172
- return displayName(resolveMemoryDoc(linkName).name) === linkName;
172
+ return displayName(resolveMemoryDoc(linkName, { includeDescendants: true }).name) === linkName;
173
173
  }
174
174
  catch {
175
175
  return false;
@@ -35,7 +35,7 @@ export const writeLeaf = defineLeaf({
35
35
  { kind: 'flag', name: 'system-prompt-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much of this document auto-loads into the system prompt / CLI help. Required when creating — there is no kind default; pick a rung explicitly.' },
36
36
  { kind: 'flag', name: 'file-read-visibility', type: 'enum', choices: [...VISIBILITY_RUNGS], required: false, constraint: 'Rung controlling how much surfaces through file context. Required when creating — there is no kind default. Every value except none also requires at least one applies-to route in the resulting document.' },
37
37
  { kind: 'flag', name: 'gate', type: 'string', required: false, constraint: 'Frontmatter gate — YAML/JSON object predicate over node config using the same field/matcher vocabulary described in the guide.' },
38
- { kind: 'flag', name: 'applies-to', type: 'string', required: false, repeatable: true, constraint: 'One explicit file-context route per occurrence. In a project store, `.` fires when cwd/profile mounts that workspace during first-message assembly. Any other value is a glob matched after an actual file read against the absolute path, basename, and path relative to the owning project root. There is no positional fallback from where the memory file lives. Required whenever file-read-visibility is not none.' },
38
+ { kind: 'flag', name: 'applies-to', type: 'string', required: false, repeatable: true, constraint: 'One explicit file-context route per occurrence. In a project store, `.` fires when cwd/profile mounts that workspace during first-message assembly AND on any file read beneath the store\u2019s owning dir. Any other value is a glob matched after an actual file read against the absolute path, basename, and path relative to the owning project root. There is no positional fallback from where the memory file lives. Required whenever file-read-visibility is not none.' },
39
39
  { kind: 'flag', name: 'read-when', type: 'string', required: false, constraint: 'Frontmatter read-when — YAML/JSON object predicate over a read file’s own frontmatter using the same field/matcher vocabulary described in the guide.' },
40
40
  { kind: 'flag', name: 'slash', type: 'bool', required: false, default: false, constraint: 'Presence flags this doc invocable as a pi slash command (`/<name>`, `/` in a nested name rendered as `:`) — the doc body becomes the command’s injected prompt. Default false: most docs are consulted, not invoked.' },
41
41
  { kind: 'flag', name: 'rationale', type: 'string', required: false, constraint: 'Frontmatter rationale — the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting this flag on an update PRESERVES an existing rationale unchanged.' },
@@ -9,7 +9,6 @@ export declare class SettingsShell implements Component, Focusable {
9
9
  private readonly models;
10
10
  private readonly keybindings;
11
11
  private readonly brokerLimits;
12
- private readonly keys;
13
12
  private readonly destinations;
14
13
  private index;
15
14
  /** The rail owns the arrow keys until you step into a panel. */
@@ -18,7 +17,6 @@ export declare class SettingsShell implements Component, Focusable {
18
17
  private cachedWidth;
19
18
  private cachedHeight;
20
19
  private cachedLines;
21
- private keySaveInFlight;
22
20
  onCancel?: () => void;
23
21
  constructor(tui: TUI, agentDir: string, runtime: ModelRuntime, registry: ModelRegistry, bindings: BindingResolution<BindingId>, rawKeybindings: unknown, terminalRows: () => number, rawTmuxPassthrough?: unknown);
24
22
  set focused(value: boolean);
@@ -42,7 +40,6 @@ export declare class SettingsShell implements Component, Focusable {
42
40
  /** Typing a letter on the rail jumps to the destination it starts. */
43
41
  private jumpToInitial;
44
42
  private routeToPanel;
45
- private saveExaKey;
46
43
  private close;
47
44
  dispose(): void;
48
45
  private renderRail;
@@ -5,11 +5,10 @@
5
5
  import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
6
6
  import { configPath } from "../../core/config.js";
7
7
  import { formatGesture, matchesPiTuiInput, } from "../../core/keybindings/index.js";
8
- import { reconcileInboxFrontDoor, resolveExaKeyPath, writeExaKey, } from "./setup-core.js";
8
+ import { reconcileInboxFrontDoor } from "./setup-core.js";
9
9
  import { runSetupSettingsProcess } from "./setup-settings-runner.js";
10
10
  import { BrokerLimitsPanel } from "./panels/broker-limits-panel.js";
11
11
  import { KeybindingsPanel } from "./panels/keybindings-panel.js";
12
- import { KeysPanel } from "./panels/keys-panel.js";
13
12
  import { ModelsPanel } from "./panels/models-panel.js";
14
13
  import { PAGE_CHROME_ROWS, columns, padAnsi, pageFrame, theme, } from "./panels/panel.js";
15
14
  import { PrefsPanel } from "./panels/prefs-panel.js";
@@ -27,7 +26,6 @@ export class SettingsShell {
27
26
  models;
28
27
  keybindings;
29
28
  brokerLimits;
30
- keys;
31
29
  destinations;
32
30
  index = 0;
33
31
  /** The rail owns the arrow keys until you step into a panel. */
@@ -36,7 +34,6 @@ export class SettingsShell {
36
34
  cachedWidth;
37
35
  cachedHeight;
38
36
  cachedLines;
39
- keySaveInFlight = false;
40
37
  onCancel;
41
38
  constructor(tui, agentDir, runtime, registry, bindings, rawKeybindings, terminalRows, rawTmuxPassthrough = undefined) {
42
39
  this.tui = tui;
@@ -49,9 +46,6 @@ export class SettingsShell {
49
46
  this.models.onChange = invalidate;
50
47
  this.keybindings = new KeybindingsPanel(bindings, rawKeybindings, rawTmuxPassthrough, terminalRows, invalidate);
51
48
  this.brokerLimits = new BrokerLimitsPanel(invalidate);
52
- this.keys = new KeysPanel();
53
- this.keys.onSubmit = () => this.saveExaKey();
54
- this.keys.onCancel = () => this.enterRail();
55
49
  const prefs = (group, title, subtitle) => new PrefsPanel(group, title, subtitle, invalidate);
56
50
  this.destinations = [
57
51
  {
@@ -96,7 +90,6 @@ export class SettingsShell {
96
90
  label: "Updates",
97
91
  panel: prefs("updates", "Updates", "How crouter checks for and applies new releases."),
98
92
  },
99
- { id: "keys", group: "Integrations", label: "API keys", panel: this.keys },
100
93
  ];
101
94
  }
102
95
  // --- focus and navigation -------------------------------------------------
@@ -117,7 +110,6 @@ export class SettingsShell {
117
110
  }
118
111
  syncFocus() {
119
112
  const live = this._focused && !this.railFocused;
120
- this.keys.focused = live && this.current.id === "keys";
121
113
  this.keybindings.focused = live && this.current.id === "keybindings";
122
114
  this.brokerLimits.focused = live && this.current.id === "broker-limits";
123
115
  }
@@ -167,14 +159,6 @@ export class SettingsShell {
167
159
  // Threshold fields take digits; a remapped destination gesture must
168
160
  // never swallow a number the user is typing into a limit.
169
161
  return /^\d+$/.test(data);
170
- case "keys":
171
- // The key field takes text and cursor movement; step gestures still
172
- // reach the shell so the field is never a trap.
173
- return ((data.length === 1 && data.charCodeAt(0) >= 32) ||
174
- matchesKey(data, "left") ||
175
- matchesKey(data, "right") ||
176
- matchesKey(data, "up") ||
177
- matchesKey(data, "down"));
178
162
  default:
179
163
  return false;
180
164
  }
@@ -253,31 +237,12 @@ export class SettingsShell {
253
237
  if (this.current.panel.handleInput(data, match))
254
238
  this.invalidate();
255
239
  }
256
- saveExaKey() {
257
- if (this.keySaveInFlight)
258
- return;
259
- const key = this.keys.value;
260
- if (key === "") {
261
- this.keys.setNotice("Paste an Exa key before saving.");
262
- this.invalidate();
263
- return;
264
- }
265
- this.keySaveInFlight = true;
266
- this.keys.setNotice("Saving Exa key…");
267
- this.invalidate();
268
- void writeExaKey(resolveExaKeyPath(), key).then(() => this.keys.setNotice("Saved Exa key.", true), (error) => this.keys.setNotice(`Could not save Exa key: ${error instanceof Error ? error.message : String(error)}`)).finally(() => {
269
- this.keySaveInFlight = false;
270
- this.invalidate();
271
- this.tui.requestRender();
272
- });
273
- }
274
240
  close() {
275
241
  this.onCancel?.();
276
242
  }
277
243
  dispose() {
278
244
  this.providers.dispose();
279
245
  this.models.dispose();
280
- this.keys.focused = false;
281
246
  this.keybindings.focused = false;
282
247
  this.brokerLimits.focused = false;
283
248
  }
@@ -340,8 +305,6 @@ export class SettingsShell {
340
305
  return this.keybindings.footerHint();
341
306
  case "broker-limits":
342
307
  return `${move} field \u00b7 type a value \u00b7 ${this.label("crtr.setup.advance")} save`;
343
- case "keys":
344
- return `paste the key \u00b7 ${this.label("crtr.setup.advance")} save`;
345
308
  default:
346
309
  return `${move} row \u00b7 ${this.label("crtr.setup.toggle")} or ${this.label("crtr.setup.cell-previous")}/${this.label("crtr.setup.cell-next")} change value`;
347
310
  }
@@ -8,7 +8,7 @@ import { defineLeaf } from '../../core/command.js';
8
8
  import { BROKER_LIMIT_OVERRIDE_PRECEDENCE } from './setup.js';
9
9
  export const sysSettingsLeaf = defineLeaf({
10
10
  name: 'settings',
11
- description: 'open the settings page — providers, models, keybindings, display, panes, broker limits, updates, API keys',
11
+ description: 'open the settings page — providers, models, keybindings, display, panes, broker limits, updates',
12
12
  whenToUse: 'a human wants to change crouter configuration interactively. Agents should use sys config get/set, which is scriptable; sys setup installs a fresh machine.',
13
13
  help: {
14
14
  name: 'sys settings',
@@ -17,10 +17,9 @@ export const sysSettingsLeaf = defineLeaf({
17
17
  output: [],
18
18
  outputKind: 'object',
19
19
  effects: [
20
- 'Opens a pi-tui page whose left rail groups ongoing configuration: Intelligence (providers, models), Interface (keybindings, display), Workspace (panes, broker limits, updates), and Integrations (API keys).',
20
+ 'Opens a pi-tui page whose left rail groups ongoing configuration: Intelligence (providers, models), Interface (keybindings, display), and Workspace (panes, broker limits, updates).',
21
21
  'Preference, model-ladder and keybinding edits save to the user config as you make them; saved keybinding changes also refresh crouter-owned tmux bindings without overwriting occupied user keys.',
22
22
  `Broker warning and automatic-revive hard-cap values save under user config brokerThresholds; crtrd reads them on its next supervision tick. ${BROKER_LIMIT_OVERRIDE_PRECEDENCE}`,
23
- 'Saving a replacement Exa key writes it directly to ~/.crouter/exa.key. Machine installs and bootstrap remain under sys setup.',
24
23
  'Requires a TTY. Without one it prints a pointer to sys config and exits.',
25
24
  ],
26
25
  },
@@ -2,7 +2,7 @@ import { type BindingId, type BindingResolution, type KeybindingDiagnostic, type
2
2
  export declare const OFFICIAL_MARKETPLACE_NAME = "crouter-official-marketplace";
3
3
  export declare const OFFICIAL_MARKETPLACE_URL = "https://github.com/crouton-labs/crouter-official-marketplace.git";
4
4
  export declare const OFFICIAL_MARKETPLACE_REF = "main";
5
- export type SetupPackageId = 'official-marketplace' | 'crtr-extensions' | 'oauth-adapter' | 'capture-plugin';
5
+ export type SetupPackageId = 'official-marketplace' | 'crtr-extensions' | 'oauth-adapter' | 'capture-plugin' | 'search-plugin';
6
6
  /** `nerd-font` is not a command on PATH like the other two — it is an installed
7
7
  * font face, detected by fontconfig/font-directory scan. The viewer renders
8
8
  * every tool and command icon with Nerd Font codepoints, so without it a fresh
@@ -36,20 +36,15 @@ export interface SetupSubmission {
36
36
  packages: SetupPackageManifestEntry[];
37
37
  installedPackageIds: Set<SetupPackageId>;
38
38
  systemDeps: SetupSystemInstallPlan[];
39
- exaKey: string;
40
- exaKeyPath: string;
41
39
  }
42
40
  export interface SetupExecutionSummary {
43
41
  installed: string[];
44
42
  skipped: string[];
45
- exaKeyWritten: boolean;
46
- exaKeyPath: string;
47
43
  }
48
44
  export interface SetupInitialState {
49
45
  manifest: SetupPackageManifestEntry[];
50
46
  installedPackageIds: Set<SetupPackageId>;
51
47
  systemPlans: SetupSystemInstallPlan[];
52
- exaKeyPath: string;
53
48
  }
54
49
  export interface KeybindingEditResult {
55
50
  readonly ok: boolean;
@@ -96,15 +91,6 @@ export declare function detectCommandOnPath(command: string): boolean;
96
91
  export declare function detectPackageManager(platform?: NodeJS.Platform, commandExists?: (command: string) => boolean): PackageManager;
97
92
  export declare function buildSystemInstallPlan(tool: SetupTool, packageManager: PackageManager, commandExists?: (command: string) => boolean): SetupSystemInstallPlan;
98
93
  export declare function resolveSettingsPath(homeDir?: string): string;
99
- export declare function resolveExaKeyPath(homeDir?: string): string;
100
- /** Where an already-present API key is resolved from — env var wins over the
101
- * key file, matching the runtime resolver in search/exa.ts. */
102
- export type ApiKeySource = 'env' | 'file';
103
- export interface ApiKeyPresence {
104
- present: boolean;
105
- source?: ApiKeySource;
106
- }
107
- export declare function detectExaKey(homeDir?: string): ApiKeyPresence;
108
94
  export declare function buildSetupInitialState(opts?: {
109
95
  manifest?: SetupPackageManifestEntry[];
110
96
  settingsPath?: string;
@@ -112,8 +98,7 @@ export declare function buildSetupInitialState(opts?: {
112
98
  packageManager?: PackageManager;
113
99
  commandExists?: (command: string) => boolean;
114
100
  }): SetupInitialState;
115
- export declare function renderSetupStaticInstructions(state: Pick<SetupInitialState, 'manifest' | 'installedPackageIds' | 'systemPlans' | 'exaKeyPath'>): string;
116
- export declare function writeExaKey(exaKeyPath: string, exaKey: string): Promise<void>;
101
+ export declare function renderSetupStaticInstructions(state: SetupInitialState): string;
117
102
  /** Refresh crouter-owned tmux bindings. Setup must never fail because tmux is unavailable. */
118
103
  export declare function reconcileInboxFrontDoor(): Promise<void>;
119
104
  export declare function formatSetupExecutionSummary(summary: SetupExecutionSummary): string;
@@ -1,6 +1,4 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { chmod, mkdir, writeFile } from 'node:fs/promises';
3
- import { readFileSync } from 'node:fs';
4
2
  import { homedir } from 'node:os';
5
3
  import { basename, dirname, join, resolve } from 'node:path';
6
4
  import { isDeepStrictEqual } from 'node:util';
@@ -193,18 +191,25 @@ const PACKAGE_COPY = {
193
191
  shortDescription: 'crtr plugin mounting `crtr capture` — browser automation and UI measurement over CDP.',
194
192
  longDescription: 'Installs the `capture` plugin from the official crtr marketplace. It mounts `crtr capture` as a transparent passthrough to the external Capture CLI (`npm install -g @crouton-kit/capture`) for CDP screenshots, rendered-geometry measurement, HAR, accessibility trees, and JS execution against a live page. Uncheck it on a machine where agents never drive a browser — the command then costs nothing in their root prompt.',
195
193
  },
194
+ 'search-plugin': {
195
+ name: 'search',
196
+ shortDescription: 'crtr plugin mounting `crtr search` — public-web discovery, grounded answers, and content extraction.',
197
+ longDescription: 'Installs the `search` plugin from the official crtr marketplace. It mounts `crtr search` and calls Exa directly, resolving its own API key from EXA_API_KEY or ~/.crouter/exa.key — crouter never handles that credential. Uncheck it on a machine whose agents get web search from somewhere else, such as a Northlight home where the platform serves `crtr search` from its own plugin.',
198
+ },
196
199
  };
197
200
  const PACKAGE_SOURCES = {
198
201
  'official-marketplace': { kind: 'marketplace-source', source: OFFICIAL_MARKETPLACE_URL },
199
202
  'crtr-extensions': { kind: 'local-path', source: builtinPiPackageDir('pi-crtr-extensions') },
200
203
  'oauth-adapter': { kind: 'npm', source: 'npm:pi-claude-oauth-adapter@0.1.2' },
201
204
  'capture-plugin': { kind: 'marketplace-plugin', source: 'crouter-official-marketplace/capture' },
205
+ 'search-plugin': { kind: 'marketplace-plugin', source: 'crouter-official-marketplace/search' },
202
206
  };
203
207
  const LOCAL_PACKAGE_NAME_ALIASES = {
204
208
  'official-marketplace': [],
205
209
  'crtr-extensions': ['pi-crtr-extensions'],
206
210
  'oauth-adapter': [],
207
211
  'capture-plugin': [],
212
+ 'search-plugin': [],
208
213
  };
209
214
  export function buildSetupManifest(resolveBuiltinPackageDir = builtinPiPackageDir) {
210
215
  // Order matters at execution: the marketplace source registers before the
@@ -214,6 +219,7 @@ export function buildSetupManifest(resolveBuiltinPackageDir = builtinPiPackageDi
214
219
  'crtr-extensions',
215
220
  'oauth-adapter',
216
221
  'capture-plugin',
222
+ 'search-plugin',
217
223
  ].map((id) => {
218
224
  const copy = PACKAGE_COPY[id];
219
225
  const source = PACKAGE_SOURCES[id].kind === 'local-path'
@@ -394,22 +400,6 @@ export function buildSystemInstallPlan(tool, packageManager, commandExists = det
394
400
  export function resolveSettingsPath(homeDir = homedir()) {
395
401
  return join(homeDir, '.pi', 'agent', 'settings.json');
396
402
  }
397
- export function resolveExaKeyPath(homeDir = homedir()) {
398
- return join(homeDir, '.crouter', 'exa.key');
399
- }
400
- function detectApiKey(envVar, keyPath) {
401
- if ((process.env[envVar] ?? '').trim() !== '')
402
- return { present: true, source: 'env' };
403
- try {
404
- if (readFileSync(keyPath, 'utf8').trim() !== '')
405
- return { present: true, source: 'file' };
406
- }
407
- catch { /* absent/unreadable → not present */ }
408
- return { present: false };
409
- }
410
- export function detectExaKey(homeDir = homedir()) {
411
- return detectApiKey('EXA_API_KEY', resolveExaKeyPath(homeDir));
412
- }
413
403
  export function buildSetupInitialState(opts = {}) {
414
404
  const homeDir = opts.homeDir ?? homedir();
415
405
  const settingsPath = opts.settingsPath ?? resolveSettingsPath(homeDir);
@@ -421,7 +411,7 @@ export function buildSetupInitialState(opts = {}) {
421
411
  const commandExists = opts.commandExists ?? detectCommandOnPath;
422
412
  const packageManager = opts.packageManager ?? detectPackageManager(process.platform, commandExists);
423
413
  const systemPlans = ['tmux', 'nvim', 'nerd-font'].map((tool) => buildSystemInstallPlan(tool, packageManager, commandExists));
424
- return { manifest, installedPackageIds, systemPlans, exaKeyPath: resolveExaKeyPath(homeDir) };
414
+ return { manifest, installedPackageIds, systemPlans };
425
415
  }
426
416
  export function renderSetupStaticInstructions(state) {
427
417
  const packageLines = state.manifest.map((entry) => {
@@ -446,8 +436,6 @@ export function renderSetupStaticInstructions(state) {
446
436
  'System deps:',
447
437
  ...systemLines,
448
438
  '',
449
- `- Exa key: write to ${state.exaKeyPath} or set EXA_API_KEY`,
450
- '',
451
439
  `Inbox: the inbox-toggle key (${inboxShortcut() ?? 'disabled'}) is a crouter keybinding — view or rebind it in \`crtr sys settings\`; it is installed on the same tmux server as every other crouter binding.`,
452
440
  'Run `crtr sys setup` in a TTY to open the wizard.',
453
441
  ].join('\n') + '\n';
@@ -474,11 +462,6 @@ function ensureManagedRenderer() {
474
462
  ensureRenderer();
475
463
  return isRendererReady() ? 'provisioned' : 'unavailable';
476
464
  }
477
- export async function writeExaKey(exaKeyPath, exaKey) {
478
- await mkdir(dirname(exaKeyPath), { recursive: true });
479
- await writeFile(exaKeyPath, `${exaKey}\n`, 'utf8');
480
- await chmod(exaKeyPath, 0o600).catch(() => undefined);
481
- }
482
465
  /** Refresh crouter-owned tmux bindings. Setup must never fail because tmux is unavailable. */
483
466
  export async function reconcileInboxFrontDoor() {
484
467
  try {
@@ -491,7 +474,6 @@ export function formatSetupExecutionSummary(summary) {
491
474
  'crtr sys setup complete',
492
475
  summary.installed.length > 0 ? `installed: ${summary.installed.join(', ')}` : 'installed: none',
493
476
  summary.skipped.length > 0 ? `skipped: ${summary.skipped.join(', ')}` : 'skipped: none',
494
- summary.exaKeyWritten ? `exa key written: ${summary.exaKeyPath}` : 'exa key written: no',
495
477
  ].join('\n') + '\n';
496
478
  }
497
479
  /** Marketplace manifest entries already present in any scope — the marketplace
@@ -587,17 +569,11 @@ export async function executeSetupSubmission(submission) {
587
569
  skippedPackages.push('termrender renderer (unavailable — install uv)');
588
570
  break;
589
571
  }
590
- const exaKeyWritten = submission.exaKey.trim() !== '';
591
- if (exaKeyWritten) {
592
- await writeExaKey(submission.exaKeyPath, submission.exaKey);
593
- }
594
572
  // A fresh-machine run can install tmux after preflight reconciliation, so
595
573
  // refresh the front-door bindings once the setup transaction has completed.
596
574
  await reconcileInboxFrontDoor();
597
575
  return {
598
576
  installed: [...installedPackages, ...installedSystemDeps],
599
577
  skipped: [...skippedPackages, ...skippedSystemDeps],
600
- exaKeyWritten,
601
- exaKeyPath: submission.exaKeyPath,
602
578
  };
603
579
  }
@@ -10,7 +10,6 @@ export declare class SetupWizard implements Component, Focusable {
10
10
  private readonly providers;
11
11
  private readonly plugins;
12
12
  private readonly dependencies;
13
- private readonly keys;
14
13
  private step;
15
14
  private confirming;
16
15
  private cachedWidth;
@@ -24,7 +23,6 @@ export declare class SetupWizard implements Component, Focusable {
24
23
  set focused(value: boolean);
25
24
  get focused(): boolean;
26
25
  invalidate(): void;
27
- private syncFocus;
28
26
  private matches;
29
27
  private label;
30
28
  /** Gestures safe to advertise while a text field owns the keyboard. */
@@ -40,7 +38,7 @@ export declare class SetupWizard implements Component, Focusable {
40
38
  private finalize;
41
39
  private currentPanel;
42
40
  handleInput(data: string): void;
43
- /** `Providers › Plugins › Dependencies › Exa key`, active step lit, done
41
+ /** `Providers › Plugins › Dependencies`, active step lit, done
44
42
  * steps checked — a first run should always show how much is left. */
45
43
  private renderTrail;
46
44
  private footer;