@phren/cli 0.0.55 → 0.0.56
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.
- package/README.md +70 -92
- package/{mcp/dist → dist}/cli/cli.js +10 -0
- package/{mcp/dist → dist}/cli/hooks-session.js +47 -0
- package/{mcp/dist → dist}/entrypoint.js +3 -0
- package/{mcp/dist → dist}/package-metadata.js +1 -1
- package/{mcp/dist → dist}/session/utils.js +24 -1
- package/{mcp/dist → dist}/test-global-setup.js +4 -4
- package/{mcp/dist → dist}/tools/session.js +10 -37
- package/package.json +24 -38
- package/LICENSE +0 -21
- package/icon.svg +0 -416
- /package/{mcp/dist → dist}/capabilities/cli.js +0 -0
- /package/{mcp/dist → dist}/capabilities/index.js +0 -0
- /package/{mcp/dist → dist}/capabilities/mcp.js +0 -0
- /package/{mcp/dist → dist}/capabilities/types.js +0 -0
- /package/{mcp/dist → dist}/capabilities/vscode.js +0 -0
- /package/{mcp/dist → dist}/capabilities/web-ui.js +0 -0
- /package/{mcp/dist → dist}/cli/actions.js +0 -0
- /package/{mcp/dist → dist}/cli/config.js +0 -0
- /package/{mcp/dist → dist}/cli/extract.js +0 -0
- /package/{mcp/dist → dist}/cli/govern.js +0 -0
- /package/{mcp/dist → dist}/cli/graph.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-citations.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-context.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-globs.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks-output.js +0 -0
- /package/{mcp/dist → dist}/cli/hooks.js +0 -0
- /package/{mcp/dist → dist}/cli/namespaces.js +0 -0
- /package/{mcp/dist → dist}/cli/ops.js +0 -0
- /package/{mcp/dist → dist}/cli/search.js +0 -0
- /package/{mcp/dist → dist}/cli/team.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-git.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-prompt.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-session-handlers.js +0 -0
- /package/{mcp/dist → dist}/cli-hooks-stop.js +0 -0
- /package/{mcp/dist → dist}/content/archive.js +0 -0
- /package/{mcp/dist → dist}/content/citation.js +0 -0
- /package/{mcp/dist → dist}/content/dedup.js +0 -0
- /package/{mcp/dist → dist}/content/learning.js +0 -0
- /package/{mcp/dist → dist}/content/metadata.js +0 -0
- /package/{mcp/dist → dist}/content/validate.js +0 -0
- /package/{mcp/dist → dist}/core/finding.js +0 -0
- /package/{mcp/dist → dist}/core/project.js +0 -0
- /package/{mcp/dist → dist}/core/search.js +0 -0
- /package/{mcp/dist → dist}/data/access.js +0 -0
- /package/{mcp/dist → dist}/data/tasks.js +0 -0
- /package/{mcp/dist → dist}/embedding.js +0 -0
- /package/{mcp/dist → dist}/finding/context.js +0 -0
- /package/{mcp/dist → dist}/finding/impact.js +0 -0
- /package/{mcp/dist → dist}/finding/journal.js +0 -0
- /package/{mcp/dist → dist}/finding/lifecycle.js +0 -0
- /package/{mcp/dist → dist}/generated/memory-ui-graph.browser.js +0 -0
- /package/{mcp/dist → dist}/governance/audit.js +0 -0
- /package/{mcp/dist → dist}/governance/locks.js +0 -0
- /package/{mcp/dist → dist}/governance/policy.js +0 -0
- /package/{mcp/dist → dist}/governance/rbac.js +0 -0
- /package/{mcp/dist → dist}/governance/scores.js +0 -0
- /package/{mcp/dist → dist}/hooks.js +0 -0
- /package/{mcp/dist → dist}/index-query.js +0 -0
- /package/{mcp/dist → dist}/index.js +0 -0
- /package/{mcp/dist → dist}/init/config.js +0 -0
- /package/{mcp/dist → dist}/init/init-configure.js +0 -0
- /package/{mcp/dist → dist}/init/init-hooks-mode.js +0 -0
- /package/{mcp/dist → dist}/init/init-mcp-mode.js +0 -0
- /package/{mcp/dist → dist}/init/init-uninstall.js +0 -0
- /package/{mcp/dist → dist}/init/init-walkthrough.js +0 -0
- /package/{mcp/dist → dist}/init/init.js +0 -0
- /package/{mcp/dist → dist}/init/preferences.js +0 -0
- /package/{mcp/dist → dist}/init/setup.js +0 -0
- /package/{mcp/dist → dist}/init/shared.js +0 -0
- /package/{mcp/dist → dist}/init-bootstrap.js +0 -0
- /package/{mcp/dist → dist}/init-detect.js +0 -0
- /package/{mcp/dist → dist}/init-env.js +0 -0
- /package/{mcp/dist → dist}/init-fresh.js +0 -0
- /package/{mcp/dist → dist}/init-hooks.js +0 -0
- /package/{mcp/dist → dist}/init-mcp.js +0 -0
- /package/{mcp/dist → dist}/init-modes.js +0 -0
- /package/{mcp/dist → dist}/init-npm.js +0 -0
- /package/{mcp/dist → dist}/init-project-local.js +0 -0
- /package/{mcp/dist → dist}/init-semantic.js +0 -0
- /package/{mcp/dist → dist}/init-types.js +0 -0
- /package/{mcp/dist → dist}/init-uninstall.js +0 -0
- /package/{mcp/dist → dist}/init-update.js +0 -0
- /package/{mcp/dist → dist}/init-walkthrough.js +0 -0
- /package/{mcp/dist → dist}/link/checksums.js +0 -0
- /package/{mcp/dist → dist}/link/context.js +0 -0
- /package/{mcp/dist → dist}/link/doctor.js +0 -0
- /package/{mcp/dist → dist}/link/link.js +0 -0
- /package/{mcp/dist → dist}/link/skills.js +0 -0
- /package/{mcp/dist → dist}/logger.js +0 -0
- /package/{mcp/dist → dist}/machine-identity.js +0 -0
- /package/{mcp/dist → dist}/memory-ui-graph.runtime.js +0 -0
- /package/{mcp/dist → dist}/phren-art.js +0 -0
- /package/{mcp/dist → dist}/phren-core.js +0 -0
- /package/{mcp/dist → dist}/phren-dotenv.js +0 -0
- /package/{mcp/dist → dist}/phren-paths.js +0 -0
- /package/{mcp/dist → dist}/proactivity.js +0 -0
- /package/{mcp/dist → dist}/profile-store.js +0 -0
- /package/{mcp/dist → dist}/project-config.js +0 -0
- /package/{mcp/dist → dist}/project-locator.js +0 -0
- /package/{mcp/dist → dist}/project-topics.js +0 -0
- /package/{mcp/dist → dist}/provider-adapters.js +0 -0
- /package/{mcp/dist → dist}/query-correlation.js +0 -0
- /package/{mcp/dist → dist}/runtime-profile.js +0 -0
- /package/{mcp/dist → dist}/session/checkpoints.js +0 -0
- /package/{mcp/dist → dist}/shared/content.js +0 -0
- /package/{mcp/dist → dist}/shared/data-utils.js +0 -0
- /package/{mcp/dist → dist}/shared/embedding-cache.js +0 -0
- /package/{mcp/dist → dist}/shared/fragment-graph.js +0 -0
- /package/{mcp/dist → dist}/shared/governance.js +0 -0
- /package/{mcp/dist → dist}/shared/index.js +0 -0
- /package/{mcp/dist → dist}/shared/ollama.js +0 -0
- /package/{mcp/dist → dist}/shared/process.js +0 -0
- /package/{mcp/dist → dist}/shared/retrieval.js +0 -0
- /package/{mcp/dist → dist}/shared/search-fallback.js +0 -0
- /package/{mcp/dist → dist}/shared/sqljs.js +0 -0
- /package/{mcp/dist → dist}/shared/stemmer.js +0 -0
- /package/{mcp/dist → dist}/shared/vector-index.js +0 -0
- /package/{mcp/dist → dist}/shared.js +0 -0
- /package/{mcp/dist → dist}/shell/entry.js +0 -0
- /package/{mcp/dist → dist}/shell/input.js +0 -0
- /package/{mcp/dist → dist}/shell/palette.js +0 -0
- /package/{mcp/dist → dist}/shell/render.js +0 -0
- /package/{mcp/dist → dist}/shell/shell.js +0 -0
- /package/{mcp/dist → dist}/shell/state-store.js +0 -0
- /package/{mcp/dist → dist}/shell/types.js +0 -0
- /package/{mcp/dist → dist}/shell/view-list.js +0 -0
- /package/{mcp/dist → dist}/shell/view.js +0 -0
- /package/{mcp/dist → dist}/skill/files.js +0 -0
- /package/{mcp/dist → dist}/skill/registry.js +0 -0
- /package/{mcp/dist → dist}/skill/state.js +0 -0
- /package/{mcp/dist → dist}/startup-embedding.js +0 -0
- /package/{mcp/dist → dist}/status.js +0 -0
- /package/{mcp/dist → dist}/store-registry.js +0 -0
- /package/{mcp/dist → dist}/store-routing.js +0 -0
- /package/{mcp/dist → dist}/synonyms.json +0 -0
- /package/{mcp/dist → dist}/task/github.js +0 -0
- /package/{mcp/dist → dist}/task/hygiene.js +0 -0
- /package/{mcp/dist → dist}/task/lifecycle.js +0 -0
- /package/{mcp/dist → dist}/telemetry.js +0 -0
- /package/{mcp/dist → dist}/tool-registry.js +0 -0
- /package/{mcp/dist → dist}/tools/config.js +0 -0
- /package/{mcp/dist → dist}/tools/data.js +0 -0
- /package/{mcp/dist → dist}/tools/extract-facts.js +0 -0
- /package/{mcp/dist → dist}/tools/extract.js +0 -0
- /package/{mcp/dist → dist}/tools/finding.js +0 -0
- /package/{mcp/dist → dist}/tools/graph.js +0 -0
- /package/{mcp/dist → dist}/tools/hooks.js +0 -0
- /package/{mcp/dist → dist}/tools/memory.js +0 -0
- /package/{mcp/dist → dist}/tools/ops.js +0 -0
- /package/{mcp/dist → dist}/tools/search.js +0 -0
- /package/{mcp/dist → dist}/tools/skills.js +0 -0
- /package/{mcp/dist → dist}/tools/tasks.js +0 -0
- /package/{mcp/dist → dist}/tools/types.js +0 -0
- /package/{mcp/dist → dist}/ui/assets.js +0 -0
- /package/{mcp/dist → dist}/ui/data.js +0 -0
- /package/{mcp/dist → dist}/ui/graph.js +0 -0
- /package/{mcp/dist → dist}/ui/memory-ui.js +0 -0
- /package/{mcp/dist → dist}/ui/page.js +0 -0
- /package/{mcp/dist → dist}/ui/scripts.js +0 -0
- /package/{mcp/dist → dist}/ui/server.js +0 -0
- /package/{mcp/dist → dist}/ui/styles.js +0 -0
- /package/{mcp/dist → dist}/update.js +0 -0
- /package/{mcp/dist → dist}/utils.js +0 -0
package/README.md
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
# phren MCP server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server that indexes your personal phren and exposes it to AI agents via full-text search.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
45
|
+
See [docs/api-reference.md](../docs/api-reference.md) for the full API reference.
|
|
53
46
|
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
-
|
|
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
|
-
|
|
52
|
+
## Governance and security highlights
|
|
63
53
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
+
```
|
|
79
|
+
name: string - Project name (e.g. "my-app", "backend")
|
|
80
|
+
```
|
|
111
81
|
|
|
112
|
-
|
|
82
|
+
### list_projects
|
|
113
83
|
|
|
114
|
-
|
|
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
|
-
##
|
|
88
|
+
## How it works
|
|
119
89
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
42
|
+
if (process.env.PHREN_DEBUG) {
|
|
20
43
|
process.stderr.write(`[phren] ${scope}: ${errorMessage(err)}\n`);
|
|
21
44
|
}
|
|
22
45
|
}
|
|
@@ -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, "
|
|
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]
|
|
30
|
-
execFileSync("
|
|
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
|
-
|
|
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
|
|
180
|
-
if (
|
|
181
|
-
|
|
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
|
-
|
|
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,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phren/cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Knowledge layer for AI agents
|
|
3
|
+
"version": "0.0.56",
|
|
4
|
+
"description": "Knowledge layer for AI agents — CLI, MCP server, and data layer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"phren": "
|
|
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"
|
|
8
19
|
},
|
|
9
20
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"icon.svg",
|
|
21
|
+
"dist",
|
|
12
22
|
"starter",
|
|
13
23
|
"skills",
|
|
14
24
|
"scripts/preuninstall.mjs"
|
|
15
25
|
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node ../../scripts/build.mjs",
|
|
28
|
+
"dev": "tsx src/index.ts",
|
|
29
|
+
"lint": "biome lint --config-path ../../biome.json src/",
|
|
30
|
+
"test": "echo 'Run tests from repo root: pnpm -w test'",
|
|
31
|
+
"preuninstall": "node scripts/preuninstall.mjs"
|
|
32
|
+
},
|
|
16
33
|
"dependencies": {
|
|
17
34
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
18
35
|
"chalk": "^5.6.2",
|
|
@@ -25,33 +42,6 @@
|
|
|
25
42
|
"sql.js-fts5": "^1.4.0",
|
|
26
43
|
"zod": "^4.3.6"
|
|
27
44
|
},
|
|
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
45
|
"engines": {
|
|
56
46
|
"node": ">=20.0.0"
|
|
57
47
|
},
|
|
@@ -66,11 +56,7 @@
|
|
|
66
56
|
"license": "MIT",
|
|
67
57
|
"repository": {
|
|
68
58
|
"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"
|
|
59
|
+
"url": "git+https://github.com/alaarab/phren.git",
|
|
60
|
+
"directory": "packages/cli"
|
|
75
61
|
}
|
|
76
62
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Ala Arab
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|