@aragon-agent/cli 0.6.34 → 0.6.36

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 (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +173 -7
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +67 -3
  6. package/dist/agent/controller.js +106 -5
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/kit-apply.d.ts +56 -0
  9. package/dist/agent/kit-apply.js +209 -0
  10. package/dist/agent/model-profile-settings.d.ts +28 -0
  11. package/dist/agent/model-profile-settings.js +142 -29
  12. package/dist/agent/reducer.d.ts +13 -2
  13. package/dist/agent/reducer.js +1 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/agents-command.d.ts +15 -0
  16. package/dist/commands/agents-command.js +27 -0
  17. package/dist/commands/builtins.js +135 -4
  18. package/dist/commands/kit-command.d.ts +20 -0
  19. package/dist/commands/kit-command.js +131 -0
  20. package/dist/commands/perf.d.ts +11 -0
  21. package/dist/commands/perf.js +2 -1
  22. package/dist/commands/registry.d.ts +14 -0
  23. package/dist/config/app-paths.d.ts +15 -0
  24. package/dist/config/app-paths.js +33 -2
  25. package/dist/config/env.d.ts +4 -1
  26. package/dist/config/env.js +28 -2
  27. package/dist/config/kit-resolution.d.ts +93 -0
  28. package/dist/config/kit-resolution.js +226 -0
  29. package/dist/config/load.d.ts +10 -0
  30. package/dist/config/load.js +8 -1
  31. package/dist/config/model-kits.d.ts +224 -0
  32. package/dist/config/model-kits.js +302 -0
  33. package/dist/config/model-profile-store.d.ts +25 -0
  34. package/dist/config/model-profiles.d.ts +41 -3
  35. package/dist/config/model-profiles.js +46 -5
  36. package/dist/config/schema.d.ts +84 -0
  37. package/dist/config/schema.js +39 -0
  38. package/dist/config/store.js +12 -1
  39. package/dist/config/ui-state.d.ts +18 -0
  40. package/dist/config/ui-state.js +9 -0
  41. package/dist/exec/events.d.ts +25 -0
  42. package/dist/exec/events.js +4 -0
  43. package/dist/exec/index.js +16 -1
  44. package/dist/exec/runner.js +10 -0
  45. package/dist/input/keymap.js +18 -4
  46. package/dist/proc/limits.d.ts +16 -0
  47. package/dist/proc/limits.js +16 -0
  48. package/dist/proc/supervisor.d.ts +20 -1
  49. package/dist/proc/supervisor.js +42 -14
  50. package/dist/prompts/command.d.ts +34 -0
  51. package/dist/prompts/command.js +237 -0
  52. package/dist/prompts/copy.d.ts +89 -0
  53. package/dist/prompts/copy.js +131 -0
  54. package/dist/prompts/fill.d.ts +51 -0
  55. package/dist/prompts/fill.js +46 -0
  56. package/dist/prompts/limits.d.ts +53 -0
  57. package/dist/prompts/limits.js +53 -0
  58. package/dist/prompts/normalize.d.ts +53 -0
  59. package/dist/prompts/normalize.js +190 -0
  60. package/dist/prompts/resolve.d.ts +31 -0
  61. package/dist/prompts/resolve.js +60 -0
  62. package/dist/prompts/search.d.ts +58 -0
  63. package/dist/prompts/search.js +146 -0
  64. package/dist/prompts/store.d.ts +86 -0
  65. package/dist/prompts/store.js +453 -0
  66. package/dist/prompts/types.d.ts +204 -0
  67. package/dist/prompts/types.js +13 -0
  68. package/dist/session/checkpoint.d.ts +8 -1
  69. package/dist/session/checkpoint.js +6 -0
  70. package/dist/session/persist.d.ts +19 -1
  71. package/dist/session/persist.js +13 -1
  72. package/dist/session/startup-session.d.ts +7 -2
  73. package/dist/session/startup-session.js +5 -2
  74. package/dist/session/store.js +3 -0
  75. package/dist/session/validate-session.js +64 -1
  76. package/dist/team/limits.d.ts +25 -0
  77. package/dist/team/limits.js +26 -0
  78. package/dist/team/runtime.js +27 -2
  79. package/dist/team/settled.d.ts +57 -0
  80. package/dist/team/settled.js +87 -0
  81. package/dist/team/subagent.js +48 -2
  82. package/dist/team/timeline.d.ts +74 -0
  83. package/dist/team/timeline.js +101 -0
  84. package/dist/team/types.d.ts +24 -1
  85. package/dist/todo/acceptance.d.ts +120 -0
  86. package/dist/todo/acceptance.js +399 -0
  87. package/dist/todo/follow-through.d.ts +48 -23
  88. package/dist/todo/follow-through.js +110 -44
  89. package/dist/todo/limits.d.ts +62 -1
  90. package/dist/todo/limits.js +62 -1
  91. package/dist/todo/panel-rows.d.ts +29 -39
  92. package/dist/todo/panel-rows.js +37 -83
  93. package/dist/todo/plan-sections.d.ts +60 -0
  94. package/dist/todo/plan-sections.js +108 -0
  95. package/dist/todo/prompt.d.ts +17 -12
  96. package/dist/todo/prompt.js +59 -40
  97. package/dist/todo/row-window.d.ts +51 -0
  98. package/dist/todo/row-window.js +107 -0
  99. package/dist/todo/store.d.ts +139 -31
  100. package/dist/todo/store.js +271 -73
  101. package/dist/todo/todo-tool.d.ts +33 -8
  102. package/dist/todo/todo-tool.js +186 -30
  103. package/dist/todo/types.d.ts +58 -0
  104. package/dist/ui/App.js +414 -54
  105. package/dist/ui/Composer.d.ts +6 -2
  106. package/dist/ui/Composer.js +2 -2
  107. package/dist/ui/Header.d.ts +17 -1
  108. package/dist/ui/Header.js +8 -2
  109. package/dist/ui/PromptInput.d.ts +21 -1
  110. package/dist/ui/PromptInput.js +22 -2
  111. package/dist/ui/ScrollbackShell.d.ts +22 -7
  112. package/dist/ui/ScrollbackShell.js +8 -1
  113. package/dist/ui/StatusBar.d.ts +18 -0
  114. package/dist/ui/StatusBar.js +11 -3
  115. package/dist/ui/TeamPanel.d.ts +35 -41
  116. package/dist/ui/TeamPanel.js +53 -163
  117. package/dist/ui/TodoPanel.d.ts +17 -14
  118. package/dist/ui/TodoPanel.js +146 -38
  119. package/dist/ui/Transcript.js +23 -3
  120. package/dist/ui/composer-input.d.ts +10 -0
  121. package/dist/ui/composer-input.js +57 -24
  122. package/dist/ui/entries/TeamCard.d.ts +18 -2
  123. package/dist/ui/entries/TeamCard.js +47 -13
  124. package/dist/ui/entries/TodoCard.d.ts +26 -11
  125. package/dist/ui/entries/TodoCard.js +56 -15
  126. package/dist/ui/glyphs.d.ts +19 -0
  127. package/dist/ui/glyphs.js +4 -0
  128. package/dist/ui/layout/agents-board.d.ts +102 -0
  129. package/dist/ui/layout/agents-board.js +389 -0
  130. package/dist/ui/layout/cells.d.ts +61 -0
  131. package/dist/ui/layout/cells.js +156 -0
  132. package/dist/ui/layout/header-hints.d.ts +23 -3
  133. package/dist/ui/layout/header-hints.js +73 -7
  134. package/dist/ui/layout/kit-chip.d.ts +50 -0
  135. package/dist/ui/layout/kit-chip.js +81 -0
  136. package/dist/ui/layout/status-detail-layout.js +9 -0
  137. package/dist/ui/layout/status-layout.d.ts +13 -0
  138. package/dist/ui/layout/team-row.d.ts +139 -0
  139. package/dist/ui/layout/team-row.js +246 -0
  140. package/dist/ui/layout/todo-layout.d.ts +38 -0
  141. package/dist/ui/layout/todo-layout.js +75 -0
  142. package/dist/ui/layout/virtual-window.js +32 -3
  143. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  144. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  145. package/dist/ui/overlays/HelpOverlay.js +24 -2
  146. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  147. package/dist/ui/overlays/KitEditor.js +101 -0
  148. package/dist/ui/overlays/KitManager.d.ts +23 -0
  149. package/dist/ui/overlays/KitManager.js +104 -0
  150. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  151. package/dist/ui/overlays/KitPicker.js +58 -0
  152. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  153. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  154. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  155. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  156. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  157. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  158. package/dist/ui/overlays/PromptPicker.js +129 -0
  159. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  160. package/dist/ui/overlays/PromptTextArea.js +141 -0
  161. package/dist/ui/overlays/SettingsScreen.js +139 -16
  162. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  163. package/dist/ui/overlays/kit-picker-state.js +136 -0
  164. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  165. package/dist/ui/overlays/kit-settings-state.js +267 -0
  166. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  167. package/dist/ui/overlays/model-profile-state.js +59 -6
  168. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  169. package/dist/ui/overlays/prompt-library-state.js +335 -0
  170. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  171. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  172. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  173. package/dist/ui/scrollback/LiveTail.js +7 -2
  174. package/dist/ui/scrollback/band.js +14 -1
  175. package/dist/ui/scrollback/live-preview.js +10 -3
  176. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  177. package/dist/ui/scrollback/tail-window.js +17 -2
  178. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  179. package/dist/ui/team-status.d.ts +166 -0
  180. package/dist/ui/team-status.js +252 -0
  181. package/dist/ui/transcript-text.js +13 -4
  182. package/dist/ui/use-startup-notices.d.ts +35 -1
  183. package/dist/ui/use-startup-notices.js +29 -2
  184. package/dist/ui/use-wheel-routing.js +10 -2
  185. package/package.json +2 -2
@@ -0,0 +1,46 @@
1
+ /**
2
+ * How a prompt lands in the composer (quick-prompt-library D-6 / §4.6).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * PURE, AND IT KNOWS NOTHING ABOUT THE DRAFT'S REAL SHAPE. It is handed the current
7
+ * buffer and caret, and it answers "what should be inserted, or what should the new
8
+ * buffer be". It deliberately does NOT return the resulting buffer: a draft may
9
+ * hold paste-collapse TOKENS (`[Pasted text #1 +8 lines]` plus a payload map), so
10
+ * "the buffer after applying this plan" is a fact only `editorReducer` can compute.
11
+ * Recomputing it here would be a second authority, and the two would drift exactly
12
+ * on the drafts this feature exists to append to.
13
+ *
14
+ * WHY `insert` IS THE DEFAULT (and why the decision lives here rather than in
15
+ * `App`): the only place that knows what the draft currently holds is the editor
16
+ * (the host has no copy of the text), and silently discarding what someone has
17
+ * already typed is the one thing a "fill" gesture must never do.
18
+ */
19
+ /** Two newlines, so the typed text and the prompt read as two paragraphs. */
20
+ const SEPARATOR = '\n\n';
21
+ /**
22
+ * Decide between replacing and inserting, and build the fragment.
23
+ *
24
+ * A blank draft is replaced even in `insert` mode: "insert at the caret" into an
25
+ * empty buffer and "replace the empty buffer" are the same edit, and saying so
26
+ * keeps the caller from having to special-case it.
27
+ */
28
+ export function planPromptFill(input) {
29
+ const cursor = Math.max(0, Math.min(input.draft.length, Math.floor(input.cursor)));
30
+ const blank = input.draft.trim().length === 0;
31
+ if (input.mode === 'replace' || blank) {
32
+ return { mode: 'replace', text: input.content, cursor: input.content.length };
33
+ }
34
+ const before = input.draft.slice(0, cursor);
35
+ const after = input.draft.slice(cursor);
36
+ // A SEPARATOR IS ADDED ONLY WHERE ONE IS MISSING, which is what keeps the plan
37
+ // from producing three newlines when the caret already sits on a blank line.
38
+ const lead = before.length > 0 && !before.endsWith(SEPARATOR) ? SEPARATOR : '';
39
+ const trail = after.length > 0 && !after.startsWith(SEPARATOR) ? SEPARATOR : '';
40
+ const text = `${lead}${input.content}${trail}`;
41
+ return { mode: 'insert', text, cursor: cursor + lead.length };
42
+ }
43
+ /** Lines a body occupies, for the "8 lines, editable" acknowledgement. */
44
+ export function promptLineCount(content) {
45
+ return content.split('\n').length;
46
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Structural bounds for the quick-prompt library (quick-prompt-library §5.1).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * STRUCTURE, NOT POLICY (index.md:52-56). Nothing here is user-tunable: these are
7
+ * what an entry, the library and the picker's query can physically carry. The
8
+ * three keys a user may actually change (`prompts.enabled`, `insertMode`, `hint`)
9
+ * live in `config/schema.ts`, are clamped there, and are persisted there. Mixing
10
+ * the two is how a "temporary" ceiling becomes a setting nobody clamps.
11
+ *
12
+ * EVERY NUMBER IS A CODE-POINT COUNT, not a byte count and not a UTF-16 length.
13
+ * Titles and bodies are typed in Chinese by the users this feature exists for, so
14
+ * a byte-based ceiling would silently allow a third of the characters a
15
+ * Latin-alphabet user gets. `[...text].length` is the measurement everywhere in
16
+ * this tree.
17
+ */
18
+ export declare const PROMPT_LIMITS: {
19
+ /** Entries one library may hold. The 201st create is REFUSED, never truncated. */
20
+ readonly maxPrompts: 200;
21
+ /** Title, without surrounding whitespace. */
22
+ readonly titleMaxChars: 80;
23
+ /** Body. Large enough for a 40-line procedure, small enough to rewrite cheaply. */
24
+ readonly contentMaxChars: 8000;
25
+ /** Optional one-line note under the title. */
26
+ readonly descriptionMaxChars: 200;
27
+ /** Tags per entry. */
28
+ readonly maxTags: 6;
29
+ /** Characters per tag. */
30
+ readonly tagMaxChars: 24;
31
+ /** Body preview lines the list page's detail block may spend (read-only). */
32
+ readonly detailPreviewLines: 3;
33
+ /** Rows the list page's detail block may occupy, preview lines included. */
34
+ readonly detailMaxLines: 6;
35
+ /** Characters of a body shown on ONE preview line. */
36
+ readonly previewLineChars: 72;
37
+ };
38
+ /**
39
+ * The picker's own structural bounds.
40
+ *
41
+ * A SEPARATE OBJECT because the picker is a separate surface with a separate
42
+ * budget: its rows carry an index and a title and nothing else, and its query is
43
+ * a filter rather than content. Sharing one object would tie a future change to
44
+ * the library's row height.
45
+ */
46
+ export declare const PROMPT_PICKER_LIMITS: {
47
+ /** Longest query the filter accepts; further typing is ignored. */
48
+ readonly queryMaxChars: 64;
49
+ /** `1`-`9` pick directly, so nine. */
50
+ readonly quickDigits: 9;
51
+ /** Rows the picker's detail block may occupy. */
52
+ readonly detailMaxLines: 4;
53
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Structural bounds for the quick-prompt library (quick-prompt-library §5.1).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * STRUCTURE, NOT POLICY (index.md:52-56). Nothing here is user-tunable: these are
7
+ * what an entry, the library and the picker's query can physically carry. The
8
+ * three keys a user may actually change (`prompts.enabled`, `insertMode`, `hint`)
9
+ * live in `config/schema.ts`, are clamped there, and are persisted there. Mixing
10
+ * the two is how a "temporary" ceiling becomes a setting nobody clamps.
11
+ *
12
+ * EVERY NUMBER IS A CODE-POINT COUNT, not a byte count and not a UTF-16 length.
13
+ * Titles and bodies are typed in Chinese by the users this feature exists for, so
14
+ * a byte-based ceiling would silently allow a third of the characters a
15
+ * Latin-alphabet user gets. `[...text].length` is the measurement everywhere in
16
+ * this tree.
17
+ */
18
+ export const PROMPT_LIMITS = {
19
+ /** Entries one library may hold. The 201st create is REFUSED, never truncated. */
20
+ maxPrompts: 200,
21
+ /** Title, without surrounding whitespace. */
22
+ titleMaxChars: 80,
23
+ /** Body. Large enough for a 40-line procedure, small enough to rewrite cheaply. */
24
+ contentMaxChars: 8000,
25
+ /** Optional one-line note under the title. */
26
+ descriptionMaxChars: 200,
27
+ /** Tags per entry. */
28
+ maxTags: 6,
29
+ /** Characters per tag. */
30
+ tagMaxChars: 24,
31
+ /** Body preview lines the list page's detail block may spend (read-only). */
32
+ detailPreviewLines: 3,
33
+ /** Rows the list page's detail block may occupy, preview lines included. */
34
+ detailMaxLines: 6,
35
+ /** Characters of a body shown on ONE preview line. */
36
+ previewLineChars: 72,
37
+ };
38
+ /**
39
+ * The picker's own structural bounds.
40
+ *
41
+ * A SEPARATE OBJECT because the picker is a separate surface with a separate
42
+ * budget: its rows carry an index and a title and nothing else, and its query is
43
+ * a filter rather than content. Sharing one object would tie a future change to
44
+ * the library's row height.
45
+ */
46
+ export const PROMPT_PICKER_LIMITS = {
47
+ /** Longest query the filter accepts; further typing is ignored. */
48
+ queryMaxChars: 64,
49
+ /** `1`-`9` pick directly, so nine. */
50
+ quickDigits: 9,
51
+ /** Rows the picker's detail block may occupy. */
52
+ detailMaxLines: 4,
53
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Reading the library off disk without trusting it (quick-prompt-library D-4).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * ONE FIELD'S NONSENSE MUST NOT TAKE ANOTHER ENTRY WITH IT. `config/ui-state.ts`
7
+ * wrote that rule for a two-field file; this is the same rule one level up, for a
8
+ * file the user may hand-edit and one bad character in which would otherwise cost
9
+ * them two hundred prompts. So every entry is normalised on its own, an entry that
10
+ * is structurally unusable is DROPPED AND COUNTED, and the top-level file is only
11
+ * discarded when its shape is unknown (a parse failure or a `schema` that is not
12
+ * `1`).
13
+ *
14
+ * LENGTH IS CLAMPED HERE, NOT ENFORCED. The write path refuses an over-long title
15
+ * (`PromptWriteError`) because the user can still fix it; the read path clamps,
16
+ * because by then the text is the only copy and dropping the entry to protect a
17
+ * ceiling would delete the user's work to keep a number tidy.
18
+ */
19
+ import type { PromptLibrary, QuickPrompt } from './types.js';
20
+ /**
21
+ * One entry, or `null` when it cannot be salvaged.
22
+ *
23
+ * `id` IS REPAIRED, NOT TRUSTED. A duplicate id would make `{kind:'edit', id}`
24
+ * ambiguous and `deletePrompt(id)` delete two entries, so the caller passes the
25
+ * ids it has already claimed and an unrepairable one is replaced here.
26
+ */
27
+ export declare function normalizePrompt(raw: unknown, now?: number): QuickPrompt | null;
28
+ export interface NormalisedLibrary {
29
+ library: PromptLibrary;
30
+ /** Entries that could not be salvaged, for the one-shot notice (§8). */
31
+ dropped: number;
32
+ /** Why the whole file was discarded. `'none'` means it was usable. */
33
+ repaired: 'none' | 'schema' | 'parse';
34
+ }
35
+ /**
36
+ * The whole file, with per-entry tolerance and one repair verdict.
37
+ *
38
+ * `repaired` is what the store uses to decide whether to write the `.bak` copy;
39
+ * keeping the verdict in the return value rather than in the store means the pure
40
+ * function stays pure and the decision stays testable.
41
+ */
42
+ export declare function normalizePromptLibrary(raw: unknown, now?: number): NormalisedLibrary;
43
+ /**
44
+ * The canonical "most useful first" comparison (`/prompts sort favorite`).
45
+ *
46
+ * Favourite, then most used, then most recent, then title, then id. The last two
47
+ * are not decoration: without a total order two entries with identical counters
48
+ * would swap places between calls, and a "sorted" library that reorders itself on
49
+ * every read is worse than an unsorted one.
50
+ */
51
+ export declare function comparePrompts(a: QuickPrompt, b: QuickPrompt): number;
52
+ /** Test seam: `normalize.ts` owns no state, but the name keeps the call sites honest. */
53
+ export declare function emptyPromptLibrary(): PromptLibrary;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Reading the library off disk without trusting it (quick-prompt-library D-4).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * ONE FIELD'S NONSENSE MUST NOT TAKE ANOTHER ENTRY WITH IT. `config/ui-state.ts`
7
+ * wrote that rule for a two-field file; this is the same rule one level up, for a
8
+ * file the user may hand-edit and one bad character in which would otherwise cost
9
+ * them two hundred prompts. So every entry is normalised on its own, an entry that
10
+ * is structurally unusable is DROPPED AND COUNTED, and the top-level file is only
11
+ * discarded when its shape is unknown (a parse failure or a `schema` that is not
12
+ * `1`).
13
+ *
14
+ * LENGTH IS CLAMPED HERE, NOT ENFORCED. The write path refuses an over-long title
15
+ * (`PromptWriteError`) because the user can still fix it; the read path clamps,
16
+ * because by then the text is the only copy and dropping the entry to protect a
17
+ * ceiling would delete the user's work to keep a number tidy.
18
+ */
19
+ import { randomUUID } from 'node:crypto';
20
+ import { PROMPT_LIMITS } from './limits.js';
21
+ /** First `max` code points. */
22
+ function clampText(text, max) {
23
+ const points = [...text];
24
+ return points.length <= max ? text : points.slice(0, max).join('');
25
+ }
26
+ /**
27
+ * Control characters except `\n` and `\t`, and `\r` folded into `\n`.
28
+ *
29
+ * A `\r` that survives reaches the renderer as a carriage return, which moves the
30
+ * caret to column 0 and overwrites the row it is on; a stray `\u0000` is a frame
31
+ * marker in this codebase and must never appear inside stored content.
32
+ */
33
+ function sanitiseText(text) {
34
+ return text
35
+ .replace(/\r\n?/g, '\n')
36
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '');
37
+ }
38
+ function asString(value) {
39
+ return typeof value === 'string' ? value : null;
40
+ }
41
+ function asCount(value, fallback) {
42
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
43
+ return fallback;
44
+ return Math.floor(value);
45
+ }
46
+ function asTimestamp(value, fallback) {
47
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
48
+ return fallback;
49
+ return Math.floor(value);
50
+ }
51
+ /** Whitespace-folded, deduplicated, clamped, bounded. Order is preserved. */
52
+ function normaliseTags(value) {
53
+ if (!Array.isArray(value))
54
+ return [];
55
+ const seen = new Set();
56
+ const tags = [];
57
+ for (const raw of value) {
58
+ const text = asString(raw);
59
+ if (text === null)
60
+ continue;
61
+ const tag = clampText(text.trim().replace(/\s+/g, ' '), PROMPT_LIMITS.tagMaxChars);
62
+ if (tag.length === 0 || seen.has(tag))
63
+ continue;
64
+ seen.add(tag);
65
+ tags.push(tag);
66
+ if (tags.length >= PROMPT_LIMITS.maxTags)
67
+ break;
68
+ }
69
+ return tags;
70
+ }
71
+ /**
72
+ * One entry, or `null` when it cannot be salvaged.
73
+ *
74
+ * `id` IS REPAIRED, NOT TRUSTED. A duplicate id would make `{kind:'edit', id}`
75
+ * ambiguous and `deletePrompt(id)` delete two entries, so the caller passes the
76
+ * ids it has already claimed and an unrepairable one is replaced here.
77
+ */
78
+ export function normalizePrompt(raw, now = Date.now()) {
79
+ if (!raw || typeof raw !== 'object')
80
+ return null;
81
+ const src = raw;
82
+ const titleRaw = asString(src.title);
83
+ const contentRaw = asString(src.content);
84
+ if (titleRaw === null || contentRaw === null)
85
+ return null;
86
+ const title = clampText(sanitiseText(titleRaw).trim(), PROMPT_LIMITS.titleMaxChars);
87
+ const content = clampText(sanitiseText(contentRaw), PROMPT_LIMITS.contentMaxChars);
88
+ // I-5: neither field is ever the empty string, so an entry with no body left
89
+ // after sanitising is not an entry.
90
+ if (title.length === 0 || content.trim().length === 0)
91
+ return null;
92
+ const id = asString(src.id);
93
+ const description = clampText(sanitiseText(asString(src.description) ?? '').trim(), PROMPT_LIMITS.descriptionMaxChars);
94
+ const createdAt = asTimestamp(src.createdAt, now);
95
+ const updatedAt = asTimestamp(src.updatedAt, createdAt);
96
+ const entry = {
97
+ id: id !== null && id.trim().length > 0 ? id : randomUUID(),
98
+ title,
99
+ content,
100
+ useCount: asCount(src.useCount, 0),
101
+ createdAt,
102
+ updatedAt: Math.max(createdAt, updatedAt),
103
+ };
104
+ if (description.length > 0)
105
+ entry.description = description;
106
+ const tags = normaliseTags(src.tags);
107
+ if (tags.length > 0)
108
+ entry.tags = tags;
109
+ if (src.favorite === true)
110
+ entry.favorite = true;
111
+ const lastUsedAt = asTimestamp(src.lastUsedAt, 0);
112
+ if (lastUsedAt > 0)
113
+ entry.lastUsedAt = lastUsedAt;
114
+ return entry;
115
+ }
116
+ /**
117
+ * The whole file, with per-entry tolerance and one repair verdict.
118
+ *
119
+ * `repaired` is what the store uses to decide whether to write the `.bak` copy;
120
+ * keeping the verdict in the return value rather than in the store means the pure
121
+ * function stays pure and the decision stays testable.
122
+ */
123
+ export function normalizePromptLibrary(raw, now = Date.now()) {
124
+ const empty = (repaired) => ({
125
+ library: { schema: 1, prompts: [], updatedAt: 0 },
126
+ dropped: 0,
127
+ repaired,
128
+ });
129
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
130
+ return empty('parse');
131
+ const src = raw;
132
+ // A `schema` mismatch DISCARDS NOTHING on disk: the store copies the file to
133
+ // `<name>.bak` first (D-4). Reporting an empty library here is about the shape
134
+ // this process can read, not about what the user is allowed to get back.
135
+ if (src.schema !== 1)
136
+ return empty('schema');
137
+ if (!Array.isArray(src.prompts))
138
+ return empty('parse');
139
+ const entries = src.prompts;
140
+ const prompts = [];
141
+ const claimed = new Set();
142
+ let dropped = 0;
143
+ for (let at = 0; at < entries.length; at += 1) {
144
+ if (prompts.length >= PROMPT_LIMITS.maxPrompts) {
145
+ // THE CEILING APPLIES ON READ TOO, and the leftovers are COUNTED rather than
146
+ // silently forgotten: a hand-edited 300-entry file would lose 100 entries on
147
+ // the next write, so the user has to be told that this happened.
148
+ dropped += entries.length - at;
149
+ break;
150
+ }
151
+ const entry = normalizePrompt(entries[at], now);
152
+ if (!entry) {
153
+ dropped += 1;
154
+ continue;
155
+ }
156
+ if (claimed.has(entry.id))
157
+ entry.id = randomUUID();
158
+ claimed.add(entry.id);
159
+ prompts.push(entry);
160
+ }
161
+ return {
162
+ library: { schema: 1, prompts, updatedAt: asTimestamp(src.updatedAt, 0) },
163
+ dropped,
164
+ repaired: 'none',
165
+ };
166
+ }
167
+ /**
168
+ * The canonical "most useful first" comparison (`/prompts sort favorite`).
169
+ *
170
+ * Favourite, then most used, then most recent, then title, then id. The last two
171
+ * are not decoration: without a total order two entries with identical counters
172
+ * would swap places between calls, and a "sorted" library that reorders itself on
173
+ * every read is worse than an unsorted one.
174
+ */
175
+ export function comparePrompts(a, b) {
176
+ if ((a.favorite === true) !== (b.favorite === true))
177
+ return a.favorite === true ? -1 : 1;
178
+ if (a.useCount !== b.useCount)
179
+ return b.useCount - a.useCount;
180
+ const recent = (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
181
+ if (recent !== 0)
182
+ return recent;
183
+ if (a.title !== b.title)
184
+ return a.title < b.title ? -1 : 1;
185
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
186
+ }
187
+ /** Test seam: `normalize.ts` owns no state, but the name keeps the call sites honest. */
188
+ export function emptyPromptLibrary() {
189
+ return { schema: 1, prompts: [], updatedAt: 0 };
190
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `<n|title>` -> one entry (quick-prompt-library D-22).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * TITLES MAY REPEAT (I-2), so this function has exactly one job the command layer
7
+ * cannot do for itself: turn a possibly-ambiguous reference into either one entry
8
+ * or a NAMED failure with candidates. It never picks the first of several.
9
+ *
10
+ * WHY THAT IS NOT PEDANTRY: `/prompts rm <title>` has no recycle bin (D-13), so
11
+ * "the first of the two entries with that title" is a one-keystroke way to delete
12
+ * the wrong prompt - and the user would have no way to tell which one they had
13
+ * been talking to. A refusal that PRINTS the candidates costs one more command and
14
+ * keeps the data.
15
+ */
16
+ import type { PromptLibrary, PromptRefResolution } from './types.js';
17
+ /**
18
+ * Resolve a reference, in this fixed order:
19
+ *
20
+ * 1. a pure decimal number is a 1-based INDEX (never a title), because a script
21
+ * that says `2` must never depend on what the second prompt is called;
22
+ * 2. an exact title (case-sensitive, surrounding whitespace removed);
23
+ * 3. a unique title PREFIX, so `clean` finds `clean code review`.
24
+ *
25
+ * A number that is out of range is `not_found` rather than falling through to the
26
+ * title rules: `0` and `999` are index mistakes, and treating them as titles would
27
+ * silently match a prompt literally named "999".
28
+ */
29
+ export declare function resolvePromptRef(library: PromptLibrary, ref: string): PromptRefResolution;
30
+ /** The index a resolved entry sits at, for the "3 days ago" style detail lines. */
31
+ export declare function promptIndexOf(library: PromptLibrary, id: string): number;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `<n|title>` -> one entry (quick-prompt-library D-22).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * TITLES MAY REPEAT (I-2), so this function has exactly one job the command layer
7
+ * cannot do for itself: turn a possibly-ambiguous reference into either one entry
8
+ * or a NAMED failure with candidates. It never picks the first of several.
9
+ *
10
+ * WHY THAT IS NOT PEDANTRY: `/prompts rm <title>` has no recycle bin (D-13), so
11
+ * "the first of the two entries with that title" is a one-keystroke way to delete
12
+ * the wrong prompt - and the user would have no way to tell which one they had
13
+ * been talking to. A refusal that PRINTS the candidates costs one more command and
14
+ * keeps the data.
15
+ */
16
+ /** How many candidates a refusal names. Five is what a terminal row shows. */
17
+ const MAX_CANDIDATES = 5;
18
+ function exactTitle(prompts, ref) {
19
+ return prompts.filter((prompt) => prompt.title === ref);
20
+ }
21
+ function prefixTitle(prompts, ref) {
22
+ return prompts.filter((prompt) => prompt.title.startsWith(ref));
23
+ }
24
+ function decide(matches) {
25
+ if (matches.length === 1)
26
+ return { ok: true, prompt: matches[0] };
27
+ if (matches.length === 0)
28
+ return { ok: false, code: 'not_found' };
29
+ return { ok: false, code: 'ambiguous', candidates: matches.slice(0, MAX_CANDIDATES) };
30
+ }
31
+ /**
32
+ * Resolve a reference, in this fixed order:
33
+ *
34
+ * 1. a pure decimal number is a 1-based INDEX (never a title), because a script
35
+ * that says `2` must never depend on what the second prompt is called;
36
+ * 2. an exact title (case-sensitive, surrounding whitespace removed);
37
+ * 3. a unique title PREFIX, so `clean` finds `clean code review`.
38
+ *
39
+ * A number that is out of range is `not_found` rather than falling through to the
40
+ * title rules: `0` and `999` are index mistakes, and treating them as titles would
41
+ * silently match a prompt literally named "999".
42
+ */
43
+ export function resolvePromptRef(library, ref) {
44
+ const trimmed = ref.trim();
45
+ if (trimmed.length === 0)
46
+ return { ok: false, code: 'not_found' };
47
+ if (/^\d+$/.test(trimmed)) {
48
+ const at = Number.parseInt(trimmed, 10) - 1;
49
+ const prompt = library.prompts[at];
50
+ return prompt ? { ok: true, prompt } : { ok: false, code: 'not_found' };
51
+ }
52
+ const exact = exactTitle(library.prompts, trimmed);
53
+ if (exact.length > 0)
54
+ return decide(exact);
55
+ return decide(prefixTitle(library.prompts, trimmed));
56
+ }
57
+ /** The index a resolved entry sits at, for the "3 days ago" style detail lines. */
58
+ export function promptIndexOf(library, id) {
59
+ return library.prompts.findIndex((prompt) => prompt.id === id);
60
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Filtering and view ordering for the picker (quick-prompt-library D-9 / D-10).
3
+ *
4
+ * ASCII ONLY - `src/prompts/**` is inside `glyphs.test.ts::inScope`.
5
+ *
6
+ * SUBSEQUENCE MATCHING, NOT SUBSTRING MATCHING. `clr` should find `clean code
7
+ * review`, and Chinese titles make the same demand in a different shape: the
8
+ * characters a user remembers are rarely contiguous. The query is matched
9
+ * case-insensitively against the title, then the tags, then the description, and
10
+ * the FIELD a match landed in is a stronger signal than how good the match was.
11
+ *
12
+ * NO CHARACTER HIGHLIGHTING (D-10). Colouring the matched characters would mean
13
+ * splitting a row into styled runs, and every row in this package is one
14
+ * `wrap="truncate"` `<Text>` precisely so its height is exactly one row. The
15
+ * detail block answers "why did this match" instead.
16
+ */
17
+ import type { QuickPrompt, PromptSortView } from './types.js';
18
+ /**
19
+ * Greedy subsequence match; `null` when `query` is not a subsequence of `text`.
20
+ *
21
+ * The score is LOWER-is-better and is built from what a reader would actually
22
+ * call a good match: how early the first hit is, how tightly the hits cluster,
23
+ * and how many characters had to be skipped between them.
24
+ */
25
+ export declare function subsequenceScore(text: string, query: string): number | null;
26
+ /**
27
+ * Filter, but NEVER REORDER BEYOND WHAT THE QUERY EARNS.
28
+ *
29
+ * Ties keep the library's own order, because a filter that reshuffles entries with
30
+ * equal scores makes the list move under the user's arrow keys while they type.
31
+ * Favourites win ties, which is the only preference the picker states.
32
+ */
33
+ export declare function searchPrompts(prompts: readonly QuickPrompt[], query: string): {
34
+ prompt: QuickPrompt;
35
+ score: number;
36
+ }[];
37
+ /**
38
+ * The three presentations (D-9).
39
+ *
40
+ * `manual` is the only persisted order; the other two are VANISHING views. The one
41
+ * that rearranges is `favorite` - `recent` sorts by `lastUsedAt` only, so an entry
42
+ * that has never been used keeps its place at the tail instead of jumping around
43
+ * while the user watches.
44
+ */
45
+ export declare function viewOrder(prompts: readonly QuickPrompt[], view: PromptSortView): QuickPrompt[];
46
+ /** The next view in the `Ctrl+S` cycle (manual -> recent -> favorite -> manual). */
47
+ export declare function nextView(view: PromptSortView): PromptSortView;
48
+ /**
49
+ * Title order, for the `/prompts sort name` write.
50
+ *
51
+ * A TOTAL order (title, then id): a sort the user asked for has to be reproducible, and
52
+ * two entries sharing a title would otherwise swap places between runs - a "sorted"
53
+ * library that reorders itself on every read is worse than an unsorted one. Code-unit
54
+ * comparison rather than `localeCompare`, because the file is written on one machine and
55
+ * read on the next, and a locale-dependent order would silently rewrite the user's
56
+ * sequence on a different machine for no reason they could see.
57
+ */
58
+ export declare function sortedPromptTitleOrder(prompts: readonly QuickPrompt[]): QuickPrompt[];