@phren/cli 0.0.1

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/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,119 @@
1
+ import { execFileSync } from "child_process";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { runLink } from "./link.js";
5
+ import { runPhrenUpdate } from "./update.js";
6
+ import { EXEC_TIMEOUT_MS, } from "./shared.js";
7
+ export function resultMsg(r) {
8
+ if (!r.ok)
9
+ return r.error;
10
+ return typeof r.data === "string" ? r.data : JSON.stringify(r.data);
11
+ }
12
+ export function editDistance(a, b) {
13
+ const m = a.length;
14
+ const n = b.length;
15
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
16
+ for (let i = 0; i <= m; i++)
17
+ dp[i][0] = i;
18
+ for (let j = 0; j <= n; j++)
19
+ dp[0][j] = j;
20
+ for (let i = 1; i <= m; i++) {
21
+ for (let j = 1; j <= n; j++) {
22
+ dp[i][j] = a[i - 1] === b[j - 1]
23
+ ? dp[i - 1][j - 1]
24
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
25
+ }
26
+ }
27
+ return dp[m][n];
28
+ }
29
+ export function tokenize(input) {
30
+ const out = [];
31
+ let current = "";
32
+ let quote = null;
33
+ for (let i = 0; i < input.length; i++) {
34
+ const ch = input[i];
35
+ if ((ch === '"' || ch === "'") && (!quote || quote === ch)) {
36
+ quote = quote ? null : ch;
37
+ continue;
38
+ }
39
+ if (!quote && /\s/.test(ch)) {
40
+ if (current) {
41
+ out.push(current);
42
+ current = "";
43
+ }
44
+ continue;
45
+ }
46
+ current += ch;
47
+ }
48
+ if (current)
49
+ out.push(current);
50
+ return out;
51
+ }
52
+ export function tasksByFilter(items, filter) {
53
+ const needle = filter.toLowerCase().trim();
54
+ if (!needle)
55
+ return items;
56
+ return items.filter((item) => `${item.id} ${item.line} ${item.context || ""} ${item.githubIssue ? `#${item.githubIssue}` : ""} ${item.githubUrl || ""}`.toLowerCase().includes(needle));
57
+ }
58
+ export function queueByFilter(items, filter) {
59
+ const needle = filter.toLowerCase().trim();
60
+ if (!needle)
61
+ return items;
62
+ return items.filter((item) => `${item.id} ${item.section} ${item.text}`.toLowerCase().includes(needle));
63
+ }
64
+ export function expandIds(input) {
65
+ const parts = input.split(",").map((s) => s.trim()).filter(Boolean);
66
+ const result = [];
67
+ for (const part of parts) {
68
+ const rangeMatch = part.match(/^([AQD])(\d+)-\1?(\d+)$/i);
69
+ if (rangeMatch) {
70
+ const prefix = rangeMatch[1].toUpperCase();
71
+ const start = Number.parseInt(rangeMatch[2], 10);
72
+ const end = Number.parseInt(rangeMatch[3], 10);
73
+ for (let i = Math.min(start, end); i <= Math.max(start, end); i++) {
74
+ result.push(`${prefix}${i}`);
75
+ }
76
+ }
77
+ else {
78
+ result.push(part);
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+ export function normalizeSection(sectionRaw) {
84
+ const normalized = sectionRaw.toLowerCase();
85
+ if (["active", "a"].includes(normalized))
86
+ return "Active";
87
+ if (["queue", "queued", "q"].includes(normalized))
88
+ return "Queue";
89
+ if (["done", "d"].includes(normalized))
90
+ return "Done";
91
+ return null;
92
+ }
93
+ // ── Infrastructure ───────────────────────────────────────────────────────────
94
+ export function resolveEntryScript() {
95
+ const current = fileURLToPath(import.meta.url);
96
+ return path.resolve(path.dirname(current), "index.js");
97
+ }
98
+ export async function defaultRunHooks(phrenPath) {
99
+ const entry = resolveEntryScript();
100
+ execFileSync(process.execPath, [entry, "hook-session-start"], {
101
+ cwd: phrenPath,
102
+ stdio: "ignore",
103
+ timeout: EXEC_TIMEOUT_MS,
104
+ });
105
+ execFileSync(process.execPath, [entry, "hook-stop"], {
106
+ cwd: phrenPath,
107
+ stdio: "ignore",
108
+ timeout: EXEC_TIMEOUT_MS,
109
+ });
110
+ return "Lifecycle hooks rerun (session-start + stop).";
111
+ }
112
+ export async function defaultRunUpdate() {
113
+ const result = await runPhrenUpdate();
114
+ return result.message;
115
+ }
116
+ export async function defaultRunRelink(phrenPath) {
117
+ await runLink(phrenPath, { register: false, allTools: true });
118
+ return "Relink completed for detected tools.";
119
+ }
@@ -0,0 +1,252 @@
1
+ // ── ANSI utilities ──────────────────────────────────────────────────────────
2
+ const ESC = "\x1b[";
3
+ export const RESET = `${ESC}0m`;
4
+ export const style = {
5
+ bold: (s) => `${ESC}1m${s}${RESET}`,
6
+ dim: (s) => `${ESC}2m${s}${RESET}`,
7
+ italic: (s) => `${ESC}3m${s}${RESET}`,
8
+ cyan: (s) => `${ESC}36m${s}${RESET}`,
9
+ green: (s) => `${ESC}32m${s}${RESET}`,
10
+ yellow: (s) => `${ESC}33m${s}${RESET}`,
11
+ red: (s) => `${ESC}31m${s}${RESET}`,
12
+ magenta: (s) => `${ESC}35m${s}${RESET}`,
13
+ blue: (s) => `${ESC}34m${s}${RESET}`,
14
+ white: (s) => `${ESC}37m${s}${RESET}`,
15
+ gray: (s) => `${ESC}90m${s}${RESET}`,
16
+ boldCyan: (s) => `${ESC}1;36m${s}${RESET}`,
17
+ boldGreen: (s) => `${ESC}1;32m${s}${RESET}`,
18
+ boldYellow: (s) => `${ESC}1;33m${s}${RESET}`,
19
+ boldRed: (s) => `${ESC}1;31m${s}${RESET}`,
20
+ boldMagenta: (s) => `${ESC}1;35m${s}${RESET}`,
21
+ boldBlue: (s) => `${ESC}1;34m${s}${RESET}`,
22
+ dimItalic: (s) => `${ESC}2;3m${s}${RESET}`,
23
+ invert: (s) => `${ESC}7m${s}${RESET}`,
24
+ };
25
+ export function badge(label, colorFn) {
26
+ return colorFn(`[${label}]`);
27
+ }
28
+ export function separator(width = 50) {
29
+ return style.dim("━".repeat(Math.max(1, width)));
30
+ }
31
+ export function stripAnsi(s) {
32
+ return s.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
33
+ }
34
+ export function visibleWidth(s) {
35
+ return stripAnsi(s).length;
36
+ }
37
+ export function padToWidth(s, width) {
38
+ if (width <= 0)
39
+ return "";
40
+ if (width === 1)
41
+ return truncateLine(s, width);
42
+ const visible = stripAnsi(s);
43
+ if (visible.length > width)
44
+ return visible.slice(0, width - 1) + "…";
45
+ return s + " ".repeat(width - visible.length);
46
+ }
47
+ // ANSI handling: `s` may contain ANSI escape codes (styled text from the style.*
48
+ // helpers). We measure visible width via stripAnsi, then if truncation is needed we
49
+ // slice the *plain* text (discarding ANSI codes) to avoid cutting mid-escape. A
50
+ // trailing reset is appended to guard against any residual SGR state from earlier
51
+ // output on the same terminal line.
52
+ export function truncateLine(s, cols) {
53
+ if (cols <= 0)
54
+ return "";
55
+ if (cols === 1)
56
+ return "…" + "\x1b[0m";
57
+ const visible = stripAnsi(s);
58
+ if (visible.length <= cols)
59
+ return s;
60
+ return visible.slice(0, cols - 1) + "…" + "\x1b[0m";
61
+ }
62
+ // Reserve one column to avoid terminal autowrap when a line exactly fills the width.
63
+ // Many terminals wrap on the last visible column, which corrupts full-screen redraws.
64
+ export function renderWidth(columns = process.stdout.columns || 80) {
65
+ return Math.max(1, columns - 1);
66
+ }
67
+ export function wrapSegments(segments, cols, opts = {}) {
68
+ const indent = opts.indent ?? " ";
69
+ const maxLines = Math.max(1, opts.maxLines ?? Number.POSITIVE_INFINITY);
70
+ const separator = opts.separator ?? " ";
71
+ const indentWidth = visibleWidth(indent);
72
+ const available = Math.max(1, cols - indentWidth);
73
+ const lines = [];
74
+ let current = indent;
75
+ let currentWidth = indentWidth;
76
+ const pushEllipsis = () => {
77
+ const extraSep = currentWidth > indentWidth ? separator : "";
78
+ lines.push(truncateLine(current + extraSep + "…", cols));
79
+ };
80
+ for (const raw of segments) {
81
+ if (!raw)
82
+ continue;
83
+ const segment = truncateLine(raw, available);
84
+ const segmentWidth = visibleWidth(segment);
85
+ const separatorWidth = currentWidth > indentWidth ? visibleWidth(separator) : 0;
86
+ if (currentWidth > indentWidth && currentWidth + separatorWidth + segmentWidth > cols) {
87
+ if (lines.length + 1 >= maxLines) {
88
+ pushEllipsis();
89
+ return lines.join("\n");
90
+ }
91
+ lines.push(current);
92
+ current = indent + segment;
93
+ currentWidth = indentWidth + segmentWidth;
94
+ continue;
95
+ }
96
+ if (currentWidth > indentWidth) {
97
+ current += separator;
98
+ currentWidth += separatorWidth;
99
+ }
100
+ current += segment;
101
+ currentWidth += segmentWidth;
102
+ }
103
+ lines.push(current);
104
+ return lines.slice(0, maxLines).join("\n");
105
+ }
106
+ // ── Phren theme ────────────────────────────────────────────────────────────
107
+ // Neural gradient palette: purple → blue → cyan (256-color ANSI)
108
+ const PHREN_GRADIENT = [
109
+ "\x1b[38;5;93m", // vivid purple
110
+ "\x1b[38;5;99m", // purple-blue
111
+ "\x1b[38;5;105m", // blue-purple
112
+ "\x1b[38;5;111m", // sky blue
113
+ "\x1b[38;5;75m", // dodger blue
114
+ "\x1b[38;5;81m", // cyan-blue
115
+ "\x1b[38;5;87m", // bright cyan
116
+ ];
117
+ // Apply gradient coloring across non-whitespace characters
118
+ export function gradient(text, colors = PHREN_GRADIENT) {
119
+ const plain = stripAnsi(text);
120
+ const chars = [...plain];
121
+ const nonSpaceCount = chars.filter(ch => !/\s/.test(ch)).length;
122
+ if (!nonSpaceCount || !colors.length)
123
+ return text;
124
+ let result = "";
125
+ let vi = 0;
126
+ for (const ch of chars) {
127
+ if (/\s/.test(ch)) {
128
+ result += ch;
129
+ }
130
+ else {
131
+ const ci = Math.min(Math.floor(vi * colors.length / nonSpaceCount), colors.length - 1);
132
+ result += colors[ci] + ch;
133
+ vi++;
134
+ }
135
+ }
136
+ return result + RESET;
137
+ }
138
+ // Block-letter logo for startup animation
139
+ const PHREN_LOGO = [
140
+ " ██████╗ ██████╗ ██████╗ ████████╗███████╗██╗ ██╗",
141
+ "██╔════╝██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝",
142
+ "██║ ██║ ██║██████╔╝ ██║ █████╗ ╚███╔╝ ",
143
+ "██║ ██║ ██║██╔══██╗ ██║ ██╔══╝ ██╔██╗ ",
144
+ "╚██████╗╚██████╔╝██║ ██║ ██║ ███████╗██╔╝ ╚██╗",
145
+ " ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝",
146
+ ];
147
+ // Compact phren character for startup (imported inline to avoid circular deps)
148
+ const PHREN_STARTUP = [
149
+ "\x1b[96m ✦\x1b[0m",
150
+ "\x1b[38;5;57m ▄\x1b[35m██████\x1b[38;5;57m▄\x1b[0m",
151
+ "\x1b[35m ██\x1b[95m▓▓\x1b[35m██\x1b[95m▓▓\x1b[35m██\x1b[0m",
152
+ "\x1b[35m █\x1b[38;5;57m◆\x1b[35m██\x1b[38;5;57m◆\x1b[35m███\x1b[0m",
153
+ "\x1b[35m ██\x1b[2m\x1b[35m▽\x1b[0m\x1b[35m████\x1b[95m█\x1b[0m",
154
+ "\x1b[38;5;57m ▀\x1b[35m██████\x1b[38;5;57m▀\x1b[0m",
155
+ "\x1b[38;5;57m ██ ██\x1b[0m",
156
+ ];
157
+ // ── Line-based viewport: edge-triggered scroll (stable, no jumpiness) ─────────
158
+ export function lineViewport(allLines, cursorFirstLine, cursorLastLine, height, prevStart) {
159
+ if (allLines.length === 0 || height <= 0)
160
+ return { lines: [], scrollStart: 0 };
161
+ if (allLines.length <= height)
162
+ return { lines: allLines.slice(), scrollStart: 0 };
163
+ const first = Math.max(0, Math.min(cursorFirstLine, allLines.length - 1));
164
+ const last = Math.max(first, Math.min(cursorLastLine, allLines.length - 1));
165
+ let start = Math.max(0, prevStart);
166
+ // Scroll up if cursor is above viewport
167
+ if (first < start)
168
+ start = first;
169
+ // Scroll down if cursor is below viewport
170
+ if (last >= start + height)
171
+ start = last - height + 1;
172
+ // Clamp
173
+ start = Math.min(start, Math.max(0, allLines.length - height));
174
+ return { lines: allLines.slice(start, start + height), scrollStart: start };
175
+ }
176
+ // ── Help text ────────────────────────────────────────────────────────────────
177
+ export function shellHelpText() {
178
+ const hdr = (s) => style.bold(s);
179
+ const k = (s) => style.boldCyan(s);
180
+ const d = (s) => style.dim(s);
181
+ const cmd = (s) => style.cyan(s);
182
+ return [
183
+ "",
184
+ hdr("Navigation"),
185
+ ` ${k("← →")} ${d("switch tabs")} ${k("↑ ↓")} ${d("move cursor")} ${k("↵")} ${d("activate")} ${k("q")} ${d("quit")}`,
186
+ ` ${k("/")} ${d("filter")} ${k(":")} ${d("command palette")} ${k("Esc")} ${d("cancel / clear filter")} ${k("?")} ${d("toggle this help")}`,
187
+ "",
188
+ hdr("View-specific keys"),
189
+ ` ${style.bold("Projects")} ${k("↵")} ${d("open project tasks")} ${k("i")} ${d("cycle intro mode")}`,
190
+ ` ${style.bold("Tasks")} ${k("a")} ${d("add task")} ${k("d")} ${d("toggle active/queue")} ${k("↵")} ${d("mark complete")}`,
191
+ ` ${style.bold("Fragments")} ${k("a")} ${d("tell phren")} ${k("d")} ${d("delete selected")}`,
192
+ ` ${style.bold("Review Queue")} ${k("a")} ${d("approve")} ${k("r")} ${d("reject")} ${k("e")} ${d("edit")}`,
193
+ ` ${style.bold("Skills")} ${k("t")} ${d("toggle enabled")} ${k("d")} ${d("remove")}`,
194
+ "",
195
+ hdr("Palette commands (:cmd)"),
196
+ ` ${cmd(":open <project>")} ${d("set active project context")}`,
197
+ ` ${cmd(":add <task>")} ${d("add task")}`,
198
+ ` ${cmd(":complete <id|match>")} ${d("mark done")}`,
199
+ ` ${cmd(":move <id|match> <active|queue|done>")} ${d("move item")}`,
200
+ ` ${cmd(":reprioritize <id|match> <high|medium|low>")}`,
201
+ ` ${cmd(":context <id|match> <text>")}`,
202
+ ` ${cmd(":pin <id>")} ${cmd(":unpin <id>")} ${cmd(":work next")} ${cmd(":tidy [keep]")}`,
203
+ ` ${cmd(":find add <text>")} ${cmd(":find remove <id|match>")}`,
204
+ ` ${cmd(":intro always|once-per-version|off")}`,
205
+ ` ${cmd(":mq approve|reject|edit <id>")}`,
206
+ ` ${cmd(":govern")} ${cmd(":consolidate")} ${cmd(":search <query>")}`,
207
+ ` ${cmd(":undo")} ${cmd(":diff")} ${cmd(":conflicts")} ${cmd(":reset")}`,
208
+ ` ${cmd(":run fix")} ${cmd(":relink")} ${cmd(":rerun hooks")} ${cmd(":update")}`,
209
+ ` ${cmd(":machines")}`,
210
+ ].join("\n");
211
+ }
212
+ // ── Terminal control ──────────────────────────────────────────────────────────
213
+ export function clearScreen() {
214
+ if (process.stdout.isTTY) {
215
+ // Move cursor to home and overwrite in place (no full clear = no flicker)
216
+ process.stdout.write("\x1b[H");
217
+ }
218
+ }
219
+ // Clear any leftover lines below the rendered content
220
+ export function clearToEnd() {
221
+ if (process.stdout.isTTY) {
222
+ process.stdout.write("\x1b[J");
223
+ }
224
+ }
225
+ export function shellStartupFrames(version) {
226
+ const cols = process.stdout.columns || 80;
227
+ const tagline = style.dim("local memory for working agents");
228
+ const versionBadge = badge(`v${version}`, style.boldBlue);
229
+ if (cols >= 56) {
230
+ const logo = PHREN_LOGO.map(line => " " + gradient(line));
231
+ const phren = PHREN_STARTUP.map(line => " " + line);
232
+ const sep = gradient("━".repeat(Math.min(52, cols)));
233
+ return [
234
+ // Frame 1: Phren appears
235
+ ["", ...phren, "", ` ${versionBadge} ${tagline}`, ""].join("\n"),
236
+ // Frame 2: Full logo materializes with phren
237
+ ["", ...phren, "", ...logo, "", ` ${versionBadge} ${tagline}`, ""].join("\n"),
238
+ // Frame 3: Complete with brand separator
239
+ ["", ...phren, "", ...logo, ` ${sep}`, ` ${gradient("◆")} ${style.bold("phren")} ${versionBadge} ${tagline}`, ""].join("\n"),
240
+ ];
241
+ }
242
+ // Narrow terminal: progressive text reveal with gradient
243
+ const stages = ["c", "cor", "phren"];
244
+ const spinners = ["◜", "◠", "◝"];
245
+ return stages.map((stage, i) => [
246
+ "",
247
+ ` ${gradient(stage)} ${style.dim(spinners[i])}`,
248
+ "",
249
+ ` ${versionBadge} ${tagline}`,
250
+ "",
251
+ ].join("\n"));
252
+ }
@@ -0,0 +1,81 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { phrenErr, PhrenError, phrenOk, shellStateFile } from "./shared.js";
4
+ import { getRuntimeHealth, withFileLock as withFileLockRaw } from "./shared-governance.js";
5
+ import { errorMessage } from "./utils.js";
6
+ function withSafeLock(filePath, fn) {
7
+ try {
8
+ return withFileLockRaw(filePath, fn);
9
+ }
10
+ catch (err) {
11
+ const msg = errorMessage(err);
12
+ if (msg.includes("could not acquire lock")) {
13
+ return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
14
+ }
15
+ throw err;
16
+ }
17
+ }
18
+ const SHELL_STATE_VERSION = 3;
19
+ const VALID_VIEWS = new Set(["Projects", "Tasks", "Findings", "Review Queue", "Skills", "Hooks", "Machines/Profiles", "Health"]);
20
+ export function loadShellState(phrenPath) {
21
+ const file = shellStateFile(phrenPath);
22
+ const fallback = {
23
+ version: SHELL_STATE_VERSION,
24
+ view: "Projects",
25
+ page: 1,
26
+ perPage: 40,
27
+ introMode: "once-per-version",
28
+ };
29
+ if (!fs.existsSync(file))
30
+ return fallback;
31
+ try {
32
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
33
+ const persistedView = VALID_VIEWS.has(raw.view)
34
+ ? raw.view
35
+ : fallback.view;
36
+ return {
37
+ version: SHELL_STATE_VERSION,
38
+ view: persistedView,
39
+ project: raw.project,
40
+ filter: raw.filter,
41
+ page: Number.isFinite(raw.page) ? Number(raw.page) : fallback.page,
42
+ perPage: Number.isFinite(raw.perPage) ? Number(raw.perPage) : fallback.perPage,
43
+ introMode: raw.introMode === "always" || raw.introMode === "off" ? raw.introMode : "once-per-version",
44
+ introSeenVersion: typeof raw.introSeenVersion === "string" ? raw.introSeenVersion : undefined,
45
+ };
46
+ }
47
+ catch (err) {
48
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
49
+ process.stderr.write(`[phren] loadShellState parse: ${errorMessage(err)}\n`);
50
+ return fallback;
51
+ }
52
+ }
53
+ export function saveShellState(phrenPath, state) {
54
+ const file = shellStateFile(phrenPath);
55
+ fs.mkdirSync(path.dirname(file), { recursive: true });
56
+ withSafeLock(file, () => {
57
+ const out = {
58
+ version: SHELL_STATE_VERSION,
59
+ view: state.view,
60
+ project: state.project,
61
+ filter: state.filter,
62
+ page: state.page,
63
+ perPage: state.perPage,
64
+ introMode: state.introMode,
65
+ introSeenVersion: state.introSeenVersion,
66
+ };
67
+ fs.writeFileSync(file, JSON.stringify(out, null, 2) + "\n");
68
+ return phrenOk(undefined);
69
+ });
70
+ }
71
+ export function resetShellState(phrenPath) {
72
+ const file = shellStateFile(phrenPath);
73
+ return withSafeLock(file, () => {
74
+ if (fs.existsSync(file))
75
+ fs.unlinkSync(file);
76
+ return phrenOk("Shell state reset.");
77
+ });
78
+ }
79
+ export function readRuntimeHealth(phrenPath) {
80
+ return getRuntimeHealth(phrenPath);
81
+ }
@@ -0,0 +1,13 @@
1
+ // Projects is level 0 (the home screen); these sub-views are level 1 (drill-down into a project)
2
+ // Health is NOT a sub-view — it's a global overlay accessible from anywhere via [h]
3
+ export const SUB_VIEWS = ["Tasks", "Findings", "Review Queue", "Skills", "Hooks"];
4
+ export const TAB_ICONS = {
5
+ Projects: "◉",
6
+ Tasks: "▤",
7
+ Findings: "✦",
8
+ "Review Queue": "◈",
9
+ Skills: "◆",
10
+ Hooks: "⚡",
11
+ Health: "♡",
12
+ };
13
+ export const MAX_UNDO_STACK = 10;
@@ -0,0 +1,14 @@
1
+ import { RESET, padToWidth, truncateLine, lineViewport, style } from "./shell-render.js";
2
+ export function formatSelectableLine(line, cols, selected) {
3
+ return selected
4
+ ? `\x1b[7m${padToWidth(line, cols)}${RESET}`
5
+ : truncateLine(line, cols);
6
+ }
7
+ export function viewportWithStatus(allLines, cursorFirstLine, cursorLastLine, usableHeight, previousScroll, currentIndex, totalItems) {
8
+ const vp = lineViewport(allLines, cursorFirstLine, cursorLastLine, Math.max(1, usableHeight), previousScroll);
9
+ if (allLines.length > usableHeight) {
10
+ const pct = totalItems <= 1 ? 100 : Math.round((currentIndex / Math.max(totalItems - 1, 1)) * 100);
11
+ vp.lines.push(style.dim(` ━━━${currentIndex + 1}/${totalItems} ${pct}%`));
12
+ }
13
+ return vp;
14
+ }