@crouton-kit/crouter 0.3.18 → 0.3.19

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 (257) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/orchestrator.md +1 -1
  12. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  13. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  14. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  15. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  16. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  17. package/dist/builtin-personas/runtime-base.md +6 -3
  18. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  19. package/dist/builtin-personas/waiting.md +8 -0
  20. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  21. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  22. package/dist/builtin-views/_lib/states.mjs +161 -0
  23. package/dist/builtin-views/canvas/client.mjs +303 -0
  24. package/dist/builtin-views/canvas/view.mjs +576 -0
  25. package/dist/builtin-views/git-pr/client.mjs +440 -0
  26. package/dist/builtin-views/git-pr/view.mjs +675 -0
  27. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  28. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  29. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  30. package/dist/builtin-views/inbox/view.mjs +889 -0
  31. package/dist/builtin-views/linkedin/client.mjs +610 -0
  32. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  34. package/dist/clients/attach/attach-cmd.js +354 -0
  35. package/dist/clients/attach/chat-view.d.ts +77 -0
  36. package/dist/clients/attach/chat-view.js +450 -0
  37. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  38. package/dist/clients/attach/clipboard-image.js +113 -0
  39. package/dist/clients/attach/config-load.d.ts +31 -0
  40. package/dist/clients/attach/config-load.js +113 -0
  41. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  42. package/dist/clients/attach/extension-dialogs.js +101 -0
  43. package/dist/clients/attach/input-controller.d.ts +54 -0
  44. package/dist/clients/attach/input-controller.js +204 -0
  45. package/dist/clients/attach/slash-commands.d.ts +36 -0
  46. package/dist/clients/attach/slash-commands.js +200 -0
  47. package/dist/clients/attach/view-socket.d.ts +48 -0
  48. package/dist/clients/attach/view-socket.js +126 -0
  49. package/dist/commands/attention.js +3 -3
  50. package/dist/commands/canvas-prune.js +1 -1
  51. package/dist/commands/daemon.js +3 -2
  52. package/dist/commands/human/prompts.js +1 -1
  53. package/dist/commands/human/queue.js +43 -8
  54. package/dist/commands/human/shared.d.ts +2 -0
  55. package/dist/commands/memory/find.d.ts +1 -0
  56. package/dist/commands/memory/find.js +180 -0
  57. package/dist/commands/memory/lint.d.ts +1 -0
  58. package/dist/commands/memory/lint.js +140 -0
  59. package/dist/commands/memory/list.d.ts +1 -0
  60. package/dist/commands/memory/list.js +79 -0
  61. package/dist/commands/memory/read.js +103 -0
  62. package/dist/commands/memory/shared.d.ts +30 -0
  63. package/dist/commands/memory/shared.js +122 -0
  64. package/dist/commands/memory/write.d.ts +1 -0
  65. package/dist/commands/memory/write.js +85 -0
  66. package/dist/commands/memory.d.ts +2 -0
  67. package/dist/commands/memory.js +27 -0
  68. package/dist/commands/node.d.ts +3 -2
  69. package/dist/commands/node.js +607 -61
  70. package/dist/commands/pkg/market-manage.js +1 -1
  71. package/dist/commands/push.js +6 -6
  72. package/dist/commands/revive.js +1 -1
  73. package/dist/commands/skill/author.js +1 -1
  74. package/dist/commands/skill/shared.d.ts +1 -8
  75. package/dist/commands/skill/shared.js +2 -55
  76. package/dist/commands/skill.js +9 -14
  77. package/dist/commands/sys/doctor.js +1 -1
  78. package/dist/commands/sys/update.js +1 -1
  79. package/dist/commands/view-cycle.d.ts +2 -0
  80. package/dist/commands/view-cycle.js +125 -0
  81. package/dist/commands/view-list.d.ts +2 -0
  82. package/dist/commands/view-list.js +66 -0
  83. package/dist/commands/view-new.d.ts +2 -0
  84. package/dist/commands/view-new.js +70 -0
  85. package/dist/commands/view-pick.d.ts +2 -0
  86. package/dist/commands/view-pick.js +119 -0
  87. package/dist/commands/view-run.d.ts +2 -0
  88. package/dist/commands/view-run.js +191 -0
  89. package/dist/commands/view.d.ts +2 -0
  90. package/dist/commands/view.js +29 -0
  91. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  92. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  93. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  94. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  95. package/dist/core/__tests__/cascade-close.test.js +12 -2
  96. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  97. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  98. package/dist/core/__tests__/context-intro.test.js +76 -62
  99. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  100. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  101. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  102. package/dist/core/__tests__/detach-focus.test.js +206 -0
  103. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  104. package/dist/core/__tests__/draw-style.test.js +258 -0
  105. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  106. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  107. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  108. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  109. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  110. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  111. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  112. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  113. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  114. package/dist/core/__tests__/helpers/harness.js +111 -1
  115. package/dist/core/__tests__/home-session.test.js +41 -8
  116. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  117. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  118. package/dist/core/__tests__/kickoff.test.js +37 -3
  119. package/dist/core/__tests__/live-mutation.test.js +50 -33
  120. package/dist/core/__tests__/memory.test.js +23 -115
  121. package/dist/core/__tests__/placement-focus.test.js +5 -0
  122. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  123. package/dist/core/__tests__/relaunch.test.js +4 -3
  124. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  125. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  126. package/dist/core/__tests__/spawn-root.test.js +10 -0
  127. package/dist/core/__tests__/spike-harness.test.js +1 -0
  128. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  129. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  130. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  131. package/dist/core/__tests__/wake-origin.test.js +110 -0
  132. package/dist/core/bootstrap.js +1 -1
  133. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  134. package/dist/core/canvas/browse/app.js +24 -2
  135. package/dist/core/canvas/browse/model.d.ts +38 -2
  136. package/dist/core/canvas/browse/model.js +134 -10
  137. package/dist/core/canvas/browse/render.d.ts +6 -12
  138. package/dist/core/canvas/browse/render.js +72 -104
  139. package/dist/core/canvas/canvas.js +15 -22
  140. package/dist/core/canvas/db.js +46 -0
  141. package/dist/core/canvas/index.d.ts +1 -0
  142. package/dist/core/canvas/index.js +1 -0
  143. package/dist/core/canvas/pid.d.ts +4 -0
  144. package/dist/core/canvas/pid.js +23 -0
  145. package/dist/core/canvas/render.d.ts +11 -2
  146. package/dist/core/canvas/render.js +69 -0
  147. package/dist/core/canvas/types.d.ts +85 -3
  148. package/dist/core/canvas/wakeups.d.ts +76 -0
  149. package/dist/core/canvas/wakeups.js +185 -0
  150. package/dist/core/config.js +4 -1
  151. package/dist/core/frontmatter.js +37 -124
  152. package/dist/core/memory-resolver.d.ts +49 -0
  153. package/dist/core/memory-resolver.js +141 -0
  154. package/dist/core/personas/loader.d.ts +7 -0
  155. package/dist/core/personas/loader.js +36 -5
  156. package/dist/core/personas/resolve.js +13 -3
  157. package/dist/core/predicate.d.ts +63 -0
  158. package/dist/core/predicate.js +189 -0
  159. package/dist/core/resolver.js +26 -5
  160. package/dist/core/runtime/bearings.d.ts +53 -12
  161. package/dist/core/runtime/bearings.js +132 -59
  162. package/dist/core/runtime/broker-cli.d.ts +1 -0
  163. package/dist/core/runtime/broker-cli.js +46 -0
  164. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  165. package/dist/core/runtime/broker-protocol.js +153 -0
  166. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  167. package/dist/core/runtime/broker-sdk.js +72 -0
  168. package/dist/core/runtime/broker.d.ts +55 -0
  169. package/dist/core/runtime/broker.js +1128 -0
  170. package/dist/core/runtime/close.js +35 -6
  171. package/dist/core/runtime/host.d.ts +53 -0
  172. package/dist/core/runtime/host.js +186 -0
  173. package/dist/core/runtime/kickoff.d.ts +2 -1
  174. package/dist/core/runtime/kickoff.js +91 -5
  175. package/dist/core/runtime/launch.d.ts +45 -2
  176. package/dist/core/runtime/launch.js +65 -2
  177. package/dist/core/runtime/lifecycle.js +23 -6
  178. package/dist/core/runtime/memory.d.ts +2 -42
  179. package/dist/core/runtime/memory.js +11 -162
  180. package/dist/core/runtime/nodes.d.ts +33 -0
  181. package/dist/core/runtime/nodes.js +59 -1
  182. package/dist/core/runtime/persona.js +21 -11
  183. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  184. package/dist/core/runtime/pi-vendored.js +49 -0
  185. package/dist/core/runtime/placement.d.ts +32 -14
  186. package/dist/core/runtime/placement.js +206 -52
  187. package/dist/core/runtime/promote.d.ts +0 -6
  188. package/dist/core/runtime/promote.js +1 -12
  189. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  190. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  191. package/dist/core/runtime/reset.js +8 -6
  192. package/dist/core/runtime/revive.d.ts +2 -0
  193. package/dist/core/runtime/revive.js +34 -34
  194. package/dist/core/runtime/spawn.d.ts +19 -0
  195. package/dist/core/runtime/spawn.js +75 -22
  196. package/dist/core/runtime/stop-guard.d.ts +1 -1
  197. package/dist/core/runtime/stop-guard.js +6 -1
  198. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  199. package/dist/core/runtime/tmux-chrome.js +1 -1
  200. package/dist/core/runtime/tmux.d.ts +28 -0
  201. package/dist/core/runtime/tmux.js +80 -6
  202. package/dist/core/scope.d.ts +11 -0
  203. package/dist/core/scope.js +39 -0
  204. package/dist/core/spawn.d.ts +5 -0
  205. package/dist/core/spawn.js +20 -2
  206. package/dist/core/substrate/gate.d.ts +13 -0
  207. package/dist/core/substrate/gate.js +21 -0
  208. package/dist/core/substrate/index.d.ts +7 -0
  209. package/dist/core/substrate/index.js +18 -0
  210. package/dist/core/substrate/on-read.d.ts +14 -0
  211. package/dist/core/substrate/on-read.js +292 -0
  212. package/dist/core/substrate/render.d.ts +25 -0
  213. package/dist/core/substrate/render.js +256 -0
  214. package/dist/core/substrate/schema.d.ts +76 -0
  215. package/dist/core/substrate/schema.js +124 -0
  216. package/dist/core/substrate/session-cache.d.ts +30 -0
  217. package/dist/core/substrate/session-cache.js +77 -0
  218. package/dist/core/substrate/subject.d.ts +41 -0
  219. package/dist/core/substrate/subject.js +54 -0
  220. package/dist/core/tui/contract.d.ts +83 -0
  221. package/dist/core/tui/contract.js +8 -0
  222. package/dist/core/tui/draw.d.ts +96 -0
  223. package/dist/core/tui/draw.js +339 -0
  224. package/dist/core/tui/host.d.ts +29 -0
  225. package/dist/core/tui/host.js +379 -0
  226. package/dist/core/tui/loader.d.ts +16 -0
  227. package/dist/core/tui/loader.js +94 -0
  228. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  229. package/dist/core/wake.d.ts +86 -0
  230. package/dist/core/wake.js +308 -0
  231. package/dist/daemon/crtrd.d.ts +29 -4
  232. package/dist/daemon/crtrd.js +662 -46
  233. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  234. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  235. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  236. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  237. package/dist/pi-extensions/canvas-commands.js +10 -0
  238. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  239. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  240. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  241. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  242. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  243. package/dist/pi-extensions/canvas-nav.js +87 -23
  244. package/dist/pi-extensions/canvas-stophook.js +17 -8
  245. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  246. package/dist/pi-extensions/canvas-view.js +75 -0
  247. package/dist/prompts/skill.js +19 -26
  248. package/dist/prompts/view.d.ts +7 -0
  249. package/dist/prompts/view.js +101 -0
  250. package/dist/types.d.ts +4 -0
  251. package/dist/types.js +1 -0
  252. package/package.json +9 -4
  253. package/dist/commands/skill/find.d.ts +0 -4
  254. package/dist/commands/skill/find.js +0 -257
  255. package/dist/commands/skill/read.js +0 -91
  256. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  257. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,379 @@
1
+ // host.ts — the alt-screen loop that hosts a ViewModule.
2
+ //
3
+ // Models browse/app.ts's loop, generalized: the host owns the screen, input,
4
+ // chrome, and the single-flight async lane; the view paints into the `Draw` it's
5
+ // handed and returns a `ViewAction` per keystroke.
6
+ //
7
+ // • TTY gate: !process.stdin.isTTY → view.dump(state) to stdout, exit 0.
8
+ // • setup/restore terminal (alt-screen, raw); restore exactly once, however we
9
+ // leave (quit / Ctrl-C / crash / process exit).
10
+ // • single-flight async lane: at most ONE async hook (refresh or an async
11
+ // onKey) in flight; a busy indicator shows in the chrome; keystrokes that
12
+ // arrive mid-flight are DROPPED (this paces fetches/sends).
13
+ // • chrome: title row + separator on top; an error banner + a footer (status
14
+ // left, keymap hints right) on the bottom; the view gets the content Rect.
15
+ // • loop: parseKeypress → view.onKey → ViewAction → render; refreshMs polling;
16
+ // resize → render (NOT refresh) so a resize mid-fetch repaints from current
17
+ // state without re-entering the in-flight hook.
18
+ import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from './terminal.js';
19
+ import { createDraw, detectColorCaps } from './draw.js';
20
+ // Numeric SGR codes only (a color NAME emits a broken CSI — see draw.ts guard).
21
+ const FG = { cyan: '36', green: '32', yellow: '33', red: '31', grey: '90' };
22
+ /** The persistent state signal: word + glyph + hue, derived from host signals so
23
+ * every view gets it for free. Each pairs hue with a glyph/word — mono-safe. */
24
+ const CHIP = {
25
+ working: { word: 'working', glyph: '⟳', fg: FG.cyan },
26
+ blocked: { word: 'blocked', glyph: '⚠', fg: FG.red },
27
+ attention: { word: 'attention', glyph: '⚠', fg: FG.yellow },
28
+ ready: { word: 'ready', glyph: '●', fg: FG.green },
29
+ idle: { word: 'idle', glyph: '◌', fg: FG.grey },
30
+ };
31
+ /** Spinner frames for the working chip (animated only while busy). */
32
+ const SPINNER = ['⟳', '⟲'];
33
+ /** Explicit interaction modes flip the chip to the compose accent (yellow `33`)
34
+ * so entering an input mode is unmistakable. Known modes get a tailored glyph;
35
+ * any other mode word falls back to `✎`. Color is yellow; the glyph + bold are
36
+ * the mono carrier (survive NO_COLOR). */
37
+ const MODE_GLYPH = { compose: '✎', react: '☺' };
38
+ const MODE_GLYPH_FALLBACK = '✎';
39
+ /** Banner glyph + hue by severity (color never carries meaning alone: the glyph
40
+ * + bold survive NO_COLOR). */
41
+ const BANNER = {
42
+ info: { glyph: 'ℹ', fg: FG.cyan },
43
+ action: { glyph: '▸', fg: FG.yellow },
44
+ error: { glyph: '✗', fg: FG.red },
45
+ };
46
+ /** Derive the one state chip from host signals (resume guidance): busy→working,
47
+ * error banner→blocked, action banner→attention, else ready (once loaded) / idle.
48
+ * An info banner does not block — the chip stays ready/idle and the cyan banner
49
+ * carries the notice. */
50
+ function deriveState(c) {
51
+ if (c.busy)
52
+ return 'working';
53
+ if (c.banner?.level === 'error')
54
+ return 'blocked';
55
+ if (c.banner?.level === 'action')
56
+ return 'attention';
57
+ return c.loaded ? 'ready' : 'idle';
58
+ }
59
+ /** Color-code the transient footer status by kind (not dim, so it leads): a
60
+ * trailing … or an in-progress verb → cyan; a completed verb → green; else plain.
61
+ * The words carry meaning; the hue is decorative (mono-safe). */
62
+ function statusStyle(s) {
63
+ if (/…$/.test(s) || /^(loading|sending|reacting|opening|working|refreshing)\b/i.test(s))
64
+ return { fg: FG.cyan };
65
+ if (/^(sent|reacted|done|saved|caught up)\b/i.test(s) || /\bsent\b/i.test(s))
66
+ return { fg: FG.green };
67
+ return {};
68
+ }
69
+ /** Visible width of a span group. */
70
+ function spanWidth(spans) {
71
+ let n = 0;
72
+ for (const s of spans)
73
+ n += Array.from(s.text).length;
74
+ return n;
75
+ }
76
+ /** Compact relative-time cue for "updated <rel>": now/Ns/Nm/Nh/Nd. */
77
+ function relTime(deltaMs) {
78
+ const s = Math.max(0, Math.floor(deltaMs / 1000));
79
+ if (s < 5)
80
+ return 'just now';
81
+ if (s < 60)
82
+ return `${s}s`;
83
+ const m = Math.floor(s / 60);
84
+ if (m < 60)
85
+ return `${m}m`;
86
+ const h = Math.floor(m / 60);
87
+ if (h < 24)
88
+ return `${h}h`;
89
+ return `${Math.floor(h / 24)}d`;
90
+ }
91
+ /** Build the right-flushed keymap span group, applying the overflow ladder: full
92
+ * (key bold + label dim) → keys only → drop trailing hints (keep the last, q quit)
93
+ * → the last hint alone. Returns spans ≤ `avail` where possible. */
94
+ function keymapSpans(hints, avail) {
95
+ if (hints.length === 0 || avail <= 0)
96
+ return [];
97
+ const sep = () => ({ text: ' · ', style: { dim: true } });
98
+ const build = (hs, labels) => {
99
+ const out = [];
100
+ hs.forEach((h, i) => {
101
+ if (i > 0)
102
+ out.push(sep());
103
+ out.push({ text: h.keys, style: { bold: true } });
104
+ if (labels)
105
+ out.push({ text: ` ${h.label}`, style: { dim: true } });
106
+ });
107
+ return out;
108
+ };
109
+ let spans = build(hints, true);
110
+ if (spanWidth(spans) <= avail)
111
+ return spans;
112
+ spans = build(hints, false);
113
+ if (spanWidth(spans) <= avail)
114
+ return spans;
115
+ const last = hints[hints.length - 1];
116
+ for (let keep = hints.length - 1; keep > 0; keep--) {
117
+ spans = build([...hints.slice(0, keep), last], false);
118
+ if (spanWidth(spans) <= avail)
119
+ return spans;
120
+ }
121
+ return build([last], false); // may still overflow — spansRight left-clips with …
122
+ }
123
+ /** Draw the host chrome into `draw` and return the content Rect for the view.
124
+ * Three zones: title row (state rail + title/subtitle + state chip + liveness),
125
+ * a hairline, and a footer (status left, keymap right) with a severity banner
126
+ * on the row above it when set. */
127
+ function drawChrome(draw, size, manifest, c, now = Date.now()) {
128
+ const { cols, rows } = size;
129
+ const st = deriveState(c);
130
+ // Chip selection. An explicit interaction mode (compose/react) WINS the chip
131
+ // over the derived state — even while busy — so entering input is unmistakable
132
+ // (precedence: setMode > derived). The mode chip is the yellow compose accent;
133
+ // only the derived `working` chip animates its spinner.
134
+ let chipWord;
135
+ let chipGlyph;
136
+ let chipFg;
137
+ let animate = false;
138
+ if (c.mode) {
139
+ chipWord = c.mode;
140
+ chipGlyph = MODE_GLYPH[c.mode] ?? MODE_GLYPH_FALLBACK;
141
+ chipFg = FG.yellow;
142
+ }
143
+ else {
144
+ const chip = CHIP[st];
145
+ chipWord = chip.word;
146
+ chipGlyph = chip.glyph;
147
+ chipFg = chip.fg;
148
+ animate = st === 'working';
149
+ }
150
+ const chipStyle = { fg: chipFg, bold: true };
151
+ // ── Title row: right cluster first (measure), then title clipped to fit. ──
152
+ const glyph = animate ? (SPINNER[c.tick % SPINNER.length] ?? chipGlyph) : chipGlyph;
153
+ const rightCluster = [{ text: `${glyph} ${chipWord}`, style: chipStyle }];
154
+ if (c.loaded && c.lastRefresh > 0) {
155
+ rightCluster.push({ text: ` · updated ${relTime(now - c.lastRefresh)}`, style: { dim: true } });
156
+ }
157
+ const rightW = spanWidth(rightCluster);
158
+ draw.text(0, 0, '▎', chipStyle); // state rail (always drawn; word carries meaning in mono)
159
+ // Dynamic subtitle overrides the static manifest default; null ⇒ manifest.
160
+ const subtitle = c.subtitle ?? manifest.subtitle;
161
+ const titleSpans = [{ text: manifest.title, style: { bold: true } }];
162
+ if (subtitle)
163
+ titleSpans.push({ text: ` · ${subtitle}`, style: { dim: true } });
164
+ draw.spans(0, 2, titleSpans, Math.max(0, cols - 2 - rightW - 1));
165
+ draw.spansRight(0, cols, rightCluster, rightW);
166
+ draw.hline(1, 0, cols); // hairline separator (dim)
167
+ // ── Footer: status left (color-coded, not dim), keymap right. ──
168
+ const footerRow = rows - 1;
169
+ const status = c.status ?? '';
170
+ const statusW = Math.min(spanWidth([{ text: status }]), Math.max(0, Math.floor(cols / 2)));
171
+ if (status)
172
+ draw.spans(footerRow, 0, [{ text: status, style: statusStyle(status) }], statusW);
173
+ const avail = Math.max(0, cols - statusW - 1);
174
+ draw.spansRight(footerRow, cols, keymapSpans(manifest.keymap ?? [], avail), avail);
175
+ // ── Banner (bottom-1, only when set): severity-coded, full-width. ──
176
+ let bottomRows = 1;
177
+ if (c.banner) {
178
+ const b = BANNER[c.banner.level];
179
+ draw.spans(footerRow - 1, 0, [
180
+ { text: `${b.glyph} `, style: { fg: b.fg, bold: true } },
181
+ { text: c.banner.msg, style: { bold: true } },
182
+ ], cols);
183
+ bottomRows = 2;
184
+ }
185
+ const top = 2; // title + separator
186
+ const height = Math.max(1, rows - top - bottomRows);
187
+ return { row: top, col: 0, width: cols, height };
188
+ }
189
+ export { drawChrome };
190
+ /** Host a view in the alt screen until it quits (or Ctrl-C). */
191
+ export async function runView(view, opts = {}) {
192
+ const options = Object.freeze({ ...(opts.options ?? {}) });
193
+ const chrome = { status: null, banner: null, busy: false, loaded: false, lastRefresh: 0, tick: 0, subtitle: null, mode: null };
194
+ const host = {
195
+ options,
196
+ setStatus(msg) { chrome.status = msg; },
197
+ setBanner(msg, level) { chrome.banner = msg == null ? null : { msg, level }; },
198
+ setError(msg) { chrome.banner = msg == null ? null : { msg, level: 'error' }; },
199
+ setSubtitle(s) { chrome.subtitle = s; },
200
+ setMode(mode) { chrome.mode = mode; },
201
+ };
202
+ // ── Non-TTY / piped path: build state, best-effort load, dump, exit 0. ──
203
+ if (!process.stdin.isTTY) {
204
+ const state = await view.init(host);
205
+ if (view.refresh) {
206
+ try {
207
+ await view.refresh(state, host);
208
+ }
209
+ catch { /* dump current state regardless */ }
210
+ }
211
+ // Thread the host's current banner so a view can surface guidance without
212
+ // mirroring it into state (older views ignore the arg).
213
+ let text = view.dump(state, { banner: chrome.banner });
214
+ if (!text.endsWith('\n'))
215
+ text += '\n';
216
+ process.stdout.write(text);
217
+ return;
218
+ }
219
+ const caps = detectColorCaps();
220
+ const state = await view.init(host);
221
+ // Restore the terminal exactly once, however we leave.
222
+ let restored = false;
223
+ const cleanup = () => {
224
+ if (restored)
225
+ return;
226
+ restored = true;
227
+ try {
228
+ restoreTerminal();
229
+ }
230
+ catch { /* best-effort */ }
231
+ };
232
+ process.once('exit', cleanup);
233
+ const render = () => {
234
+ const size = getTerminalSize();
235
+ const { draw, frame } = createDraw(size, caps);
236
+ const content = drawChrome(draw, size, view.manifest, chrome);
237
+ try {
238
+ view.render(state, draw, content);
239
+ }
240
+ catch (e) {
241
+ chrome.banner = { msg: `render error: ${errText(e)}`, level: 'error' };
242
+ }
243
+ process.stdout.write(frame());
244
+ };
245
+ // Busy-tick repaint: while an async op is in flight, a ~120ms timer advances
246
+ // the spinner + re-renders so live setStatus narration shows. Render-only — it
247
+ // NEVER re-enters a hook, and Ctrl-C still escapes (handled before the drop).
248
+ let tickTimer;
249
+ const startBusyTick = () => {
250
+ if (tickTimer)
251
+ return;
252
+ tickTimer = setInterval(() => { chrome.tick++; render(); }, 120);
253
+ };
254
+ const stopBusyTick = () => {
255
+ if (tickTimer) {
256
+ clearInterval(tickTimer);
257
+ tickTimer = undefined;
258
+ }
259
+ };
260
+ // The single-flight lane. Returns true if the op ran (false if one was already
261
+ // in flight). Always repaints around the op so the busy indicator shows.
262
+ let busy = false;
263
+ const runRefresh = async () => {
264
+ if (!view.refresh || busy)
265
+ return;
266
+ busy = true;
267
+ chrome.busy = true;
268
+ render();
269
+ startBusyTick();
270
+ try {
271
+ await view.refresh(state, host);
272
+ }
273
+ catch (e) {
274
+ chrome.banner = { msg: errText(e), level: 'error' };
275
+ }
276
+ finally {
277
+ busy = false;
278
+ chrome.busy = false;
279
+ chrome.loaded = true;
280
+ chrome.lastRefresh = Date.now();
281
+ stopBusyTick();
282
+ render();
283
+ }
284
+ };
285
+ setupTerminal();
286
+ render(); // initial loading paint (before any fetch)
287
+ await runRefresh(); // first data load
288
+ await new Promise((resolveLoop) => {
289
+ let done = false;
290
+ let timer;
291
+ const finish = () => {
292
+ if (done)
293
+ return;
294
+ done = true;
295
+ if (timer)
296
+ clearInterval(timer);
297
+ stopBusyTick();
298
+ cleanup();
299
+ resolveLoop();
300
+ };
301
+ if (typeof view.manifest.refreshMs === 'number' && view.manifest.refreshMs > 0) {
302
+ timer = setInterval(() => { void runRefresh(); }, view.manifest.refreshMs);
303
+ }
304
+ const apply = async (action) => {
305
+ switch (action.type) {
306
+ case 'render':
307
+ render();
308
+ break;
309
+ case 'refresh':
310
+ await runRefresh();
311
+ break;
312
+ case 'quit':
313
+ finish();
314
+ break;
315
+ case 'none': break;
316
+ }
317
+ };
318
+ const onData = async (data) => {
319
+ if (done)
320
+ return;
321
+ let parsed;
322
+ try {
323
+ parsed = parseKeypress(data);
324
+ }
325
+ catch {
326
+ return;
327
+ }
328
+ const { input, key } = parsed;
329
+ // Ctrl-C is the universal escape hatch — works even mid-flight.
330
+ if (key.ctrl && input === 'c') {
331
+ finish();
332
+ return;
333
+ }
334
+ // Drop keystrokes while an async op is in flight (paces fetch/send).
335
+ if (busy)
336
+ return;
337
+ if (!view.onKey) {
338
+ if (input === 'q')
339
+ finish(); // minimal default so a no-onKey view escapes
340
+ return;
341
+ }
342
+ try {
343
+ const r = view.onKey({ input, key }, state, host);
344
+ let action;
345
+ if (r instanceof Promise) {
346
+ busy = true;
347
+ chrome.busy = true;
348
+ render();
349
+ startBusyTick();
350
+ try {
351
+ action = await r;
352
+ }
353
+ finally {
354
+ busy = false;
355
+ chrome.busy = false;
356
+ stopBusyTick();
357
+ }
358
+ }
359
+ else {
360
+ action = r;
361
+ }
362
+ await apply(action);
363
+ }
364
+ catch (e) {
365
+ chrome.banner = { msg: errText(e), level: 'error' };
366
+ render();
367
+ }
368
+ };
369
+ process.stdin.on('data', (d) => { void onData(d); });
370
+ // Resize → repaint from current state (never re-enter the in-flight hook).
371
+ process.stdout.on('resize', () => { if (!done)
372
+ render(); });
373
+ });
374
+ }
375
+ function errText(e) {
376
+ if (e instanceof Error)
377
+ return e.message;
378
+ return String(e);
379
+ }
@@ -0,0 +1,16 @@
1
+ import type { ViewModule } from './contract.js';
2
+ export interface ResolvedView {
3
+ id: string;
4
+ dir: string;
5
+ entry: string;
6
+ scope: 'project' | 'user' | 'builtin';
7
+ }
8
+ /** Scope search project→user→builtin; first hit wins. null if no view dir holds
9
+ * a `<name>/view.mjs`. */
10
+ export declare function resolveView(name: string): ResolvedView | null;
11
+ /** Enumerate every resolvable view across scopes (first scope wins per id) —
12
+ * backs `view list` / `view pick`. */
13
+ export declare function listViews(): ResolvedView[];
14
+ /** Dynamic-import a resolved view and validate it's a usable ViewModule. Throws
15
+ * a guided error (with the entry path) if the module is malformed. */
16
+ export declare function loadView(r: ResolvedView): Promise<ViewModule>;
@@ -0,0 +1,94 @@
1
+ // loader.ts — resolve + dynamically import view modules across scopes.
2
+ //
3
+ // A view = a directory containing `view.mjs` (the entry). Resolution order in
4
+ // resolveView, first hit wins: project → user → builtin (see scope.ts viewsDir).
5
+ // loadView does a plain `import(pathToFileURL(entry).href)` — no transpile, no
6
+ // bundle (the published binary is `node dist/cli.js` with no esbuild/tsx) — then
7
+ // validates the module is a usable ViewModule, throwing a guided error if not.
8
+ import { existsSync, readdirSync, statSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { pathToFileURL } from 'node:url';
11
+ import { viewsDir } from '../scope.js';
12
+ const SCOPE_ORDER = ['project', 'user', 'builtin'];
13
+ /** Scope search project→user→builtin; first hit wins. null if no view dir holds
14
+ * a `<name>/view.mjs`. */
15
+ export function resolveView(name) {
16
+ for (const scope of SCOPE_ORDER) {
17
+ const root = viewsDir(scope);
18
+ if (!root)
19
+ continue;
20
+ const dir = join(root, name);
21
+ const entry = join(dir, 'view.mjs');
22
+ if (existsSync(entry))
23
+ return { id: name, dir, entry, scope };
24
+ }
25
+ return null;
26
+ }
27
+ /** Enumerate every resolvable view across scopes (first scope wins per id) —
28
+ * backs `view list` / `view pick`. */
29
+ export function listViews() {
30
+ const seen = new Set();
31
+ const out = [];
32
+ for (const scope of SCOPE_ORDER) {
33
+ const root = viewsDir(scope);
34
+ if (!root || !existsSync(root))
35
+ continue;
36
+ let names;
37
+ try {
38
+ names = readdirSync(root);
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ for (const name of names.sort()) {
44
+ if (seen.has(name))
45
+ continue; // first scope wins
46
+ const dir = join(root, name);
47
+ let isDir = false;
48
+ try {
49
+ isDir = statSync(dir).isDirectory();
50
+ }
51
+ catch {
52
+ isDir = false;
53
+ }
54
+ if (!isDir)
55
+ continue;
56
+ const entry = join(dir, 'view.mjs');
57
+ if (!existsSync(entry))
58
+ continue;
59
+ seen.add(name);
60
+ out.push({ id: name, dir, entry, scope });
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ /** Dynamic-import a resolved view and validate it's a usable ViewModule. Throws
66
+ * a guided error (with the entry path) if the module is malformed. */
67
+ export async function loadView(r) {
68
+ let mod;
69
+ try {
70
+ mod = (await import(pathToFileURL(r.entry).href));
71
+ }
72
+ catch (e) {
73
+ throw guided(r, `failed to import the module: ${e instanceof Error ? e.message : String(e)}`);
74
+ }
75
+ const candidate = (mod['default'] ?? mod['view']);
76
+ if (!candidate || typeof candidate !== 'object') {
77
+ throw guided(r, 'no default export (or named `view`) that is a ViewModule object');
78
+ }
79
+ const m = candidate.manifest;
80
+ if (!m || typeof m.id !== 'string' || typeof m.title !== 'string') {
81
+ throw guided(r, 'manifest.id and manifest.title must be strings');
82
+ }
83
+ if (typeof candidate.init !== 'function')
84
+ throw guided(r, 'init() is required');
85
+ if (typeof candidate.render !== 'function')
86
+ throw guided(r, 'render() is required');
87
+ if (typeof candidate.dump !== 'function')
88
+ throw guided(r, 'dump() is required');
89
+ return candidate;
90
+ }
91
+ function guided(r, problem) {
92
+ return new Error(`invalid view "${r.id}" (${r.scope}) at ${r.entry}: ${problem}.\n` +
93
+ 'A view.mjs must `export default` a ViewModule with { manifest: { id, title }, init(), render(), dump() }.');
94
+ }
@@ -1,9 +1,10 @@
1
- // terminal.ts — raw-mode helpers for the `crtr canvas browse` TUI.
1
+ // terminal.ts — raw-mode helpers shared by every crtr raw-ANSI TUI.
2
2
  //
3
- // Hand-rolled (no deps), mirroring humanloop's src/tui/terminal.ts. Extends its
4
- // key parsing beyond up/down/return/escape/tab/backspace/ctrl + printable input:
5
- // adds leftArrow/rightArrow (cursor keys) and shiftTab (`\x1b[Z`) so the browser
6
- // can drive tree expand/collapse and tab cycling.
3
+ // Moved verbatim from src/core/canvas/browse/terminal.ts (it was already
4
+ // generic). Hand-rolled (no deps), mirroring humanloop's src/tui/terminal.ts.
5
+ // Extends its key parsing beyond up/down/return/escape/tab/backspace/ctrl +
6
+ // printable input: adds leftArrow/rightArrow (cursor keys) and shiftTab
7
+ // (`\x1b[Z`) so callers can drive tree expand/collapse and tab cycling.
7
8
  function emptyKey() {
8
9
  return {
9
10
  upArrow: false,
@@ -77,7 +78,7 @@ export function parseKeypress(data) {
77
78
  }
78
79
  export function setupTerminal() {
79
80
  if (!process.stdin.isTTY) {
80
- throw new Error('crtr canvas browse requires an interactive terminal (TTY)');
81
+ throw new Error('this TUI requires an interactive terminal (TTY)');
81
82
  }
82
83
  process.stdin.setRawMode(true);
83
84
  process.stdin.resume();
@@ -0,0 +1,86 @@
1
+ /** Error codes this module owns (a subset of surface-design §4.3). */
2
+ export type WakeErrorCode = 'wake_in_past' | 'bad_when' | 'bad_cadence' | 'unknown_zone' | 'cadence_too_fast';
3
+ export interface WakeError {
4
+ code: WakeErrorCode;
5
+ message: string;
6
+ /** The offending input, echoed back for the rendered error block. */
7
+ received: string;
8
+ }
9
+ export type WhenResult = {
10
+ fireAt: string;
11
+ } | {
12
+ error: WakeError;
13
+ };
14
+ export type CadenceResult = {
15
+ recur: string;
16
+ firstFireAt: string;
17
+ } | {
18
+ error: WakeError;
19
+ };
20
+ export interface ParseOpts {
21
+ /** IANA zone for bare wall-clock / calendar cadence; defaults to host-local. */
22
+ tz?: string;
23
+ /** Resolution anchor (frozen "now"). */
24
+ now: Date;
25
+ }
26
+ /**
27
+ * The two `recur` JSON shapes (pinned in the plan's Shared contracts). Stored
28
+ * as a JSON string in the `wakeups.recur` column; consumed by `nextSlotAfter`.
29
+ */
30
+ export type Recur = {
31
+ every: string;
32
+ } | {
33
+ cron: string;
34
+ tz: string;
35
+ };
36
+ /** Cadence floor: reject any interval / cron min-spacing below this (AC-N4). */
37
+ export declare const CADENCE_FLOOR_MS = 60000;
38
+ /**
39
+ * Resolve a `<when>` to a single UTC `fireAt`:
40
+ * - relative duration (`90s`, `1h30m`) -> now + Σ
41
+ * - absolute zoned ISO (`…Z` / `±HH:MM`) -> that exact instant
42
+ * - absolute bare ISO (`2026-06-07T09:00`) -> wall clock in `tz`
43
+ * (default host-local) -> UTC
44
+ *
45
+ * A bare wall-clock time is interpreted in `opts.tz` when given, else the host
46
+ * zone, and frozen to a UTC instant (DST never re-enters a one-shot). A
47
+ * resolved instant not strictly in the future is rejected (`wake_in_past`).
48
+ */
49
+ export declare function parseWhen(when: string, opts: ParseOpts): WhenResult;
50
+ /**
51
+ * Resolve a `<cadence>` (the `--every` value) to a stored `recur` JSON string
52
+ * plus the first `fireAt`:
53
+ * - fixed interval (a duration) -> {"every":"<dur>"}, first = now + interval
54
+ * - calendar cron / @alias -> {"cron":"<5-field>","tz":"<iana>"},
55
+ * first = next match strictly after now
56
+ *
57
+ * Aliases (`@daily`) are expanded to a 5-field cron before baking, and the
58
+ * IANA zone (from `opts.tz`, else host-local) is baked in so the engine stays a
59
+ * pure UTC evaluator (design D7). Rejects sub-floor spacing (`cadence_too_fast`,
60
+ * < 60s — for both intervals and seconds-granular crons), ungrammatical input
61
+ * (`bad_cadence`), and unknown zones (`unknown_zone`).
62
+ */
63
+ export declare function parseCadence(every: string, opts: ParseOpts): CadenceResult;
64
+ /**
65
+ * Render a stored `recur` JSON as a compact, human-readable cadence:
66
+ * - {"every":"6h"} -> `every 6h`
67
+ * - {"cron":"0 9 * * *","tz":"America/…"} -> ``cron `0 9 * * *` (America/…)``
68
+ * - null / undefined / unparseable -> `none`
69
+ *
70
+ * The ONE cadence-display helper, shared by the `node wake` CLI surface
71
+ * (list/guidance) and the <crtr-wake> wake-provenance block (bearings.ts), so
72
+ * the cadence an agent reads in its wake block matches `crtr node wake list`
73
+ * exactly. Cadence only — never an instance count.
74
+ */
75
+ export declare function cadenceDisplay(recur: string | null | undefined): string;
76
+ /**
77
+ * Earliest occurrence of `recur` strictly greater than `now`, as a UTC ISO
78
+ * string. Anchors on `now` (not the stored `fire_at`), so it structurally
79
+ * coalesces every slot missed while the daemon was down (design §5.3, AC-E2).
80
+ * DST-correct because the IANA zone is baked into a calendar `recur`.
81
+ *
82
+ * parseCadence validates the cadence at arm time, so a throw here is a rare
83
+ * backstop (a corrupted/foreign row) — T4 must quarantine such a row, never
84
+ * re-query it each tick.
85
+ */
86
+ export declare function nextSlotAfter(recur: string, now: Date): string;