@phren/cli 0.0.55 → 0.0.57

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 (165) hide show
  1. package/README.md +70 -92
  2. package/{mcp/dist → dist}/cli/cli.js +10 -0
  3. package/{mcp/dist → dist}/cli/hooks-session.js +47 -0
  4. package/{mcp/dist → dist}/entrypoint.js +3 -0
  5. package/{mcp/dist → dist}/package-metadata.js +1 -1
  6. package/{mcp/dist → dist}/session/utils.js +24 -1
  7. package/dist/shell/render-api.js +101 -0
  8. package/{mcp/dist → dist}/test-global-setup.js +4 -4
  9. package/{mcp/dist → dist}/tools/session.js +10 -37
  10. package/package.json +25 -38
  11. package/LICENSE +0 -21
  12. package/icon.svg +0 -416
  13. /package/{mcp/dist → dist}/capabilities/cli.js +0 -0
  14. /package/{mcp/dist → dist}/capabilities/index.js +0 -0
  15. /package/{mcp/dist → dist}/capabilities/mcp.js +0 -0
  16. /package/{mcp/dist → dist}/capabilities/types.js +0 -0
  17. /package/{mcp/dist → dist}/capabilities/vscode.js +0 -0
  18. /package/{mcp/dist → dist}/capabilities/web-ui.js +0 -0
  19. /package/{mcp/dist → dist}/cli/actions.js +0 -0
  20. /package/{mcp/dist → dist}/cli/config.js +0 -0
  21. /package/{mcp/dist → dist}/cli/extract.js +0 -0
  22. /package/{mcp/dist → dist}/cli/govern.js +0 -0
  23. /package/{mcp/dist → dist}/cli/graph.js +0 -0
  24. /package/{mcp/dist → dist}/cli/hooks-citations.js +0 -0
  25. /package/{mcp/dist → dist}/cli/hooks-context.js +0 -0
  26. /package/{mcp/dist → dist}/cli/hooks-globs.js +0 -0
  27. /package/{mcp/dist → dist}/cli/hooks-output.js +0 -0
  28. /package/{mcp/dist → dist}/cli/hooks.js +0 -0
  29. /package/{mcp/dist → dist}/cli/namespaces.js +0 -0
  30. /package/{mcp/dist → dist}/cli/ops.js +0 -0
  31. /package/{mcp/dist → dist}/cli/search.js +0 -0
  32. /package/{mcp/dist → dist}/cli/team.js +0 -0
  33. /package/{mcp/dist → dist}/cli-hooks-git.js +0 -0
  34. /package/{mcp/dist → dist}/cli-hooks-prompt.js +0 -0
  35. /package/{mcp/dist → dist}/cli-hooks-session-handlers.js +0 -0
  36. /package/{mcp/dist → dist}/cli-hooks-stop.js +0 -0
  37. /package/{mcp/dist → dist}/content/archive.js +0 -0
  38. /package/{mcp/dist → dist}/content/citation.js +0 -0
  39. /package/{mcp/dist → dist}/content/dedup.js +0 -0
  40. /package/{mcp/dist → dist}/content/learning.js +0 -0
  41. /package/{mcp/dist → dist}/content/metadata.js +0 -0
  42. /package/{mcp/dist → dist}/content/validate.js +0 -0
  43. /package/{mcp/dist → dist}/core/finding.js +0 -0
  44. /package/{mcp/dist → dist}/core/project.js +0 -0
  45. /package/{mcp/dist → dist}/core/search.js +0 -0
  46. /package/{mcp/dist → dist}/data/access.js +0 -0
  47. /package/{mcp/dist → dist}/data/tasks.js +0 -0
  48. /package/{mcp/dist → dist}/embedding.js +0 -0
  49. /package/{mcp/dist → dist}/finding/context.js +0 -0
  50. /package/{mcp/dist → dist}/finding/impact.js +0 -0
  51. /package/{mcp/dist → dist}/finding/journal.js +0 -0
  52. /package/{mcp/dist → dist}/finding/lifecycle.js +0 -0
  53. /package/{mcp/dist → dist}/generated/memory-ui-graph.browser.js +0 -0
  54. /package/{mcp/dist → dist}/governance/audit.js +0 -0
  55. /package/{mcp/dist → dist}/governance/locks.js +0 -0
  56. /package/{mcp/dist → dist}/governance/policy.js +0 -0
  57. /package/{mcp/dist → dist}/governance/rbac.js +0 -0
  58. /package/{mcp/dist → dist}/governance/scores.js +0 -0
  59. /package/{mcp/dist → dist}/hooks.js +0 -0
  60. /package/{mcp/dist → dist}/index-query.js +0 -0
  61. /package/{mcp/dist → dist}/index.js +0 -0
  62. /package/{mcp/dist → dist}/init/config.js +0 -0
  63. /package/{mcp/dist → dist}/init/init-configure.js +0 -0
  64. /package/{mcp/dist → dist}/init/init-hooks-mode.js +0 -0
  65. /package/{mcp/dist → dist}/init/init-mcp-mode.js +0 -0
  66. /package/{mcp/dist → dist}/init/init-uninstall.js +0 -0
  67. /package/{mcp/dist → dist}/init/init-walkthrough.js +0 -0
  68. /package/{mcp/dist → dist}/init/init.js +0 -0
  69. /package/{mcp/dist → dist}/init/preferences.js +0 -0
  70. /package/{mcp/dist → dist}/init/setup.js +0 -0
  71. /package/{mcp/dist → dist}/init/shared.js +0 -0
  72. /package/{mcp/dist → dist}/init-bootstrap.js +0 -0
  73. /package/{mcp/dist → dist}/init-detect.js +0 -0
  74. /package/{mcp/dist → dist}/init-env.js +0 -0
  75. /package/{mcp/dist → dist}/init-fresh.js +0 -0
  76. /package/{mcp/dist → dist}/init-hooks.js +0 -0
  77. /package/{mcp/dist → dist}/init-mcp.js +0 -0
  78. /package/{mcp/dist → dist}/init-modes.js +0 -0
  79. /package/{mcp/dist → dist}/init-npm.js +0 -0
  80. /package/{mcp/dist → dist}/init-project-local.js +0 -0
  81. /package/{mcp/dist → dist}/init-semantic.js +0 -0
  82. /package/{mcp/dist → dist}/init-types.js +0 -0
  83. /package/{mcp/dist → dist}/init-uninstall.js +0 -0
  84. /package/{mcp/dist → dist}/init-update.js +0 -0
  85. /package/{mcp/dist → dist}/init-walkthrough.js +0 -0
  86. /package/{mcp/dist → dist}/link/checksums.js +0 -0
  87. /package/{mcp/dist → dist}/link/context.js +0 -0
  88. /package/{mcp/dist → dist}/link/doctor.js +0 -0
  89. /package/{mcp/dist → dist}/link/link.js +0 -0
  90. /package/{mcp/dist → dist}/link/skills.js +0 -0
  91. /package/{mcp/dist → dist}/logger.js +0 -0
  92. /package/{mcp/dist → dist}/machine-identity.js +0 -0
  93. /package/{mcp/dist → dist}/memory-ui-graph.runtime.js +0 -0
  94. /package/{mcp/dist → dist}/phren-art.js +0 -0
  95. /package/{mcp/dist → dist}/phren-core.js +0 -0
  96. /package/{mcp/dist → dist}/phren-dotenv.js +0 -0
  97. /package/{mcp/dist → dist}/phren-paths.js +0 -0
  98. /package/{mcp/dist → dist}/proactivity.js +0 -0
  99. /package/{mcp/dist → dist}/profile-store.js +0 -0
  100. /package/{mcp/dist → dist}/project-config.js +0 -0
  101. /package/{mcp/dist → dist}/project-locator.js +0 -0
  102. /package/{mcp/dist → dist}/project-topics.js +0 -0
  103. /package/{mcp/dist → dist}/provider-adapters.js +0 -0
  104. /package/{mcp/dist → dist}/query-correlation.js +0 -0
  105. /package/{mcp/dist → dist}/runtime-profile.js +0 -0
  106. /package/{mcp/dist → dist}/session/checkpoints.js +0 -0
  107. /package/{mcp/dist → dist}/shared/content.js +0 -0
  108. /package/{mcp/dist → dist}/shared/data-utils.js +0 -0
  109. /package/{mcp/dist → dist}/shared/embedding-cache.js +0 -0
  110. /package/{mcp/dist → dist}/shared/fragment-graph.js +0 -0
  111. /package/{mcp/dist → dist}/shared/governance.js +0 -0
  112. /package/{mcp/dist → dist}/shared/index.js +0 -0
  113. /package/{mcp/dist → dist}/shared/ollama.js +0 -0
  114. /package/{mcp/dist → dist}/shared/process.js +0 -0
  115. /package/{mcp/dist → dist}/shared/retrieval.js +0 -0
  116. /package/{mcp/dist → dist}/shared/search-fallback.js +0 -0
  117. /package/{mcp/dist → dist}/shared/sqljs.js +0 -0
  118. /package/{mcp/dist → dist}/shared/stemmer.js +0 -0
  119. /package/{mcp/dist → dist}/shared/vector-index.js +0 -0
  120. /package/{mcp/dist → dist}/shared.js +0 -0
  121. /package/{mcp/dist → dist}/shell/entry.js +0 -0
  122. /package/{mcp/dist → dist}/shell/input.js +0 -0
  123. /package/{mcp/dist → dist}/shell/palette.js +0 -0
  124. /package/{mcp/dist → dist}/shell/render.js +0 -0
  125. /package/{mcp/dist → dist}/shell/shell.js +0 -0
  126. /package/{mcp/dist → dist}/shell/state-store.js +0 -0
  127. /package/{mcp/dist → dist}/shell/types.js +0 -0
  128. /package/{mcp/dist → dist}/shell/view-list.js +0 -0
  129. /package/{mcp/dist → dist}/shell/view.js +0 -0
  130. /package/{mcp/dist → dist}/skill/files.js +0 -0
  131. /package/{mcp/dist → dist}/skill/registry.js +0 -0
  132. /package/{mcp/dist → dist}/skill/state.js +0 -0
  133. /package/{mcp/dist → dist}/startup-embedding.js +0 -0
  134. /package/{mcp/dist → dist}/status.js +0 -0
  135. /package/{mcp/dist → dist}/store-registry.js +0 -0
  136. /package/{mcp/dist → dist}/store-routing.js +0 -0
  137. /package/{mcp/dist → dist}/synonyms.json +0 -0
  138. /package/{mcp/dist → dist}/task/github.js +0 -0
  139. /package/{mcp/dist → dist}/task/hygiene.js +0 -0
  140. /package/{mcp/dist → dist}/task/lifecycle.js +0 -0
  141. /package/{mcp/dist → dist}/telemetry.js +0 -0
  142. /package/{mcp/dist → dist}/tool-registry.js +0 -0
  143. /package/{mcp/dist → dist}/tools/config.js +0 -0
  144. /package/{mcp/dist → dist}/tools/data.js +0 -0
  145. /package/{mcp/dist → dist}/tools/extract-facts.js +0 -0
  146. /package/{mcp/dist → dist}/tools/extract.js +0 -0
  147. /package/{mcp/dist → dist}/tools/finding.js +0 -0
  148. /package/{mcp/dist → dist}/tools/graph.js +0 -0
  149. /package/{mcp/dist → dist}/tools/hooks.js +0 -0
  150. /package/{mcp/dist → dist}/tools/memory.js +0 -0
  151. /package/{mcp/dist → dist}/tools/ops.js +0 -0
  152. /package/{mcp/dist → dist}/tools/search.js +0 -0
  153. /package/{mcp/dist → dist}/tools/skills.js +0 -0
  154. /package/{mcp/dist → dist}/tools/tasks.js +0 -0
  155. /package/{mcp/dist → dist}/tools/types.js +0 -0
  156. /package/{mcp/dist → dist}/ui/assets.js +0 -0
  157. /package/{mcp/dist → dist}/ui/data.js +0 -0
  158. /package/{mcp/dist → dist}/ui/graph.js +0 -0
  159. /package/{mcp/dist → dist}/ui/memory-ui.js +0 -0
  160. /package/{mcp/dist → dist}/ui/page.js +0 -0
  161. /package/{mcp/dist → dist}/ui/scripts.js +0 -0
  162. /package/{mcp/dist → dist}/ui/server.js +0 -0
  163. /package/{mcp/dist → dist}/ui/styles.js +0 -0
  164. /package/{mcp/dist → dist}/update.js +0 -0
  165. /package/{mcp/dist → dist}/utils.js +0 -0
package/README.md CHANGED
@@ -1,19 +1,22 @@
1
- <p align="center"><img src="docs/phren-transparent.png" width="180" alt="phren"></p>
1
+ # phren MCP server
2
2
 
3
- <h3 align="center">Your agents forget everything. Phren doesn't.</h3>
3
+ MCP server that indexes your personal phren and exposes it to AI agents via full-text search.
4
4
 
5
- <p align="center">
6
- <a href="https://www.npmjs.com/package/@phren/cli"><img src="https://img.shields.io/npm/v/%40phren%2Fcli?style=flat&labelColor=0D0D0D&color=7C3AED" alt="npm version"></a>
7
- <a href="https://github.com/alaarab/phren/blob/main/LICENSE"><img src="https://img.shields.io/github/license/alaarab/phren?style=flat&labelColor=0D0D0D&color=7C3AED" alt="license"></a>
8
- <a href="https://alaarab.github.io/phren/"><img src="https://img.shields.io/badge/docs-alaarab.github.io%2Fphren-7C3AED?style=flat&labelColor=0D0D0D" alt="docs"></a>
9
- <a href="https://alaarab.github.io/phren/whitepaper.pdf"><img src="https://img.shields.io/badge/whitepaper-PDF-7C3AED?style=flat&labelColor=0D0D0D" alt="whitepaper"></a>
10
- </p>
5
+ On startup it walks your phren directory, reads all `.md` files, and builds an in-memory SQLite FTS5 index.
11
6
 
12
- <p align="center">
13
- Persistent memory for AI agents. Findings, tasks, and patterns live in markdown files in a git repo you control. No database, no vendor lock-in. Works with Claude, Copilot, Cursor, and Codex.
14
- </p>
7
+ Public surface: 53 MCP tools across 12 modules (search, tasks, findings, memory, data, graph, sessions, ops/review, skills, hooks, config, extraction).
15
8
 
16
- ---
9
+ Notable shipped capabilities:
10
+ - finding lifecycle tools: `supersede_finding`, `retract_finding`, `resolve_contradiction`, `get_contradictions`
11
+ - finding provenance: `add_finding.source` (`human|agent|hook|extract|consolidation|unknown`)
12
+ - cross-session continuity: task checkpoints + `session_history`
13
+ - finding impact scoring from injected-context outcomes
14
+ - skill registry behavior: scope precedence, alias-collision handling, visibility gating, generated `skill-manifest.json`
15
+ - lifecycle penalties: superseded 0.25×, retracted 0.1×, contradicted 0.4× confidence in retrieval
16
+ - inactive findings stripped from FTS index (superseded/retracted findings cannot appear in search)
17
+ - auto-tagging: findings without type tags are inferred from content at write time
18
+ - session context diff: `session_start` reports new findings since last session
19
+ - decay resistance: confirmed findings decay 3× slower when repeatedly useful
17
20
 
18
21
  ## Install
19
22
 
@@ -21,109 +24,84 @@ Persistent memory for AI agents. Findings, tasks, and patterns live in markdown
21
24
  npx @phren/cli init
22
25
  ```
23
26
 
24
- One command. Sets up `~/.phren`, wires up MCP for your tools, installs hooks. Next time you open a project, context starts flowing automatically. On a new machine? Re-run init and you're back in sync.
27
+ This sets up phren without needing `sudo` or a global install. On Windows, use `npm install -g @phren/cli` if `npx` isn't available.
25
28
 
26
- ---
29
+ Or add manually to Claude Code:
27
30
 
28
- ## What actually happens
29
-
30
- **When you open a prompt:**
31
- - Hooks extract keywords from your question
32
- - Phren searches findings across projects (FTS5 full-text with semantic fallback)
33
- - Relevant snippets inject into your prompt before you hit send
34
- - You ask; Claude already knows the gotchas
35
-
36
- **When you discover something:**
37
- - `phren add-finding <project> "finding text"` captures it with optional tags (`[decision]`, `[pattern]`, `[pitfall]`, `[bug]`)
38
- - Trust scores decay over time; decisions never do; observations expire in 14 days
39
- - Findings link to fragments (named concepts like "auth" or "build") that connect knowledge across projects
31
+ ```bash
32
+ claude mcp add phren -- phren ~/.phren
33
+ ```
40
34
 
41
- **Sessions:**
42
- - Mark boundaries with `session_start` / `session_end`
43
- - Next session sees your prior summary, active tasks, recent findings, and where you left off
44
- - Checkpoints track edited files and failing tests so you can resume exactly where you stopped
35
+ ## Environment variables
45
36
 
46
- **Tasks:**
47
- - Add with priority/section. Pin across sessions. Link to GitHub issues.
48
- - Track completions and cross-project rollups.
37
+ | Variable | Default | Description |
38
+ |----------|---------|-------------|
39
+ | `PHREN_PATH` | `~/.phren` | Path to your phren instance |
40
+ | `PHREN_PROFILE` | *(none)* | Active profile name. When unset, phren uses `machines.yaml` when available and otherwise falls back to an unscoped view |
41
+ | `PHREN_ACTOR` | OS user / env | Actor identity used in governance/audit RBAC checks |
49
42
 
50
- ---
43
+ ## Tools
51
44
 
52
- ## Key features
45
+ See [docs/api-reference.md](../docs/api-reference.md) for the full API reference.
53
46
 
54
- ### Fragment graph
55
- Explore connections visually. Drag nodes to reorganize; graph auto-settles. Click a fragment to see every finding linked to it across all projects.
47
+ ## Integration model
56
48
 
57
- ### Finding lifecycle
58
- - **Supersede**: "Finding X is obsoleted by finding Y"
59
- - **Retract**: "We were wrong about this; here's why"
60
- - **Contradict**: "We have two findings that conflict; this is why"
49
+ - Claude: full native lifecycle hooks (`SessionStart`, `UserPromptSubmit`, `Stop`, `PostToolUse`) + MCP
50
+ - Copilot CLI / Cursor / Codex: MCP + generated hook config + session wrapper binaries
61
51
 
62
- Helps you reason about contradictions instead of hiding them.
52
+ ## Governance and security highlights
63
53
 
64
- ### Multi-agent support
65
- Same store works with Claude Code, Copilot, Cursor, and Codex. Agents tag findings with their tool, so you see who discovered what.
54
+ - RBAC uses `.config/access-control.json` and `.runtime/access-control.local.json`
55
+ - Web UI binds loopback-only, uses per-run auth token, enforces CSRF for mutations, and sets CSP headers
56
+ - Telemetry is opt-in only (`phren config telemetry on`) and stored locally in `.runtime/telemetry.json`
66
57
 
67
- ### Review queue
68
- Mark findings as needing review (`[Review]` section). Phren surfaces review items on every session start. Approve, reject, or edit in place.
58
+ ### search_knowledge
69
59
 
70
- ### Governance & policies
71
- Per-project retention policies. Confidence decay curves. Access control. Audit logs. Configure with `phren config` or the web UI.
60
+ Full-text search across all indexed markdown files with synonym expansion.
72
61
 
73
- ### Store subscriptions
74
- Subscribe to specific projects in a team store — others stay hidden from search and context injection:
75
- ```bash
76
- phren store subscribe team-store arc intranet
77
- phren store unsubscribe team-store legacy-projects
78
62
  ```
79
-
80
- ### Progressive disclosure
81
- Enable `PHREN_FEATURE_PROGRESSIVE_DISCLOSURE=1` to get compact memory indices instead of full snippets. Call `get_memory_detail(id)` to expand only what you need.
82
-
83
- ### Semantic dedup & conflict detection
84
- Optional: enable LLM-based duplicate detection and contradiction flagging on `add_finding`. Prevents near-duplicate entries and catches "always use X" vs "never use X" contradictions.
85
-
86
- ### Skills & hooks
87
- Drop custom slash commands into `~/.phren/global/skills/`. Hooks run on user prompt, tool use, and session events — wire phren into your own workflows.
88
-
89
- ---
90
-
91
- ## CLI quick reference
92
-
93
- ```bash
94
- phren Interactive shell (explore/search)
95
- phren search <query> Full-text search with FTS5
96
- phren add-finding <project> "insight" Capture a finding
97
- phren task add <project> "item" Add a task
98
- phren session_start <project> Start a session
99
- phren store list List personal + team stores
100
- phren team init <name> --remote <url> Create a team store
101
- phren team join <url> Join a team store
102
- phren web-ui [--port 3499] Launch the web UI
103
- phren doctor Health check & auto-fix
63
+ query: string - FTS5 query (supports AND, OR, NOT, "phrase matching")
64
+ limit?: number - Max results, 1-20, default 5
65
+ type?: string - Filter: "claude", "findings", "reference", "summary", "task", "skill"
66
+ project?: string - Filter to a specific project
67
+ tag?: string - Filter findings by type tag: decision, pitfall, pattern, tradeoff, architecture, bug
68
+ since?: string - Filter findings by date: "7d", "30d", "YYYY-MM", "YYYY-MM-DD"
69
+ status?: string - Filter by lifecycle status: active, superseded, contradicted, stale, invalid_citation, retracted
70
+ include_history?: boolean - Include superseded/retracted findings (default false)
71
+ synthesize?: boolean - Generate a synthesis paragraph from top results using an LLM
104
72
  ```
105
73
 
106
- See full CLI docs at [alaarab.github.io/phren](https://alaarab.github.io/phren/).
74
+ ### get_project_summary
107
75
 
108
- ---
76
+ Returns a project's summary.md content, path to its CLAUDE.md, and a list of indexed files.
109
77
 
110
- ## Team stores
78
+ ```
79
+ name: string - Project name (e.g. "my-app", "backend")
80
+ ```
111
81
 
112
- Shared knowledge repos for teams. One person creates with `phren team init`, others join with `phren team join`. Findings, tasks, and skills sync across team members.
82
+ ### list_projects
113
83
 
114
- Each team store can be configured with per-project subscriptions so people only see what they care about.
84
+ Lists all projects in the active profile with a brief description and which docs exist.
115
85
 
116
- ---
86
+ No parameters.
117
87
 
118
- ## Platforms
88
+ ## How it works
119
89
 
120
- - **Claude Code** (VS Code, Web, Desktop) — MCP hooks + CLI
121
- - **Copilot** (VS Code, GitHub.com) MCP hooks
122
- - **Cursor** (IDE) MCP hooks + built-in skill system
123
- - **Codex** (Claude Agent SDK) MCP tools + hooks
90
+ 1. Reads `PHREN_PATH` (or defaults to `~/.phren`)
91
+ 2. Resolves the active profile from `PHREN_PROFILE`, or from `machines.yaml` when the env var is unset
92
+ 3. If no active profile can be resolved yet, falls back to an unscoped view of top-level project directories
93
+ 4. Walks each project directory, reads `.md` files, classifies them by filename
94
+ 5. Builds an in-memory SQLite FTS5 index with Porter stemming
95
+ 6. Serves tools over stdio using the MCP protocol
124
96
 
125
- All use the same phren store. No vendor lock-in.
97
+ File types are derived from filenames: `CLAUDE.md` -> "claude", `summary.md` -> "summary", `FINDINGS.md` -> "findings", `tasks.md` -> "task", files under `reference/` -> "reference", files under `skills/` -> "skill".
126
98
 
127
- ---
99
+ ## Development
128
100
 
129
- MIT License. Made by [Ala Arab](https://github.com/alaarab).
101
+ ```bash
102
+ cd ~/phren
103
+ npm install
104
+ npm run build # Compile TypeScript
105
+ npm run dev # Run with tsx (hot reload)
106
+ npm test # Run all tests
107
+ ```
@@ -118,6 +118,16 @@ export async function runCliCommand(command, args) {
118
118
  return handleTeamNamespace(args);
119
119
  case "promote":
120
120
  return handlePromoteNamespace(args);
121
+ case "agent": {
122
+ try {
123
+ const { runAgentCli } = await import("@phren/agent");
124
+ return runAgentCli(args);
125
+ }
126
+ catch {
127
+ console.error("@phren/agent is not installed. Install it with: npm i -g @phren/agent");
128
+ process.exit(1);
129
+ }
130
+ }
121
131
  default:
122
132
  console.error(`Unknown command: ${command}\nRun 'phren --help' for available commands.`);
123
133
  process.exit(1);
@@ -13,6 +13,8 @@ import { isTaskFileName, TASKS_FILENAME } from "../data/tasks.js";
13
13
  import { buildIndex, queryRows, } from "../shared/index.js";
14
14
  import { filterTaskByPriority } from "../shared/retrieval.js";
15
15
  import { logger } from "../logger.js";
16
+ import * as crypto from "crypto";
17
+ import { sessionFileForId, readSessionStateFile, writeSessionStateFile, } from "../session/utils.js";
16
18
  const SYNC_LOCK_STALE_MS = 10 * 60 * 1000; // 10 minutes
17
19
  const MAINTENANCE_LOCK_STALE_MS = 2 * 60 * 60 * 1000; // 2 hours
18
20
  export { buildHookContext, handleGuardSkip } from "./hooks-context.js";
@@ -671,6 +673,51 @@ export async function handleHookSessionStart() {
671
673
  catch (err) {
672
674
  debugLog(`session-start onboarding detection failed: ${errorMessage(err)}`);
673
675
  }
676
+ // ── Bridge: create a real session record so session_history tracks hook sessions ──
677
+ // Uses a file lock to prevent concurrent SessionStart hooks from racing on
678
+ // the active-hook-session pointer (read previous ID, end it, write new ID).
679
+ try {
680
+ const activeSessionFile = runtimeFile(phrenPath, "active-hook-session");
681
+ withFileLock(activeSessionFile, () => {
682
+ // Retroactively end the previous hook session (if any)
683
+ try {
684
+ const prevId = fs.readFileSync(activeSessionFile, "utf-8").trim();
685
+ if (prevId) {
686
+ const prevFile = sessionFileForId(phrenPath, prevId);
687
+ const prevState = readSessionStateFile(prevFile);
688
+ if (prevState && !prevState.endedAt) {
689
+ writeSessionStateFile(prevFile, { ...prevState, endedAt: startedAt });
690
+ }
691
+ }
692
+ }
693
+ catch (err) {
694
+ // ENOENT is expected on the very first session — only log other errors
695
+ if (err.code !== "ENOENT") {
696
+ debugLog(`session-bridge end-previous failed: ${errorMessage(err)}`);
697
+ }
698
+ }
699
+ // Create new session record
700
+ const sessionId = crypto.randomUUID();
701
+ const sessionState = {
702
+ sessionId,
703
+ project: activeProject || undefined,
704
+ startedAt,
705
+ findingsAdded: 0,
706
+ tasksCompleted: 0,
707
+ hookCreated: true,
708
+ };
709
+ writeSessionStateFile(sessionFileForId(phrenPath, sessionId), sessionState);
710
+ // Write active session ID atomically so other hooks (stop, tool) can reference it.
711
+ // Plain text format (not JSON) because the reader uses readFileSync + trim.
712
+ const tmpPath = `${activeSessionFile}.${process.pid}.${Date.now()}.tmp`;
713
+ fs.writeFileSync(tmpPath, sessionId + "\n");
714
+ fs.renameSync(tmpPath, activeSessionFile);
715
+ debugLog(`session-bridge created session ${sessionId.slice(0, 8)} for hook-driven session`);
716
+ });
717
+ }
718
+ catch (err) {
719
+ debugLog(`session-bridge failed: ${errorMessage(err)}`);
720
+ }
674
721
  }
675
722
  // ── Q21: Conversation memory capture ─────────────────────────────────────────
676
723
  const INSIGHT_KEYWORDS = [
@@ -18,6 +18,8 @@ const HELP_TEXT = `phren - persistent knowledge for your agents
18
18
  phren web-ui Open the knowledge graph
19
19
  phren tasks Cross-project task view
20
20
  phren graph Fragment knowledge graph
21
+ phren agent <task> Run the coding agent
22
+ phren agent -i Interactive agent TUI
21
23
 
22
24
  phren store list List registered stores
23
25
  phren team init <name> Create a team store
@@ -187,6 +189,7 @@ const CLI_COMMANDS = [
187
189
  "store",
188
190
  "team",
189
191
  "promote",
192
+ "agent",
190
193
  ];
191
194
  async function flushTopLevelOutput() {
192
195
  await Promise.all([
@@ -2,7 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
- export const ROOT = path.join(__dirname, "..", "..");
5
+ export const ROOT = path.join(__dirname, "..");
6
6
  const PACKAGE_JSON_PATH = path.join(ROOT, "package.json");
7
7
  function readPackageJson() {
8
8
  return JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, "utf8"));
@@ -11,12 +11,35 @@ export function atomicWriteJson(filePath, data) {
11
11
  fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n");
12
12
  fs.renameSync(tmpPath, filePath);
13
13
  }
14
+ export function sessionsDir(phrenPath) {
15
+ const dir = path.join(phrenPath, ".runtime", "sessions");
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ return dir;
18
+ }
19
+ export function sessionFileForId(phrenPath, sessionId) {
20
+ return path.join(sessionsDir(phrenPath), `session-${sessionId}.json`);
21
+ }
22
+ export function readSessionStateFile(file) {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
25
+ }
26
+ catch (err) {
27
+ // ENOENT is expected for missing files — only log other errors
28
+ if (err.code !== "ENOENT") {
29
+ debugError("readSessionStateFile", err);
30
+ }
31
+ return null;
32
+ }
33
+ }
34
+ export function writeSessionStateFile(file, state) {
35
+ atomicWriteJson(file, state);
36
+ }
14
37
  /**
15
38
  * Log an error to stderr when PHREN_DEBUG is enabled.
16
39
  * Centralises the repeated `if (PHREN_DEBUG) stderr.write(...)` pattern.
17
40
  */
18
41
  export function debugError(scope, err) {
19
- if ((process.env.PHREN_DEBUG)) {
42
+ if (process.env.PHREN_DEBUG) {
20
43
  process.stderr.write(`[phren] ${scope}: ${errorMessage(err)}\n`);
21
44
  }
22
45
  }
@@ -0,0 +1,101 @@
1
+ import { listProjectCards } from "../data/access.js";
2
+ import { renderShell } from "./view.js";
3
+ import { getListItems } from "./input.js";
4
+ // ── View cycling order (same as the shell tab bar) ──────────────────────────
5
+ const VIEW_ORDER = [
6
+ "Projects", "Tasks", "Findings", "Review Queue", "Skills", "Hooks",
7
+ ];
8
+ // ── Render ──────────────────────────────────────────────────────────────────
9
+ /** Render a full shell frame for the given state. Read-only, no mutations. */
10
+ export async function renderMenuFrame(phrenPath, profile, state) {
11
+ const shellState = {
12
+ version: 3,
13
+ view: state.view,
14
+ project: state.project,
15
+ filter: state.filter,
16
+ };
17
+ const ctx = {
18
+ phrenPath,
19
+ profile,
20
+ state: shellState,
21
+ currentCursor: () => state.cursor,
22
+ currentScroll: () => state.scroll,
23
+ setScroll: () => { },
24
+ };
25
+ const stubDoctor = async () => ({
26
+ ok: true,
27
+ checks: [],
28
+ });
29
+ const output = await renderShell(ctx, "navigate", // always navigate mode (no input mode in agent menu)
30
+ "", // no inputCtx
31
+ "", // no inputBuf
32
+ false, // no help
33
+ "Tab: Chat ←→: Views ↑↓: Navigate Enter: Select /: Filter Esc: Back", stubDoctor, null, // subsectionsCache
34
+ () => { }, // setHealthLineCount
35
+ () => { });
36
+ const listCount = getListItems(phrenPath, profile, shellState, 0).length;
37
+ return { output, listCount };
38
+ }
39
+ // ── Navigation (pure function) ──────────────────────────────────────────────
40
+ /**
41
+ * Apply a key to the menu state. Returns a new state, or null to signal
42
+ * "exit menu mode" (Tab or Escape at top level).
43
+ */
44
+ export function handleMenuKey(state, keyName, listCount, phrenPath, profile) {
45
+ switch (keyName) {
46
+ // Exit menu
47
+ case "tab":
48
+ case "q":
49
+ return null;
50
+ // View cycling
51
+ case "left": {
52
+ const idx = VIEW_ORDER.indexOf(state.view);
53
+ const next = idx <= 0 ? VIEW_ORDER[VIEW_ORDER.length - 1] : VIEW_ORDER[idx - 1];
54
+ return { ...state, view: next, cursor: 0, scroll: 0, filter: undefined };
55
+ }
56
+ case "right": {
57
+ const idx = VIEW_ORDER.indexOf(state.view);
58
+ const next = idx >= VIEW_ORDER.length - 1 ? VIEW_ORDER[0] : VIEW_ORDER[idx + 1];
59
+ return { ...state, view: next, cursor: 0, scroll: 0, filter: undefined };
60
+ }
61
+ // Cursor movement
62
+ case "up":
63
+ return { ...state, cursor: Math.max(0, state.cursor - 1) };
64
+ case "down":
65
+ return { ...state, cursor: Math.min(Math.max(0, listCount - 1), state.cursor + 1) };
66
+ // Enter: drill into project (Projects view only)
67
+ case "return": {
68
+ if (state.view === "Projects" && phrenPath && profile) {
69
+ const cards = listProjectCards(phrenPath, profile);
70
+ const filtered = state.filter
71
+ ? cards.filter((c) => `${c.name} ${c.summary} ${c.docs.join(" ")}`.toLowerCase().includes(state.filter.toLowerCase()))
72
+ : cards;
73
+ const selected = filtered[state.cursor];
74
+ if (selected) {
75
+ return { ...state, view: "Tasks", project: selected.name, cursor: 0, scroll: 0 };
76
+ }
77
+ }
78
+ // Escape from sub-view back to Projects
79
+ if (state.view !== "Projects" && state.project) {
80
+ return state; // no-op for non-projects views on enter
81
+ }
82
+ return state;
83
+ }
84
+ // Escape: clear filter, or go back to projects, or exit
85
+ case "escape": {
86
+ if (state.filter)
87
+ return { ...state, filter: undefined, cursor: 0, scroll: 0 };
88
+ if (state.view !== "Projects" && state.project) {
89
+ return { ...state, view: "Projects", project: undefined, cursor: 0, scroll: 0 };
90
+ }
91
+ return null; // exit menu
92
+ }
93
+ // Health shortcut
94
+ case "h":
95
+ if (state.view === "Health")
96
+ return { ...state, view: "Projects", cursor: 0, scroll: 0 };
97
+ return { ...state, view: "Health", cursor: 0, scroll: 0 };
98
+ default:
99
+ return state;
100
+ }
101
+ }
@@ -17,8 +17,8 @@ import * as path from "path";
17
17
  import { fileURLToPath } from "url";
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = path.dirname(__filename);
20
- const REPO_ROOT = path.resolve(__dirname, "../..");
21
- const CLI_PATH = path.join(REPO_ROOT, "mcp", "dist", "index.js");
20
+ const REPO_ROOT = path.resolve(__dirname, "../../..");
21
+ const CLI_PATH = path.join(REPO_ROOT, "packages", "cli", "dist", "index.js");
22
22
  export async function setup() {
23
23
  if (fs.existsSync(CLI_PATH)) {
24
24
  // Dist already present — skip build. This is the common path when
@@ -26,8 +26,8 @@ export async function setup() {
26
26
  // re-runs where the artifact is still fresh.
27
27
  return;
28
28
  }
29
- process.stdout.write("[test-global-setup] mcp/dist missing — building...\n");
30
- execFileSync("npm", ["run", "build"], {
29
+ process.stdout.write("[test-global-setup] packages/cli/dist missing — building...\n");
30
+ execFileSync("pnpm", ["build"], {
31
31
  cwd: REPO_ROOT,
32
32
  stdio: "inherit",
33
33
  timeout: 60_000,
@@ -4,7 +4,7 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import * as crypto from "crypto";
6
6
  import { execFileSync } from "child_process";
7
- import { debugLog, isMemoryScopeVisible, normalizeMemoryScope } from "../shared.js";
7
+ import { debugLog, getProjectDirs, isMemoryScopeVisible, normalizeMemoryScope } from "../shared.js";
8
8
  import { withFileLock } from "../shared/governance.js";
9
9
  import { isValidProjectName, errorMessage } from "../utils.js";
10
10
  import { runCustomHooks } from "../hooks.js";
@@ -12,11 +12,10 @@ import { readExtractedFacts } from "./extract-facts.js";
12
12
  import { resolveFindingSessionId } from "../finding/context.js";
13
13
  import { readTasks } from "../data/tasks.js";
14
14
  import { readFindings } from "../data/access.js";
15
- import { getProjectDirs } from "../shared.js";
16
15
  import { getActiveTaskForSession } from "../task/lifecycle.js";
17
16
  import { listTaskCheckpoints, writeTaskCheckpoint } from "../session/checkpoints.js";
18
17
  import { markImpactEntriesCompletedForSession } from "../finding/impact.js";
19
- import { atomicWriteJson, debugError, scanSessionFiles } from "../session/utils.js";
18
+ import { atomicWriteJson, debugError, scanSessionFiles, sessionsDir, sessionFileForId, readSessionStateFile, writeSessionStateFile, } from "../session/utils.js";
20
19
  import { getRuntimeHealth } from "../governance/policy.js";
21
20
  import { getProjectSourcePath, readProjectConfig } from "../project-config.js";
22
21
  const STALE_SESSION_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -94,28 +93,6 @@ function extractResumptionHint(summary, fallbackNextStep, fallbackLastAttempt) {
94
93
  /** Per-connection session map keyed by arbitrary connection ID (if provided). */
95
94
  const MAX_SESSION_MAP_ENTRIES = 200;
96
95
  const _sessionMap = new Map();
97
- function sessionsDir(phrenPath) {
98
- const dir = path.join(phrenPath, ".runtime", "sessions");
99
- fs.mkdirSync(dir, { recursive: true });
100
- return dir;
101
- }
102
- function sessionFileForId(phrenPath, sessionId) {
103
- return path.join(sessionsDir(phrenPath), `session-${sessionId}.json`);
104
- }
105
- function readSessionStateFile(file) {
106
- if (!fs.existsSync(file))
107
- return null;
108
- try {
109
- return JSON.parse(fs.readFileSync(file, "utf-8"));
110
- }
111
- catch (err) {
112
- debugError("readSessionStateFile", err);
113
- return null;
114
- }
115
- }
116
- function writeSessionStateFile(file, state) {
117
- atomicWriteJson(file, state);
118
- }
119
96
  /** Find the most recent *active* (not ended) session file by mtime. */
120
97
  function findMostRecentSession(phrenPath) {
121
98
  const dir = sessionsDir(phrenPath);
@@ -157,11 +134,7 @@ function lastSummaryPath(phrenPath) {
157
134
  /** Write the last summary for fast retrieval by next session_start. */
158
135
  function writeLastSummary(phrenPath, summary, sessionId, project) {
159
136
  try {
160
- const data = { summary, sessionId, project, endedAt: new Date().toISOString() };
161
- const summaryFile = lastSummaryPath(phrenPath);
162
- const tmpPath = `${summaryFile}.tmp-${crypto.randomUUID()}`;
163
- fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
164
- fs.renameSync(tmpPath, summaryFile);
137
+ atomicWriteJson(lastSummaryPath(phrenPath), { summary, sessionId, project, endedAt: new Date().toISOString() });
165
138
  }
166
139
  catch (err) {
167
140
  debugError("writeLastSummary", err);
@@ -176,15 +149,15 @@ export function findMostRecentSummary(phrenPath) {
176
149
  function findMostRecentSummaryWithProject(phrenPath) {
177
150
  // Fast path: read from dedicated last-summary file
178
151
  try {
179
- const fastPath = lastSummaryPath(phrenPath);
180
- if (fs.existsSync(fastPath)) {
181
- const data = JSON.parse(fs.readFileSync(fastPath, "utf-8"));
182
- if (data.summary)
183
- return { summary: data.summary, project: data.project, endedAt: data.endedAt };
184
- }
152
+ const data = JSON.parse(fs.readFileSync(lastSummaryPath(phrenPath), "utf-8"));
153
+ if (data.summary)
154
+ return { summary: data.summary, project: data.project, endedAt: data.endedAt };
185
155
  }
186
156
  catch (err) {
187
- debugError("findMostRecentSummaryWithProject fastPath", err);
157
+ // ENOENT is expected when no summary has been written yet
158
+ if (err.code !== "ENOENT") {
159
+ debugError("findMostRecentSummaryWithProject fastPath", err);
160
+ }
188
161
  }
189
162
  // Slow path: scan all session files
190
163
  const dir = sessionsDir(phrenPath);
package/package.json CHANGED
@@ -1,18 +1,36 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.55",
4
- "description": "Knowledge layer for AI agents. Phren learns and recalls.",
3
+ "version": "0.0.57",
4
+ "description": "Knowledge layer for AI agents CLI, MCP server, and data layer",
5
5
  "type": "module",
6
6
  "bin": {
7
- "phren": "mcp/dist/index.js"
7
+ "phren": "dist/index.js"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./paths": "./dist/phren-paths.js",
12
+ "./runtime-profile": "./dist/runtime-profile.js",
13
+ "./shared": "./dist/shared/index.js",
14
+ "./shared/retrieval": "./dist/shared/retrieval.js",
15
+ "./data/access": "./dist/data/access.js",
16
+ "./data/tasks": "./dist/data/tasks.js",
17
+ "./core/finding": "./dist/core/finding.js",
18
+ "./session/utils": "./dist/session/utils.js",
19
+ "./shell/render-api": "./dist/shell/render-api.js"
8
20
  },
9
21
  "files": [
10
- "mcp/dist",
11
- "icon.svg",
22
+ "dist",
12
23
  "starter",
13
24
  "skills",
14
25
  "scripts/preuninstall.mjs"
15
26
  ],
27
+ "scripts": {
28
+ "build": "node ../../scripts/build.mjs",
29
+ "dev": "tsx src/index.ts",
30
+ "lint": "biome lint --config-path ../../biome.json src/",
31
+ "test": "echo 'Run tests from repo root: pnpm -w test'",
32
+ "preuninstall": "node scripts/preuninstall.mjs"
33
+ },
16
34
  "dependencies": {
17
35
  "@modelcontextprotocol/sdk": "^1.28.0",
18
36
  "chalk": "^5.6.2",
@@ -25,33 +43,6 @@
25
43
  "sql.js-fts5": "^1.4.0",
26
44
  "zod": "^4.3.6"
27
45
  },
28
- "devDependencies": {
29
- "@playwright/test": "^1.58.2",
30
- "@types/js-yaml": "^4.0.9",
31
- "@types/node": "^25.5.0",
32
- "@biomejs/biome": "^2.1.0",
33
- "@vitest/coverage-v8": "^4.1.2",
34
- "esbuild": "^0.27.4",
35
- "tsx": "^4.21.0",
36
- "typescript": "^6.0.2",
37
- "vitest": "^4.1.2"
38
- },
39
- "scripts": {
40
- "build": "node scripts/build.mjs",
41
- "dev": "tsx mcp/src/index.ts",
42
- "lint": "biome lint mcp/src/",
43
- "validate-docs": "bash scripts/validate-docs.sh",
44
- "pretest": "npm run build",
45
- "test": "vitest run",
46
- "test:e2e": "npm run build && playwright test",
47
- "test:e2e:install": "playwright install chromium",
48
- "bench": "tsx mcp/bench/locomo-runner.ts --sessions 3",
49
- "bench:retrieval": "tsx scripts/bench-retrieval-modes.ts",
50
- "bench:retrieval:synthetic": "tsx scripts/bench-retrieval-synthetic.ts",
51
- "preuninstall": "node scripts/preuninstall.mjs",
52
- "install-hooks": "cp scripts/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit",
53
- "prepublishOnly": "npm run build && npm test"
54
- },
55
46
  "engines": {
56
47
  "node": ">=20.0.0"
57
48
  },
@@ -66,11 +57,7 @@
66
57
  "license": "MIT",
67
58
  "repository": {
68
59
  "type": "git",
69
- "url": "git+https://github.com/alaarab/phren.git"
70
- },
71
- "homepage": "https://github.com/alaarab/phren#readme",
72
- "overrides": {
73
- "flatted": "^3.4.2",
74
- "undici": "^7.10.0"
60
+ "url": "git+https://github.com/alaarab/phren.git",
61
+ "directory": "packages/cli"
75
62
  }
76
63
  }