@crouton-kit/crouter 0.3.17 → 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 (283) 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/{base.md → PERSONA.md} +4 -0
  12. package/dist/builtin-personas/design/orchestrator.md +1 -1
  13. package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  15. package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
  16. package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
  17. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  18. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  19. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  20. package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
  21. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  22. package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
  23. package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
  24. package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
  25. package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
  26. package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
  27. package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
  28. package/dist/builtin-personas/runtime-base.md +6 -3
  29. package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
  30. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  31. package/dist/builtin-personas/waiting.md +8 -0
  32. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
  33. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
  34. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  35. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  36. package/dist/builtin-views/_lib/states.mjs +161 -0
  37. package/dist/builtin-views/canvas/client.mjs +303 -0
  38. package/dist/builtin-views/canvas/view.mjs +576 -0
  39. package/dist/builtin-views/git-pr/client.mjs +440 -0
  40. package/dist/builtin-views/git-pr/view.mjs +675 -0
  41. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  42. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  43. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  44. package/dist/builtin-views/inbox/view.mjs +889 -0
  45. package/dist/builtin-views/linkedin/client.mjs +610 -0
  46. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  47. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  48. package/dist/clients/attach/attach-cmd.js +354 -0
  49. package/dist/clients/attach/chat-view.d.ts +77 -0
  50. package/dist/clients/attach/chat-view.js +450 -0
  51. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  52. package/dist/clients/attach/clipboard-image.js +113 -0
  53. package/dist/clients/attach/config-load.d.ts +31 -0
  54. package/dist/clients/attach/config-load.js +113 -0
  55. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  56. package/dist/clients/attach/extension-dialogs.js +101 -0
  57. package/dist/clients/attach/input-controller.d.ts +54 -0
  58. package/dist/clients/attach/input-controller.js +204 -0
  59. package/dist/clients/attach/slash-commands.d.ts +36 -0
  60. package/dist/clients/attach/slash-commands.js +200 -0
  61. package/dist/clients/attach/view-socket.d.ts +48 -0
  62. package/dist/clients/attach/view-socket.js +126 -0
  63. package/dist/commands/attention.js +3 -3
  64. package/dist/commands/canvas-prune.js +1 -1
  65. package/dist/commands/daemon.js +4 -3
  66. package/dist/commands/human/prompts.js +4 -10
  67. package/dist/commands/human/queue.js +43 -8
  68. package/dist/commands/human/shared.d.ts +28 -1
  69. package/dist/commands/human/shared.js +48 -10
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +660 -65
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  108. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  110. package/dist/core/__tests__/cascade-close.test.js +12 -2
  111. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  112. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  113. package/dist/core/__tests__/context-intro.test.js +76 -62
  114. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  115. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  116. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  117. package/dist/core/__tests__/detach-focus.test.js +206 -0
  118. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  119. package/dist/core/__tests__/draw-style.test.js +258 -0
  120. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  121. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  122. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  123. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  124. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  125. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  126. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  127. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  128. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  129. package/dist/core/__tests__/helpers/harness.js +111 -1
  130. package/dist/core/__tests__/home-session.test.js +41 -8
  131. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  132. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  133. package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
  134. package/dist/core/__tests__/human-surface-target.test.js +98 -0
  135. package/dist/core/__tests__/kickoff.test.js +37 -3
  136. package/dist/core/__tests__/live-mutation.test.js +50 -33
  137. package/dist/core/__tests__/memory.test.js +23 -115
  138. package/dist/core/__tests__/persona-subkind.test.js +18 -15
  139. package/dist/core/__tests__/placement-focus.test.js +5 -0
  140. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  141. package/dist/core/__tests__/relaunch.test.js +4 -3
  142. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  143. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  144. package/dist/core/__tests__/spawn-root.test.js +10 -0
  145. package/dist/core/__tests__/spike-harness.test.js +1 -0
  146. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  147. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  148. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  149. package/dist/core/__tests__/wake-origin.test.js +110 -0
  150. package/dist/core/bootstrap.js +1 -1
  151. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  152. package/dist/core/canvas/browse/app.js +24 -2
  153. package/dist/core/canvas/browse/model.d.ts +38 -2
  154. package/dist/core/canvas/browse/model.js +134 -10
  155. package/dist/core/canvas/browse/render.d.ts +6 -12
  156. package/dist/core/canvas/browse/render.js +72 -104
  157. package/dist/core/canvas/canvas.js +15 -22
  158. package/dist/core/canvas/db.js +46 -0
  159. package/dist/core/canvas/index.d.ts +1 -0
  160. package/dist/core/canvas/index.js +1 -0
  161. package/dist/core/canvas/paths.d.ts +4 -1
  162. package/dist/core/canvas/paths.js +10 -4
  163. package/dist/core/canvas/pid.d.ts +4 -0
  164. package/dist/core/canvas/pid.js +23 -0
  165. package/dist/core/canvas/render.d.ts +11 -2
  166. package/dist/core/canvas/render.js +69 -0
  167. package/dist/core/canvas/types.d.ts +85 -3
  168. package/dist/core/canvas/types.js +2 -2
  169. package/dist/core/canvas/wakeups.d.ts +76 -0
  170. package/dist/core/canvas/wakeups.js +185 -0
  171. package/dist/core/config.js +4 -1
  172. package/dist/core/frontmatter.js +37 -124
  173. package/dist/core/help.d.ts +6 -0
  174. package/dist/core/help.js +7 -0
  175. package/dist/core/memory-resolver.d.ts +49 -0
  176. package/dist/core/memory-resolver.js +141 -0
  177. package/dist/core/personas/index.d.ts +4 -3
  178. package/dist/core/personas/index.js +3 -2
  179. package/dist/core/personas/loader.d.ts +41 -16
  180. package/dist/core/personas/loader.js +133 -29
  181. package/dist/core/personas/resolve.d.ts +4 -4
  182. package/dist/core/personas/resolve.js +28 -16
  183. package/dist/core/predicate.d.ts +63 -0
  184. package/dist/core/predicate.js +189 -0
  185. package/dist/core/resolver.js +26 -5
  186. package/dist/core/runtime/bearings.d.ts +53 -12
  187. package/dist/core/runtime/bearings.js +132 -59
  188. package/dist/core/runtime/broker-cli.d.ts +1 -0
  189. package/dist/core/runtime/broker-cli.js +46 -0
  190. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  191. package/dist/core/runtime/broker-protocol.js +153 -0
  192. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  193. package/dist/core/runtime/broker-sdk.js +72 -0
  194. package/dist/core/runtime/broker.d.ts +55 -0
  195. package/dist/core/runtime/broker.js +1128 -0
  196. package/dist/core/runtime/close.js +35 -6
  197. package/dist/core/runtime/host.d.ts +53 -0
  198. package/dist/core/runtime/host.js +186 -0
  199. package/dist/core/runtime/kickoff.d.ts +2 -1
  200. package/dist/core/runtime/kickoff.js +91 -5
  201. package/dist/core/runtime/launch.d.ts +45 -2
  202. package/dist/core/runtime/launch.js +65 -2
  203. package/dist/core/runtime/lifecycle.js +23 -6
  204. package/dist/core/runtime/memory.d.ts +2 -42
  205. package/dist/core/runtime/memory.js +11 -162
  206. package/dist/core/runtime/nodes.d.ts +33 -0
  207. package/dist/core/runtime/nodes.js +59 -1
  208. package/dist/core/runtime/persona.js +21 -11
  209. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  210. package/dist/core/runtime/pi-vendored.js +49 -0
  211. package/dist/core/runtime/placement.d.ts +42 -14
  212. package/dist/core/runtime/placement.js +228 -38
  213. package/dist/core/runtime/promote.d.ts +0 -6
  214. package/dist/core/runtime/promote.js +1 -12
  215. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  216. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  217. package/dist/core/runtime/reset.js +8 -6
  218. package/dist/core/runtime/revive.d.ts +2 -0
  219. package/dist/core/runtime/revive.js +34 -34
  220. package/dist/core/runtime/spawn.d.ts +19 -0
  221. package/dist/core/runtime/spawn.js +75 -22
  222. package/dist/core/runtime/stop-guard.d.ts +1 -1
  223. package/dist/core/runtime/stop-guard.js +6 -1
  224. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  225. package/dist/core/runtime/tmux-chrome.js +1 -1
  226. package/dist/core/runtime/tmux.d.ts +28 -0
  227. package/dist/core/runtime/tmux.js +80 -6
  228. package/dist/core/scope.d.ts +11 -0
  229. package/dist/core/scope.js +39 -0
  230. package/dist/core/spawn.d.ts +25 -1
  231. package/dist/core/spawn.js +72 -7
  232. package/dist/core/substrate/gate.d.ts +13 -0
  233. package/dist/core/substrate/gate.js +21 -0
  234. package/dist/core/substrate/index.d.ts +7 -0
  235. package/dist/core/substrate/index.js +18 -0
  236. package/dist/core/substrate/on-read.d.ts +14 -0
  237. package/dist/core/substrate/on-read.js +292 -0
  238. package/dist/core/substrate/render.d.ts +25 -0
  239. package/dist/core/substrate/render.js +256 -0
  240. package/dist/core/substrate/schema.d.ts +76 -0
  241. package/dist/core/substrate/schema.js +124 -0
  242. package/dist/core/substrate/session-cache.d.ts +30 -0
  243. package/dist/core/substrate/session-cache.js +77 -0
  244. package/dist/core/substrate/subject.d.ts +41 -0
  245. package/dist/core/substrate/subject.js +54 -0
  246. package/dist/core/tui/contract.d.ts +83 -0
  247. package/dist/core/tui/contract.js +8 -0
  248. package/dist/core/tui/draw.d.ts +96 -0
  249. package/dist/core/tui/draw.js +339 -0
  250. package/dist/core/tui/host.d.ts +29 -0
  251. package/dist/core/tui/host.js +379 -0
  252. package/dist/core/tui/loader.d.ts +16 -0
  253. package/dist/core/tui/loader.js +94 -0
  254. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  255. package/dist/core/wake.d.ts +86 -0
  256. package/dist/core/wake.js +308 -0
  257. package/dist/daemon/crtrd.d.ts +29 -4
  258. package/dist/daemon/crtrd.js +662 -46
  259. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  260. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  261. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  262. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  263. package/dist/pi-extensions/canvas-commands.js +10 -0
  264. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  265. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  266. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  267. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  268. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-nav.js +145 -34
  270. package/dist/pi-extensions/canvas-stophook.js +17 -8
  271. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  272. package/dist/pi-extensions/canvas-view.js +75 -0
  273. package/dist/prompts/skill.js +19 -26
  274. package/dist/prompts/view.d.ts +7 -0
  275. package/dist/prompts/view.js +101 -0
  276. package/dist/types.d.ts +4 -0
  277. package/dist/types.js +1 -0
  278. package/package.json +9 -4
  279. package/dist/commands/skill/find.d.ts +0 -4
  280. package/dist/commands/skill/find.js +0 -257
  281. package/dist/commands/skill/read.js +0 -91
  282. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  283. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -241,7 +241,7 @@ const marketUpdate = defineLeaf({
241
241
  const marketInstall = defineLeaf({
242
242
  name: 'install',
243
243
  description: 'install a plugin from a marketplace',
244
- whenToUse: 'installing a plugin by name from an already-registered marketplace, e.g. `pkg market manage install --marketplace <m> --plugin <p>`',
244
+ whenToUse: 'installing a plugin by name from a marketplace you have already registered',
245
245
  help: {
246
246
  name: 'pkg market manage install',
247
247
  summary: 'install a plugin from an added marketplace by plugin name',
@@ -59,7 +59,7 @@ function makeTierLeaf(tier) {
59
59
  effects: [
60
60
  'Writes nodes/<nodeId>/reports/<ts>-<tier>.md.',
61
61
  'Appends one inbox pointer per subscriber.',
62
- ...(tier === 'final' ? ['Marks the node done (status + intent); its window closes on next stop.'] : []),
62
+ ...(tier === 'final' ? ['Marks the node done (status + intent); its engine shuts down on next stop.'] : []),
63
63
  ],
64
64
  },
65
65
  run: async (input) => {
@@ -86,8 +86,8 @@ function makeTierLeaf(tier) {
86
86
  }
87
87
  // A RESIDENT node with no subscribers is human-driven and has no one to
88
88
  // submit a canonical result to: `push final` fans to subscribers, and a
89
- // resident root conversation has none. Finishing it would close its window
90
- // mid-conversation. Block that unless the user confirms (--force). Keyed on
89
+ // resident root conversation has none. Finishing it would shut its
90
+ // engine down mid-conversation. Block that unless the user confirms (--force). Keyed on
91
91
  // lifecycle, NOT subscriber-count alone: a TERMINAL node with no subscribers
92
92
  // was deliberately terminalized to owe a final — it self-completes here
93
93
  // (records the result, reaps) rather than being blocked for lack of a recipient.
@@ -96,7 +96,7 @@ function makeTierLeaf(tier) {
96
96
  if (noRecipient) {
97
97
  throw new InputError({
98
98
  error: 'no_final_recipient',
99
- message: 'This node is working directly with the user — it has no manager to submit a final result to. `push final` would close its window mid-conversation.',
99
+ message: 'This node is working directly with the user — it has no manager to submit a final result to. `push final` would end the conversation mid-stream.',
100
100
  next: 'You almost certainly do NOT need to finish here — just keep working with the user. If the user has explicitly asked you to finish and close this node, confirm with them first, then rerun with `crtr push final --force "<result>"`.',
101
101
  });
102
102
  }
@@ -108,7 +108,7 @@ function makeTierLeaf(tier) {
108
108
  render: (r) => {
109
109
  const n = Array.isArray(r['delivered_to']) ? r['delivered_to'].length : 0;
110
110
  const line = tier === 'final'
111
- ? 'Result recorded — node finished; its window closes on next stop. Nothing more to do here.'
111
+ ? 'Result recorded — node finished; its engine shuts down on next stop. Nothing more to do here: STOP your turn immediately. Reply with just "Done." and nothing else.'
112
112
  : tier === 'urgent'
113
113
  ? `Urgent report fanned to ${n} subscriber(s) — they are force-woken.`
114
114
  : `Progress report fanned to ${n} subscriber(s).`;
@@ -122,7 +122,7 @@ function makeTierLeaf(tier) {
122
122
  const feedReadLeaf = defineLeaf({
123
123
  name: 'read',
124
124
  description: 'drain unread pointers into a digest',
125
- whenToUse: 'you want to PROACTIVELY poll what the nodes you subscribe to — your children and anyone you follow — have reported before the watcher wakes you, draining the unread pointers in your inbox into one coalesced digest. NOTE: when a subscriber push wakes you, that wake message already IS this digest (the watcher drains your inbox to wake you), so don\'t re-run feed read to "open" it — dereference the refs in the digest you already have. Reach for read to poll before the next wake, to inspect a worker inbox via --node, or with --all to re-read the whole history (full message bodies included) after the cursor has advanced.',
125
+ whenToUse: 'you want to PROACTIVELY poll what the nodes you subscribe to — your children and anyone you follow — have reported before the watcher wakes you, draining the unread pointers in your inbox into one coalesced digest. NOTE: when a subscriber push wakes you, that wake message already IS this digest (the watcher drains your inbox to wake you), so don\'t re-run feed read to "open" it — dereference the refs in the digest you already have. Reach for it to poll before the next wake, to inspect another node\'s inbox, or to re-read the whole history after the cursor has advanced.',
126
126
  help: {
127
127
  name: 'feed read',
128
128
  summary: 'drain unread inbox pointers for the caller (or a named node) into a compact digest',
@@ -13,7 +13,7 @@ import { getNode } from '../core/canvas/index.js';
13
13
  export const reviveLeaf = defineLeaf({
14
14
  name: 'revive',
15
15
  description: 'reopen a window for a done/idle/dead/canceled node',
16
- whenToUse: 'you want to bring a dormant node back yourself — reopen a window for one that is done, idle, dead, or canceled: resume a node you closed with `node close`, reopen a finished worker for a follow-up, or restart a crashed one now instead of waiting. Resumes the saved conversation by default; pass `--fresh` to restart clean against its context dir. You rarely need this for crashes — the daemon auto-revives those; reach for it to bring a node back on demand, or to revive a canceled node the daemon will never touch on its own',
16
+ whenToUse: 'you want to bring a dormant node back yourself — reopen a window for one that is done, idle, dead, or canceled: resume a node you closed with `node close`, reopen a finished worker for a follow-up, or restart a crashed one now instead of waiting. It resumes the saved conversation by default, or can restart the node clean. You rarely need this for crashes — the daemon auto-revives those; reach for it to bring a node back on demand, or to revive a canceled node the daemon will never touch on its own',
17
17
  help: {
18
18
  name: 'canvas revive',
19
19
  summary: 'open a fresh tmux window for a node, optionally resuming its saved pi conversation',
@@ -13,7 +13,7 @@ import { VALID_TYPES, resolveWriteScope } from './shared.js';
13
13
  export const authorGuide = defineLeaf({
14
14
  name: 'guide',
15
15
  description: 'load authoring workflow + skeleton for a type',
16
- whenToUse: 'REQUIRED reading before you author a new skill OR edit an existing one — it carries the SKILL.md format, the description-drives-discovery rule (when-to-use lives in the frontmatter description, never the body), the voice constraints, and the per-type workflow. Call it once with no --type for the template picker, then again with --type for that type\'s full workflow and skeleton. Editing an existing skill counts: read this first, because the format and voice rules govern every change, not just new files.',
16
+ whenToUse: 'REQUIRED reading before you author a new skill OR edit an existing one — it carries the SKILL.md format, the description-drives-discovery rule (when-to-use lives in the frontmatter description, never the body), the voice constraints, and the per-type workflow. Editing an existing skill counts: read this first, because the format and voice rules govern every change, not just new files.',
17
17
  help: {
18
18
  name: 'skill author guide',
19
19
  summary: 'load the skill authoring workflow — two stages: omit type to pick one, pass type for its full skeleton',
@@ -5,15 +5,8 @@ export declare function buildNeighborsSection(skill: Skill): string | null;
5
5
  export declare function appendNeighbors(skill: Skill, body: string): string;
6
6
  export declare function resolveWriteScope(scopeStr: string | undefined): Scope;
7
7
  export declare const VALID_TYPES: readonly ["playbook", "primer", "reference", "runbook", "freeform"];
8
- type CatalogSource = {
8
+ export type CatalogSource = {
9
9
  plugin: string;
10
10
  roots: string[];
11
11
  };
12
- /** The skill subtree's live state: the loaded-skills catalog as a self-named
13
- * `<skills count="N">` element. The tag carries the label and the count is an
14
- * attribute, so the body is the grouped tree alone — no "Loaded skills (N)"
15
- * header to duplicate. Returns null (block omitted) when discovery fails or
16
- * nothing is loaded. */
17
- export declare function buildSkillCatalog(): string | null;
18
12
  export declare function renderCatalogSection(label: string, sources: CatalogSource[], descriptions: Map<string, string>, out: string[]): void;
19
- export {};
@@ -1,7 +1,6 @@
1
- import { stateBlock } from '../../core/help.js';
2
1
  import { usage } from '../../core/errors.js';
3
2
  import { SCOPE_SKILL_PLUGIN } from '../../types.js';
4
- import { listSkillSiblings, listSkillChildren, listAllSkills, listAllPlugins, } from '../../core/resolver.js';
3
+ import { listSkillSiblings, listSkillChildren, } from '../../core/resolver.js';
5
4
  import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
6
5
  // ---------------------------------------------------------------------------
7
6
  // Neighbors section (ported from old impl)
@@ -24,7 +23,7 @@ export function buildNeighborsSection(skill) {
24
23
  return null;
25
24
  const lines = [
26
25
  '## Neighbors',
27
- '*Auto-discovered from filesystem. Run `crtr skill read <name>` for full description + body.*',
26
+ '*Auto-discovered from filesystem. Run `crtr memory read <name>` for full description + body.*',
28
27
  '',
29
28
  ];
30
29
  if (siblings.length > 0) {
@@ -68,58 +67,6 @@ export function resolveWriteScope(scopeStr) {
68
67
  // ---------------------------------------------------------------------------
69
68
  export const VALID_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
70
69
  const CATALOG_T = 5;
71
- /** The skill subtree's live state: the loaded-skills catalog as a self-named
72
- * `<skills count="N">` element. The tag carries the label and the count is an
73
- * attribute, so the body is the grouped tree alone — no "Loaded skills (N)"
74
- * header to duplicate. Returns null (block omitted) when discovery fails or
75
- * nothing is loaded. */
76
- export function buildSkillCatalog() {
77
- let skills;
78
- try {
79
- skills = listAllSkills().filter((s) => s.enabled);
80
- }
81
- catch {
82
- return null;
83
- }
84
- if (skills.length === 0)
85
- return null;
86
- const pluginDescriptions = new Map();
87
- for (const p of listAllPlugins()) {
88
- if (p.manifest.description)
89
- pluginDescriptions.set(p.name, p.manifest.description);
90
- }
91
- const bySource = new Map();
92
- for (const s of skills) {
93
- const key = `${s.scope}\t${s.plugin}`;
94
- const arr = bySource.get(key);
95
- if (arr)
96
- arr.push(s);
97
- else
98
- bySource.set(key, [s]);
99
- }
100
- const projectSources = [];
101
- const userSources = [];
102
- for (const [key, group] of bySource) {
103
- const [scope, plugin] = key.split('\t');
104
- const names = group.map((g) => g.name);
105
- const roots = names
106
- .filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
107
- .sort();
108
- if (roots.length === 0)
109
- continue;
110
- (scope === 'project' ? projectSources : userSources).push({ plugin, roots });
111
- }
112
- const body = [];
113
- renderCatalogSection('Project', projectSources, pluginDescriptions, body);
114
- renderCatalogSection('User', userSources, pluginDescriptions, body);
115
- // renderCatalogSection leads each section with a blank separator; drop the
116
- // leading one so the element body starts on its first real line.
117
- while (body.length > 0 && body[0] === '')
118
- body.shift();
119
- body.push('');
120
- body.push("Groups shown as `name/ N skills` are collapsed. Read the group to get its menu before assuming a skill is or isn't there: `crtr skill read <group>` (or `crtr skill find list --plugin <group>`). Search across everything with `crtr skill find search <topic>`.");
121
- return stateBlock('skills', { count: skills.length }, body.join('\n'));
122
- }
123
70
  export function renderCatalogSection(label, sources, descriptions, out) {
124
71
  if (sources.length === 0)
125
72
  return;
@@ -1,27 +1,22 @@
1
- // `crtr skill` subtree handlers — P3 implementation.
2
- // Sub-branches: find {list, search, grep}, author {guide, scaffold}, state {enable, disable}.
3
- // Leaf children of skill: read.
1
+ // `crtr skill` subtree handlers.
2
+ // Sub-branches: author {guide, scaffold}, state {enable, disable}.
3
+ // Discovery and reading are now `crtr memory` verbs.
4
4
  import { defineBranch } from '../core/command.js';
5
- import { buildSkillCatalog } from './skill/shared.js';
6
- import { findBranch } from './skill/find.js';
7
- import { readLeaf } from './skill/read.js';
8
5
  import { authorBranch } from './skill/author.js';
9
6
  import { stateBranch } from './skill/state.js';
10
7
  export function registerSkill() {
11
8
  return defineBranch({
12
9
  name: 'skill',
13
10
  rootEntry: {
14
- concept: 'a SKILL.md you read to adopt its workflow',
15
- desc: 'find, read, author, and manage skills',
16
- useWhen: 'a task matches a loaded skill — read it before improvising. `crtr skill read <name>` loads one by name from the catalog below; `crtr skill find` only when the name is not already listed. Names are crtr identifiers, not file paths never cat or find SKILL.md off disk.',
17
- dynamicState: buildSkillCatalog,
11
+ concept: 'a SKILL.md you author and manage',
12
+ desc: 'author and manage skills',
13
+ useWhen: 'authoring a new skill (`crtr skill author`) or toggling a skill on/off (`crtr skill state`). To discover and read skills use `crtr memory` `crtr memory read <name>`, `crtr memory find`, `crtr memory list`.',
18
14
  },
19
15
  help: {
20
16
  name: 'skill',
21
- summary: 'discover, read, author, and manage skill state',
22
- model: '`find` when you do not yet know which skill applies — it locates candidates by topic, keyword, or body text. `read` (leaf) loads SKILL.md by name; takes the name as a positional, returns body + metadata, accepts --no-body to skip the body. `author` when you are writing a new skill — it carries the template workflow and the scaffolder. `state` when a skill should be hidden from discovery without being removed. Append `-h` at any branch or leaf for its full schema.',
23
- dynamicState: buildSkillCatalog,
17
+ summary: 'author and manage skill state — use `crtr memory` to discover and read skills',
18
+ model: '`author` when you are writing a new skill — it carries the template workflow and the scaffolder. `state` when a skill should be hidden from discovery without being removed. Discovery and reading are `crtr memory` verbs: `crtr memory list`, `crtr memory find`, `crtr memory read`. Append `-h` at any branch or leaf for its full schema.',
24
19
  },
25
- children: [findBranch, readLeaf, authorBranch, stateBranch],
20
+ children: [authorBranch, stateBranch],
26
21
  });
27
22
  }
@@ -326,7 +326,7 @@ function runChecksForScope(scope, opts) {
326
326
  export const sysDoctorLeaf = defineLeaf({
327
327
  name: 'doctor',
328
328
  description: 'diagnose installation health',
329
- whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or skill frontmatter has drifted from its filename. Reports each problem with a structured remediation; pass --fix to apply the repairs.',
329
+ whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or skill frontmatter has drifted from its filename. Reports each problem with a structured remediation, and can apply the repairs for you.',
330
330
  help: {
331
331
  name: 'sys doctor',
332
332
  summary: 'diagnose missing manifests, broken config entries, and skill frontmatter drift',
@@ -7,7 +7,7 @@ import { readPackageVersion } from './shared.js';
7
7
  export const sysUpdateLeaf = defineLeaf({
8
8
  name: 'update',
9
9
  description: 'update binary and content',
10
- whenToUse: 'upgrading crtr or its installed plugins and marketplaces — pull the latest binary, refresh installed content, or both; pass --check first to see what is out of date without applying anything.',
10
+ whenToUse: 'upgrading crtr or its installed plugins and marketplaces — pull the latest binary, refresh installed content, or both, or just check what is out of date without applying anything.',
11
11
  help: {
12
12
  name: 'sys update',
13
13
  summary: 'update the crtr binary and/or installed plugins and marketplaces',
@@ -0,0 +1,2 @@
1
+ import type { LeafDef } from '../core/command.js';
2
+ export declare const viewCycleLeaf: LeafDef;
@@ -0,0 +1,125 @@
1
+ // `crtr view cycle <dir>` — switch the view hosted in a monitor pane to the
2
+ // next/prev available view, in place. Backs the Alt+V then ] (next) / Alt+V
3
+ // then [ (prev) chord installed by installViewNavBindings, mirroring how
4
+ // `node cycle` backs Alt+] / Alt+[ for the node graph (view = the bracket
5
+ // grammar namespaced under the Alt+V view prefix).
6
+ //
7
+ // A view monitor pane is self-identifying: `view run` tags it with the tmux
8
+ // pane option @crtr_view=<id>. Cycle reads that tag, computes the next/prev id
9
+ // in `listViews()` order (the SAME set `view list` enumerates), and respawns
10
+ // the pane on `crtr view run <next-id>` — `respawn-pane -k` kills the current
11
+ // view process and re-execs in the SAME pane (cycle itself runs in tmux's
12
+ // run-shell context, NOT inside the pane, so the kill never targets the caller).
13
+ // The respawned run re-tags the pane, so a rapid re-cycle reads the right id.
14
+ //
15
+ // tmux-only; a pane with no @crtr_view tag (not a view monitor) is a no-op.
16
+ // Output is discarded by the keybinding, so this just acts.
17
+ import { defineLeaf } from '../core/command.js';
18
+ import { listViews, resolveView, loadView } from '../core/tui/loader.js';
19
+ // Commands reach the tmux driver through placement.ts (the sanctioned
20
+ // model-over-driver seam, §5.1) — never `./tmux.js` directly.
21
+ import { inTmux, currentTmux, getPaneOption, setPaneOption, paneCurrentPath, respawnPaneSync, piCommand, } from '../core/runtime/placement.js';
22
+ export const viewCycleLeaf = defineLeaf({
23
+ name: 'cycle',
24
+ description: 'switch the monitor pane to the next/prev view in place',
25
+ whenToUse: 'flipping a view monitor to the next/prev available view without re-running a command (bound to Alt+V then ] forward / Alt+V then [ back). The pane must already be hosting a view — opened with `crtr view run <name>` (or --window/--split). Use `crtr view run <name>` to open a specific view, or `crtr view list` to see the cycle order',
26
+ help: {
27
+ name: 'view cycle',
28
+ summary: 'switch the view hosted in a monitor pane to the next/previous available view, in place — the views walked one monitor at a time, in `view list` order (bound to Alt+V then ] forward / Alt+V then [ back)',
29
+ params: [
30
+ { kind: 'flag', name: 'dir', type: 'enum', choices: ['next', 'prev'], required: false, default: 'next', constraint: 'Direction along the view list: next (Alt+V then ]) or prev (Alt+V then [). Wraps at the ends.' },
31
+ { kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane to cycle. Defaults to $TMUX_PANE / your current pane. The Alt+V then ]/[ bindings pass this for you.' },
32
+ ],
33
+ output: [
34
+ { name: 'cycled', type: 'boolean', required: true, constraint: 'True when the pane was switched to another view.' },
35
+ { name: 'view', type: 'string', required: false, constraint: 'The view now hosted in the pane.' },
36
+ { name: 'from', type: 'string', required: false, constraint: 'The view it switched away from (absent when the pane was not a view monitor).' },
37
+ ],
38
+ outputKind: 'object',
39
+ effects: [
40
+ 'Respawns the target pane (respawn-pane -k) running `crtr view run <next-id>`; the prior view process is replaced in place.',
41
+ 'Re-tags the pane @crtr_view=<next-id>. No-op outside tmux or when the pane is not a view monitor.',
42
+ ],
43
+ },
44
+ run: async (input) => {
45
+ if (!inTmux())
46
+ return { cycled: false };
47
+ const dir = (input['dir'] ?? 'next');
48
+ const pane = input['pane'] ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
49
+ if (pane === undefined || pane === '')
50
+ return { cycled: false };
51
+ // Self-identification: only a pane `view run` tagged is a view monitor.
52
+ const current = getPaneOption(pane, '@crtr_view');
53
+ if (current === undefined || current === '')
54
+ return { cycled: false };
55
+ const ids = listViews().map((v) => v.id); // SAME set + order as `view list`
56
+ if (ids.length < 2)
57
+ return { cycled: false, from: current };
58
+ // Build the candidate order: views after `current` in `dir`, wrapping. If
59
+ // `current` was renamed/removed (i === -1) start at the head (next) / tail
60
+ // (prev) so the cycle lands on the FIRST available view (m3). Then advance
61
+ // past any view that fails to load, so a malformed view never closes the
62
+ // monitor on respawn (m4) — bounded to one full lap.
63
+ const n = ids.length;
64
+ const mod = (x) => ((x % n) + n) % n;
65
+ const step = dir === 'next' ? 1 : -1;
66
+ const i = ids.indexOf(current);
67
+ const candidates = [];
68
+ if (i === -1) {
69
+ const startIdx = dir === 'next' ? 0 : n - 1;
70
+ for (let k = 0; k < n; k++)
71
+ candidates.push(ids[mod(startIdx + step * k)]);
72
+ }
73
+ else {
74
+ for (let k = 1; k < n; k++)
75
+ candidates.push(ids[mod(i + step * k)]);
76
+ }
77
+ let targetId;
78
+ for (const cand of candidates) {
79
+ if (cand === current)
80
+ continue;
81
+ const rv = resolveView(cand);
82
+ if (rv === null)
83
+ continue;
84
+ try {
85
+ await loadView(rv);
86
+ targetId = cand;
87
+ break;
88
+ }
89
+ catch { /* skip malformed */ }
90
+ }
91
+ if (targetId === undefined)
92
+ return { cycled: false, from: current };
93
+ // Replay the connection params the monitor was opened with (M2): a view
94
+ // configured with --port/--target keeps them across a cycle, so a round-trip
95
+ // doesn't silently drop e.g. the LinkedIn view's CDP port. Params stick to
96
+ // the MONITOR (pane), not the view, and are harmless to views that ignore them.
97
+ const keepPort = getPaneOption(pane, '@crtr_view_port');
98
+ const keepTarget = getPaneOption(pane, '@crtr_view_target');
99
+ const argv = ['view', 'run', targetId];
100
+ if (keepPort !== undefined && keepPort !== '')
101
+ argv.push('--port', keepPort);
102
+ if (keepTarget !== undefined && keepTarget !== '')
103
+ argv.push('--target', keepTarget);
104
+ // Preserve the monitor's cwd so project-scoped views still resolve.
105
+ const cwd = paneCurrentPath(pane) ?? process.cwd();
106
+ const command = piCommand(argv, 'crtr');
107
+ const ok = respawnPaneSync({ pane, cwd, env: {}, command });
108
+ if (!ok)
109
+ return { cycled: false, from: current };
110
+ // The respawned `view run` re-tags @crtr_view (+ params), but set them now
111
+ // too so a rapid re-cycle reads the right state before the new run boots.
112
+ try {
113
+ setPaneOption(pane, '@crtr_view', targetId);
114
+ setPaneOption(pane, '@crtr_view_port', keepPort ?? '');
115
+ setPaneOption(pane, '@crtr_view_target', keepTarget ?? '');
116
+ }
117
+ catch { /* best-effort */ }
118
+ return { cycled: true, view: targetId, from: current };
119
+ },
120
+ render: (r) => r['cycled'] === true
121
+ ? `<view-cycled to="${r['view']}" from="${r['from'] ?? ''}"/>`
122
+ : r['from'] !== undefined
123
+ ? `<view-cycle-noop>no other view to switch to</view-cycle-noop>`
124
+ : `<view-cycle-noop>this pane is not hosting a view</view-cycle-noop>`,
125
+ });
@@ -0,0 +1,2 @@
1
+ import type { LeafDef } from '../core/command.js';
2
+ export declare const viewListLeaf: LeafDef;
@@ -0,0 +1,66 @@
1
+ // `crtr view list` — enumerate every resolvable view across scopes.
2
+ //
3
+ // Lists id · title · description · scope. Loads each view to read its manifest
4
+ // title/description; a malformed view is shown with an error marker (it never
5
+ // crashes the listing). Plain text — pipeable as-is. The raw array is available
6
+ // behind --json for tooling.
7
+ import { defineLeaf } from '../core/command.js';
8
+ import { listViews, loadView } from '../core/tui/loader.js';
9
+ export const viewListLeaf = defineLeaf({
10
+ name: 'list',
11
+ description: 'enumerate available views (id · title · description · scope)',
12
+ whenToUse: 'you want to see which views exist before running one — a flat roster across project, user, and builtin scopes. Pipeable plain text; pass --json for the raw array. Use `crtr view run <id>` to open one, `crtr view new <name>` to scaffold one',
13
+ help: {
14
+ name: 'view list',
15
+ summary: 'list every resolvable view (id, title, description, scope); pipeable plain text',
16
+ inputNote: 'No input parameters.',
17
+ output: [
18
+ { name: 'views', type: 'array', required: true, constraint: 'One entry per view: { id, title, description, scope, error? }.' },
19
+ ],
20
+ outputKind: 'object',
21
+ effects: ['None. Read-only.'],
22
+ },
23
+ run: async (input) => {
24
+ const rows = [];
25
+ for (const r of listViews()) {
26
+ try {
27
+ const v = await loadView(r);
28
+ rows.push({
29
+ id: r.id,
30
+ title: v.manifest.title,
31
+ description: v.manifest.description ?? '',
32
+ scope: r.scope,
33
+ });
34
+ }
35
+ catch (e) {
36
+ // Tolerate a malformed view — show it with an error marker, don't crash.
37
+ rows.push({
38
+ id: r.id,
39
+ title: '(failed to load)',
40
+ description: '',
41
+ scope: r.scope,
42
+ error: e instanceof Error ? e.message : String(e),
43
+ });
44
+ }
45
+ }
46
+ return { views: rows };
47
+ },
48
+ render: (result) => {
49
+ const rows = result['views'] ?? [];
50
+ if (rows.length === 0) {
51
+ return 'No views found. Scaffold one with `crtr view new <name>`.';
52
+ }
53
+ const idW = Math.max(...rows.map((r) => r.id.length), 2);
54
+ const titleW = Math.max(...rows.map((r) => r.title.length), 5);
55
+ const lines = rows.map((r) => {
56
+ const id = r.id.padEnd(idW);
57
+ const title = r.title.padEnd(titleW);
58
+ if (r.error !== undefined) {
59
+ return `${id} ${title} [${r.scope}] ⚠ ${r.error}`;
60
+ }
61
+ const desc = r.description !== '' ? ` ${r.description}` : '';
62
+ return `${id} ${title} [${r.scope}]${desc}`;
63
+ });
64
+ return lines.join('\n');
65
+ },
66
+ });
@@ -0,0 +1,2 @@
1
+ import type { LeafDef } from '../core/command.js';
2
+ export declare const viewNewLeaf: LeafDef;
@@ -0,0 +1,70 @@
1
+ // `crtr view new <name>` — scaffold a runnable view.mjs into a scope dir.
2
+ //
3
+ // Writes <viewsDir(scope)>/<name>/view.mjs from the stub in prompts/view.ts
4
+ // (mkdir -p first). Refuses if a view of that name already resolves. The
5
+ // scaffolded view runs as-is via `crtr view run <name>`. Mirrors how skill
6
+ // authoring scaffolds (resolve scope → ensure dir → write → point at how to run).
7
+ import { mkdirSync, existsSync } from 'node:fs';
8
+ import { writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { defineLeaf } from '../core/command.js';
11
+ import { usage } from '../core/errors.js';
12
+ import { resolveView } from '../core/tui/loader.js';
13
+ import { viewsDir, projectScopeRoot, ensureProjectScopeRoot } from '../core/scope.js';
14
+ import { viewScaffold } from '../prompts/view.js';
15
+ export const viewNewLeaf = defineLeaf({
16
+ name: 'new',
17
+ description: 'scaffold a runnable view.mjs stub into a scope dir',
18
+ whenToUse: 'you want to author a new view — this writes a minimal, runnable view.mjs (with the contract + draw/host API documented inline) into the user or project scope, ready to open immediately with `crtr view run <name>` and to edit from there. Reach for it to start a new TUI surface rather than hand-writing the module',
19
+ help: {
20
+ name: 'view new',
21
+ summary: 'scaffold <viewsDir(scope)>/<name>/view.mjs from the stub; refuses if the name already resolves',
22
+ params: [
23
+ { kind: 'positional', name: 'name', required: true, constraint: 'View id / directory name. Must not already resolve as a view.' },
24
+ { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Where to scaffold. Default: project if available, else user.' },
25
+ { kind: 'flag', name: 'title', type: 'string', required: false, constraint: 'Manifest title (header + picker). Default: the name.' },
26
+ { kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Manifest description (picker + `view list`). Default: empty.' },
27
+ ],
28
+ output: [
29
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded view.mjs.' },
30
+ { name: 'scope', type: 'string', required: true, constraint: 'The scope it was written to (user|project).' },
31
+ { name: 'run', type: 'string', required: true, constraint: 'The command to run the new view.' },
32
+ ],
33
+ outputKind: 'object',
34
+ effects: [
35
+ 'Creates the view directory and view.mjs stub at the resolved location (mkdir -p).',
36
+ 'Initializes the project scope dir if --scope project is chosen and none exists yet.',
37
+ ],
38
+ },
39
+ run: async (input) => {
40
+ const name = input['name'];
41
+ const scopeArg = input['scope'];
42
+ const title = input['title'] ?? name;
43
+ const description = input['description'] ?? '';
44
+ // Default scope: project if a project scope exists, else user.
45
+ const scope = scopeArg === 'project' || scopeArg === 'user'
46
+ ? scopeArg
47
+ : (projectScopeRoot() !== null ? 'project' : 'user');
48
+ // Refuse if a view of this name already resolves (any scope wins).
49
+ const existing = resolveView(name);
50
+ if (existing !== null) {
51
+ throw usage(`view already exists: ${name} (${existing.scope}) at ${existing.entry}. ` +
52
+ `Pick another name, or edit it directly.`);
53
+ }
54
+ // Ensure the project scope root exists before computing its views dir.
55
+ if (scope === 'project')
56
+ ensureProjectScopeRoot();
57
+ const baseDir = viewsDir(scope);
58
+ if (baseDir === null) {
59
+ throw usage(`no ${scope} scope available to scaffold into — try --scope user.`);
60
+ }
61
+ const dir = join(baseDir, name);
62
+ const entry = join(dir, 'view.mjs');
63
+ if (existsSync(entry)) {
64
+ throw usage(`view file already exists: ${entry}`);
65
+ }
66
+ mkdirSync(dir, { recursive: true });
67
+ writeFileSync(entry, viewScaffold({ id: name, title, description }), 'utf8');
68
+ return { path: entry, scope, run: `crtr view run ${name}` };
69
+ },
70
+ });
@@ -0,0 +1,2 @@
1
+ import type { LeafDef } from '../core/command.js';
2
+ export declare const viewPickLeaf: LeafDef;
@@ -0,0 +1,119 @@
1
+ // `crtr view pick` — hidden raw-ANSI picker over the available views.
2
+ //
3
+ // Backs the /view popup: a minimal full-screen list of listViews(); j/k move,
4
+ // Enter hands off to `crtr view run <id>` (execFileSync, stdio inherit — a
5
+ // one-way handoff, the picker exits as the view takes over, same pattern as
6
+ // browse's selectAndFocus), q/Esc/Ctrl-C quit. Outside a TTY it prints the
7
+ // plain list and exits 0. Reuses the core/tui terminal + draw primitives.
8
+ import { execFileSync } from 'node:child_process';
9
+ import { defineLeaf } from '../core/command.js';
10
+ import { listViews } from '../core/tui/loader.js';
11
+ import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../core/tui/terminal.js';
12
+ import { createDraw, detectColorCaps } from '../core/tui/draw.js';
13
+ export const viewPickLeaf = defineLeaf({
14
+ name: 'pick',
15
+ tier: 'hidden',
16
+ description: 'raw-ANSI view picker (used by the /view popup)',
17
+ whenToUse: 'internal — the /view popup shells this to choose a view, then it execs `crtr view run <id>`. You normally run `crtr view run <name>` or `crtr view list` directly',
18
+ help: {
19
+ name: 'view pick',
20
+ summary: 'interactive picker over available views; Enter runs the chosen view. Outside a TTY it prints the list and exits',
21
+ inputNote: 'No input parameters.',
22
+ output: [],
23
+ outputKind: 'object',
24
+ effects: [
25
+ 'Inside a TTY: takes over the terminal in raw mode until you pick (Enter) or quit (q/Esc/Ctrl-C).',
26
+ 'On Enter: execs `crtr view run <id>` (stdio inherited) — a one-way handoff; the picker exits.',
27
+ 'Outside a TTY: prints the plain view list and exits 0.',
28
+ ],
29
+ },
30
+ run: async () => {
31
+ const views = listViews();
32
+ // Non-TTY: print the plain list and exit 0.
33
+ if (!process.stdin.isTTY) {
34
+ const text = views.length > 0
35
+ ? views.map((v) => `${v.id}\t${v.scope}`).join('\n')
36
+ : '(no views)';
37
+ process.stdout.write(text + '\n');
38
+ return;
39
+ }
40
+ if (views.length === 0) {
41
+ process.stdout.write('No views found. Scaffold one with `crtr view new <name>`.\n');
42
+ return;
43
+ }
44
+ const caps = detectColorCaps();
45
+ let cursor = 0;
46
+ let scroll = 0;
47
+ let restored = false;
48
+ const cleanup = () => {
49
+ if (restored)
50
+ return;
51
+ restored = true;
52
+ try {
53
+ restoreTerminal();
54
+ }
55
+ catch { /* best-effort */ }
56
+ };
57
+ process.once('exit', cleanup);
58
+ const render = () => {
59
+ const size = getTerminalSize();
60
+ const { draw, frame } = createDraw(size, caps);
61
+ draw.text(0, 0, 'crtr views — j/k move · Enter run · q quit', { bold: true });
62
+ draw.hline(1, 0, size.cols);
63
+ const rect = { row: 2, col: 0, width: size.cols, height: Math.max(1, size.rows - 3) };
64
+ const items = views.map((v) => ({
65
+ spans: [
66
+ { text: v.id, style: { bold: true } },
67
+ { text: ` [${v.scope}]`, style: { dim: true } },
68
+ ],
69
+ }));
70
+ const res = draw.list(rect, items, cursor, scroll);
71
+ scroll = res.scroll;
72
+ process.stdout.write(frame());
73
+ };
74
+ setupTerminal();
75
+ render();
76
+ await new Promise((resolve) => {
77
+ const finish = () => {
78
+ cleanup();
79
+ process.exit(0);
80
+ };
81
+ const launch = (id) => {
82
+ cleanup();
83
+ try {
84
+ execFileSync('crtr', ['view', 'run', id], { stdio: 'inherit' });
85
+ }
86
+ catch {
87
+ // The view takes the terminal over; a sync handoff may be interrupted.
88
+ }
89
+ process.exit(0);
90
+ };
91
+ process.stdin.on('data', (d) => {
92
+ let parsed;
93
+ try {
94
+ parsed = parseKeypress(d);
95
+ }
96
+ catch {
97
+ return;
98
+ }
99
+ const { input, key } = parsed;
100
+ if (key.ctrl && input === 'c')
101
+ finish();
102
+ if (input === 'q' || key.escape)
103
+ finish();
104
+ if (input === 'j' || key.downArrow) {
105
+ cursor = Math.min(views.length - 1, cursor + 1);
106
+ render();
107
+ return;
108
+ }
109
+ if (input === 'k' || key.upArrow) {
110
+ cursor = Math.max(0, cursor - 1);
111
+ render();
112
+ return;
113
+ }
114
+ if (key.return)
115
+ launch(views[cursor].id);
116
+ });
117
+ });
118
+ },
119
+ });
@@ -0,0 +1,2 @@
1
+ import type { LeafDef } from '../core/command.js';
2
+ export declare const viewRunLeaf: LeafDef;