@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.
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- package/starter/templates/python-project/tasks.md +10 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as crypto from "crypto";
|
|
3
|
+
import { runtimeFile, debugLog } from "./shared.js";
|
|
4
|
+
import { withFileLock } from "./shared-governance.js";
|
|
5
|
+
import { errorMessage } from "./utils.js";
|
|
6
|
+
const VECTOR_INDEX_VERSION = 1;
|
|
7
|
+
const VECTOR_INDEX_TABLE_COUNT = 4;
|
|
8
|
+
const VECTOR_INDEX_BITS_PER_TABLE = 12;
|
|
9
|
+
const VECTOR_INDEX_TARGET_MULTIPLIER = 8;
|
|
10
|
+
const VECTOR_INDEX_MIN_CANDIDATES = 24;
|
|
11
|
+
const VECTOR_INDEX_FALLBACK_CAP = 64;
|
|
12
|
+
function embeddingsFilePath(phrenPath) {
|
|
13
|
+
return runtimeFile(phrenPath, "embeddings.json");
|
|
14
|
+
}
|
|
15
|
+
function vectorIndexPath(phrenPath) {
|
|
16
|
+
return runtimeFile(phrenPath, "embedding-index.json");
|
|
17
|
+
}
|
|
18
|
+
function readSourceMarker(phrenPath) {
|
|
19
|
+
try {
|
|
20
|
+
const stat = fs.statSync(embeddingsFilePath(phrenPath));
|
|
21
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function markersMatch(a, b) {
|
|
28
|
+
if (!a && !b)
|
|
29
|
+
return true;
|
|
30
|
+
if (!a || !b)
|
|
31
|
+
return false;
|
|
32
|
+
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
33
|
+
}
|
|
34
|
+
function stepSeed(seed) {
|
|
35
|
+
return (Math.imul(seed, 1664525) + 1013904223) >>> 0;
|
|
36
|
+
}
|
|
37
|
+
function pairSeed(model, table, bit) {
|
|
38
|
+
const raw = crypto.createHash("sha256").update(`${model}:${table}:${bit}`).digest();
|
|
39
|
+
return raw.readUInt32LE(0);
|
|
40
|
+
}
|
|
41
|
+
function dimensionPair(dims, model, table, bit) {
|
|
42
|
+
if (dims <= 1)
|
|
43
|
+
return [0, 0];
|
|
44
|
+
let seed = pairSeed(model, table, bit);
|
|
45
|
+
const a = seed % dims;
|
|
46
|
+
seed = stepSeed(seed);
|
|
47
|
+
let b = seed % dims;
|
|
48
|
+
if (b === a)
|
|
49
|
+
b = (b + 1) % dims;
|
|
50
|
+
return [a, b];
|
|
51
|
+
}
|
|
52
|
+
function bucketKey(vec, model, table) {
|
|
53
|
+
const dims = vec.length;
|
|
54
|
+
if (dims === 0)
|
|
55
|
+
return "";
|
|
56
|
+
let bits = "";
|
|
57
|
+
for (let bit = 0; bit < VECTOR_INDEX_BITS_PER_TABLE; bit++) {
|
|
58
|
+
const [a, b] = dimensionPair(dims, model, table, bit);
|
|
59
|
+
bits += (vec[a] ?? 0) >= (vec[b] ?? 0) ? "1" : "0";
|
|
60
|
+
}
|
|
61
|
+
return bits;
|
|
62
|
+
}
|
|
63
|
+
function oneBitNeighbors(key) {
|
|
64
|
+
const neighbors = [];
|
|
65
|
+
for (let i = 0; i < key.length; i++) {
|
|
66
|
+
neighbors.push(`${key.slice(0, i)}${key[i] === "1" ? "0" : "1"}${key.slice(i + 1)}`);
|
|
67
|
+
}
|
|
68
|
+
return neighbors;
|
|
69
|
+
}
|
|
70
|
+
function buildVectorIndexData(entries) {
|
|
71
|
+
const byModel = new Map();
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.model || entry.vec.length === 0)
|
|
74
|
+
continue;
|
|
75
|
+
const arr = byModel.get(entry.model) ?? [];
|
|
76
|
+
arr.push(entry);
|
|
77
|
+
byModel.set(entry.model, arr);
|
|
78
|
+
}
|
|
79
|
+
const models = {};
|
|
80
|
+
for (const [model, modelEntries] of byModel.entries()) {
|
|
81
|
+
const dims = modelEntries[0]?.vec.length ?? 0;
|
|
82
|
+
const tables = Array.from({ length: VECTOR_INDEX_TABLE_COUNT }, () => ({}));
|
|
83
|
+
const allPaths = [];
|
|
84
|
+
for (const entry of modelEntries) {
|
|
85
|
+
allPaths.push(entry.path);
|
|
86
|
+
for (let table = 0; table < VECTOR_INDEX_TABLE_COUNT; table++) {
|
|
87
|
+
const key = bucketKey(entry.vec, model, table);
|
|
88
|
+
const bucket = tables[table][key] ?? [];
|
|
89
|
+
bucket.push(entry.path);
|
|
90
|
+
tables[table][key] = bucket;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
models[model] = { dims, allPaths, tables };
|
|
94
|
+
}
|
|
95
|
+
return models;
|
|
96
|
+
}
|
|
97
|
+
class PersistentVectorIndex {
|
|
98
|
+
phrenPath;
|
|
99
|
+
loaded = false;
|
|
100
|
+
source = null;
|
|
101
|
+
models = {};
|
|
102
|
+
constructor(phrenPath) {
|
|
103
|
+
this.phrenPath = phrenPath;
|
|
104
|
+
}
|
|
105
|
+
loadFromDisk() {
|
|
106
|
+
if (this.loaded)
|
|
107
|
+
return;
|
|
108
|
+
this.loaded = true;
|
|
109
|
+
const filePath = vectorIndexPath(this.phrenPath);
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
112
|
+
if (parsed?.version !== VECTOR_INDEX_VERSION || !parsed.models || typeof parsed.models !== "object")
|
|
113
|
+
return;
|
|
114
|
+
this.source = parsed.source ?? null;
|
|
115
|
+
this.models = parsed.models;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const code = err instanceof Error && "code" in err ? String(err.code ?? "") : "";
|
|
119
|
+
if (code !== "ENOENT")
|
|
120
|
+
debugLog(`PersistentVectorIndex load failed: ${errorMessage(err)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
saveToDisk() {
|
|
124
|
+
const filePath = vectorIndexPath(this.phrenPath);
|
|
125
|
+
try {
|
|
126
|
+
withFileLock(filePath, () => {
|
|
127
|
+
const tmp = `${filePath}.tmp-${crypto.randomUUID()}`;
|
|
128
|
+
const payload = {
|
|
129
|
+
version: VECTOR_INDEX_VERSION,
|
|
130
|
+
source: this.source,
|
|
131
|
+
models: this.models,
|
|
132
|
+
};
|
|
133
|
+
fs.writeFileSync(tmp, JSON.stringify(payload));
|
|
134
|
+
fs.renameSync(tmp, filePath);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
debugLog(`PersistentVectorIndex save failed: ${errorMessage(err)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
ensure(entries) {
|
|
142
|
+
this.loadFromDisk();
|
|
143
|
+
const currentSource = readSourceMarker(this.phrenPath);
|
|
144
|
+
if (markersMatch(this.source, currentSource) && Object.keys(this.models).length > 0)
|
|
145
|
+
return;
|
|
146
|
+
this.models = buildVectorIndexData(entries);
|
|
147
|
+
this.source = currentSource;
|
|
148
|
+
this.saveToDisk();
|
|
149
|
+
}
|
|
150
|
+
query(model, queryVec, limit, eligiblePaths) {
|
|
151
|
+
if (queryVec.length === 0)
|
|
152
|
+
return [];
|
|
153
|
+
const modelIndex = this.models[model];
|
|
154
|
+
if (!modelIndex || modelIndex.dims === 0)
|
|
155
|
+
return [];
|
|
156
|
+
const target = Math.max(VECTOR_INDEX_MIN_CANDIDATES, limit * VECTOR_INDEX_TARGET_MULTIPLIER);
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
const keys = Array.from({ length: VECTOR_INDEX_TABLE_COUNT }, (_, table) => bucketKey(queryVec, model, table));
|
|
159
|
+
const addBucket = (table, key) => {
|
|
160
|
+
const bucket = modelIndex.tables[table]?.[key] ?? [];
|
|
161
|
+
for (const entryPath of bucket) {
|
|
162
|
+
if (eligiblePaths && !eligiblePaths.has(entryPath))
|
|
163
|
+
continue;
|
|
164
|
+
seen.add(entryPath);
|
|
165
|
+
if (seen.size >= target)
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
for (let table = 0; table < keys.length && seen.size < target; table++) {
|
|
170
|
+
addBucket(table, keys[table]);
|
|
171
|
+
}
|
|
172
|
+
for (let table = 0; table < keys.length && seen.size < target; table++) {
|
|
173
|
+
for (const neighbor of oneBitNeighbors(keys[table])) {
|
|
174
|
+
addBucket(table, neighbor);
|
|
175
|
+
if (seen.size >= target)
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (seen.size === 0) {
|
|
180
|
+
for (const entryPath of modelIndex.allPaths) {
|
|
181
|
+
if (eligiblePaths && !eligiblePaths.has(entryPath))
|
|
182
|
+
continue;
|
|
183
|
+
seen.add(entryPath);
|
|
184
|
+
if (seen.size >= VECTOR_INDEX_FALLBACK_CAP)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [...seen];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const vectorIndexInstances = new Map();
|
|
192
|
+
export function getPersistentVectorIndex(phrenPath) {
|
|
193
|
+
const existing = vectorIndexInstances.get(phrenPath);
|
|
194
|
+
if (existing)
|
|
195
|
+
return existing;
|
|
196
|
+
const created = new PersistentVectorIndex(phrenPath);
|
|
197
|
+
vectorIndexInstances.set(phrenPath, created);
|
|
198
|
+
return created;
|
|
199
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { debugLog, runtimeFile } from "./phren-paths.js";
|
|
4
|
+
import { errorMessage, isValidProjectName } from "./utils.js";
|
|
5
|
+
export { HOOK_TOOL_NAMES, hookConfigPath } from "./provider-adapters.js";
|
|
6
|
+
export { EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, PhrenError, phrenOk, phrenErr, forwardErr, parsePhrenErrorCode, isRecord, withDefaults, FINDING_TYPES, FINDING_TAGS, KNOWN_OBSERVATION_TAGS, DOC_TYPES, capCache, } from "./phren-core.js";
|
|
7
|
+
export { ROOT_MANIFEST_FILENAME, homeDir, homePath, expandHomePath, defaultPhrenPath, rootManifestPath, readRootManifest, writeRootManifest, resolveInstallContext, findNearestPhrenPath, isProjectLocalMode, runtimeDir, tryUnlink, sessionsDir, runtimeFile, installPreferencesFile, runtimeHealthFile, shellStateFile, sessionMetricsFile, memoryScoresFile, memoryUsageLogFile, sessionMarker, debugLog, appendIndexEvent, resolveFindingsPath, findPhrenPath, ensurePhrenPath, findPhrenPathWithArg, normalizeProjectNameForCreate, findProjectNameCaseInsensitive, getProjectDirs, collectNativeMemoryFiles, computePhrenLiveStateToken, getPhrenPath, qualityMarkers, atomicWriteText, } from "./phren-paths.js";
|
|
8
|
+
export { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, hasExplicitFindingSignal, hasExplicitTaskSignal, hasExecutionIntent, hasDiscoveryIntent, shouldAutoCaptureFindingsForLevel, shouldAutoCaptureTaskForLevel, } from "./proactivity.js";
|
|
9
|
+
const RESERVED_PROJECT_DIR_NAMES = new Set(["profiles", "templates", "global"]);
|
|
10
|
+
const MEMORY_SCOPE_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
11
|
+
export function normalizeMemoryScope(scope) {
|
|
12
|
+
if (typeof scope !== "string")
|
|
13
|
+
return undefined;
|
|
14
|
+
const normalized = scope.trim().toLowerCase();
|
|
15
|
+
if (!normalized)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (!MEMORY_SCOPE_PATTERN.test(normalized))
|
|
18
|
+
return undefined;
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
export function isMemoryScopeVisible(itemScope, activeScope) {
|
|
22
|
+
if (!activeScope)
|
|
23
|
+
return true;
|
|
24
|
+
if (!itemScope)
|
|
25
|
+
return true; // Untagged legacy entries are visible to all scoped agents.
|
|
26
|
+
return itemScope === "shared" || itemScope === activeScope;
|
|
27
|
+
}
|
|
28
|
+
export function impactLogFile(phrenPath) {
|
|
29
|
+
return runtimeFile(phrenPath, "impact.jsonl");
|
|
30
|
+
}
|
|
31
|
+
function isProjectDirEntry(entry) {
|
|
32
|
+
return entry.isDirectory()
|
|
33
|
+
&& !entry.name.startsWith(".")
|
|
34
|
+
&& !entry.name.endsWith(".archived")
|
|
35
|
+
&& !RESERVED_PROJECT_DIR_NAMES.has(entry.name);
|
|
36
|
+
}
|
|
37
|
+
function isCanonicalProjectDirName(name) {
|
|
38
|
+
return name === name.toLowerCase() && isValidProjectName(name);
|
|
39
|
+
}
|
|
40
|
+
export function appendAuditLog(phrenPath, event, details) {
|
|
41
|
+
const logPath = runtimeFile(phrenPath, "audit.log");
|
|
42
|
+
const line = `[${new Date().toISOString()}] ${event} ${details}\n`;
|
|
43
|
+
const lockPath = logPath + ".lock";
|
|
44
|
+
const maxWait = 5000;
|
|
45
|
+
const pollMs = 50;
|
|
46
|
+
const staleMs = 30_000;
|
|
47
|
+
const waiter = new Int32Array(new SharedArrayBuffer(4));
|
|
48
|
+
// Q82: use an inline lock (same protocol as withFileLock) to guard the
|
|
49
|
+
// append + conditional rotation so concurrent processes don't read the same
|
|
50
|
+
// old content and race to write a truncated version each.
|
|
51
|
+
let waited = 0;
|
|
52
|
+
let hasLock = false;
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
55
|
+
while (waited < maxWait) {
|
|
56
|
+
try {
|
|
57
|
+
fs.writeFileSync(lockPath, `${process.pid}\n${Date.now()}`, { flag: "wx" });
|
|
58
|
+
hasLock = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
63
|
+
process.stderr.write(`[phren] appendAuditLog lockWrite: ${errorMessage(err)}\n`);
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.statSync(lockPath);
|
|
66
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
67
|
+
try {
|
|
68
|
+
fs.unlinkSync(lockPath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Another process may have claimed or removed the stale lock between
|
|
72
|
+
// statSync and unlinkSync. Sleep before retrying to avoid a spin loop.
|
|
73
|
+
Atomics.wait(waiter, 0, 0, pollMs);
|
|
74
|
+
waited += pollMs;
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (statErr) {
|
|
80
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
81
|
+
process.stderr.write(`[phren] appendAuditLog staleStat: ${errorMessage(statErr)}\n`);
|
|
82
|
+
}
|
|
83
|
+
Atomics.wait(waiter, 0, 0, pollMs);
|
|
84
|
+
waited += pollMs;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (hasLock) {
|
|
88
|
+
fs.appendFileSync(logPath, line);
|
|
89
|
+
const stat = fs.statSync(logPath);
|
|
90
|
+
if (stat.size > 1_000_000) {
|
|
91
|
+
const content = fs.readFileSync(logPath, "utf8");
|
|
92
|
+
const lines = content.split("\n");
|
|
93
|
+
fs.writeFileSync(logPath, lines.slice(-500).join("\n"));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
debugLog(`Audit log skipped (lock timeout): ${event} ${details}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
debugLog(`Audit log write failed: ${errorMessage(err)}`);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
if (hasLock) {
|
|
105
|
+
try {
|
|
106
|
+
fs.unlinkSync(lockPath);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
110
|
+
process.stderr.write(`[phren] appendAuditLog unlock: ${errorMessage(err)}\n`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell entry point: wires PhrenShell to stdin/stdout.
|
|
3
|
+
* Extracted from shell.ts to keep the orchestrator under 300 lines.
|
|
4
|
+
*/
|
|
5
|
+
import { PhrenShell } from "./shell.js";
|
|
6
|
+
import { style, clearScreen, clearToEnd, shellStartupFrames } from "./shell-render.js";
|
|
7
|
+
import { errorMessage } from "./utils.js";
|
|
8
|
+
import { computePhrenLiveStateToken } from "./shared.js";
|
|
9
|
+
import { VERSION } from "./init-shared.js";
|
|
10
|
+
import { loadShellState, saveShellState } from "./shell-state-store.js";
|
|
11
|
+
const LIVE_STATE_POLL_MS = 2000;
|
|
12
|
+
function renderIntroFrame(frame, footer) {
|
|
13
|
+
clearScreen();
|
|
14
|
+
process.stdout.write(footer ? `${frame}\n${footer}\n` : `${frame}\n`);
|
|
15
|
+
clearToEnd();
|
|
16
|
+
}
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
async function waitForAnyKeypress() {
|
|
21
|
+
await new Promise((resolve) => {
|
|
22
|
+
const onData = () => {
|
|
23
|
+
process.stdin.removeListener("data", onData);
|
|
24
|
+
resolve();
|
|
25
|
+
};
|
|
26
|
+
process.stdin.on("data", onData);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function resolveStartupIntroPlan(phrenPath, version = VERSION) {
|
|
30
|
+
const state = loadShellState(phrenPath);
|
|
31
|
+
const mode = state.introMode === "always" || state.introMode === "off" ? state.introMode : "once-per-version";
|
|
32
|
+
if (mode === "off") {
|
|
33
|
+
return { mode, variant: "skip", holdForKeypress: false, dwellMs: 0, markSeen: false };
|
|
34
|
+
}
|
|
35
|
+
if (mode === "always") {
|
|
36
|
+
return { mode, variant: "full", holdForKeypress: false, dwellMs: 700, markSeen: true };
|
|
37
|
+
}
|
|
38
|
+
if (state.introSeenVersion !== version) {
|
|
39
|
+
return { mode, variant: "full", holdForKeypress: true, dwellMs: 0, markSeen: true };
|
|
40
|
+
}
|
|
41
|
+
return { mode, variant: "final-frame", holdForKeypress: false, dwellMs: 550, markSeen: false };
|
|
42
|
+
}
|
|
43
|
+
function markStartupIntroSeen(phrenPath, version = VERSION) {
|
|
44
|
+
const state = loadShellState(phrenPath);
|
|
45
|
+
if (state.introSeenVersion === version)
|
|
46
|
+
return;
|
|
47
|
+
saveShellState(phrenPath, { ...state, introSeenVersion: version });
|
|
48
|
+
}
|
|
49
|
+
async function playStartupIntro(phrenPath, plan = resolveStartupIntroPlan(phrenPath)) {
|
|
50
|
+
if (!process.stdout.isTTY || plan.variant === "skip")
|
|
51
|
+
return;
|
|
52
|
+
const frames = shellStartupFrames(VERSION);
|
|
53
|
+
const renderHint = plan.holdForKeypress
|
|
54
|
+
? `${style.dim("Press any key to enter")}`
|
|
55
|
+
: `${style.dim("Loading shell…")}`;
|
|
56
|
+
if (plan.variant === "full") {
|
|
57
|
+
for (const frame of frames.slice(0, -1)) {
|
|
58
|
+
renderIntroFrame(frame);
|
|
59
|
+
await sleep(160);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
renderIntroFrame(frames[frames.length - 1], renderHint);
|
|
63
|
+
if (plan.holdForKeypress) {
|
|
64
|
+
await waitForAnyKeypress();
|
|
65
|
+
}
|
|
66
|
+
else if (plan.dwellMs > 0) {
|
|
67
|
+
await sleep(plan.dwellMs);
|
|
68
|
+
}
|
|
69
|
+
if (plan.markSeen) {
|
|
70
|
+
markStartupIntroSeen(phrenPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function startLiveStatePoller({ phrenPath, shell, repaint, isExiting = () => false, intervalMs = LIVE_STATE_POLL_MS, computeToken = computePhrenLiveStateToken, }) {
|
|
74
|
+
let liveStateToken = computeToken(phrenPath);
|
|
75
|
+
let stopped = false;
|
|
76
|
+
let inFlight = false;
|
|
77
|
+
const pollOnce = async () => {
|
|
78
|
+
if (stopped || inFlight || isExiting())
|
|
79
|
+
return;
|
|
80
|
+
inFlight = true;
|
|
81
|
+
try {
|
|
82
|
+
const nextToken = computeToken(phrenPath);
|
|
83
|
+
if (nextToken === liveStateToken)
|
|
84
|
+
return;
|
|
85
|
+
liveStateToken = nextToken;
|
|
86
|
+
shell.invalidateSubsectionsCache();
|
|
87
|
+
shell.setMessage(` ${style.boldCyan("Live")} ${style.dim("store updated")}`);
|
|
88
|
+
await repaint();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
inFlight = false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const poll = setInterval(() => {
|
|
95
|
+
void pollOnce();
|
|
96
|
+
}, intervalMs);
|
|
97
|
+
poll.unref?.();
|
|
98
|
+
return () => {
|
|
99
|
+
stopped = true;
|
|
100
|
+
clearInterval(poll);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function startShell(phrenPath, profile) {
|
|
104
|
+
const shell = new PhrenShell(phrenPath, profile);
|
|
105
|
+
if (!process.stdin.isTTY) {
|
|
106
|
+
const { createInterface } = await import("readline");
|
|
107
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
108
|
+
const repaint = async () => { clearScreen(); process.stdout.write(await shell.render()); rl.setPrompt(`\n${style.boldCyan(":phren>")} `); rl.prompt(); };
|
|
109
|
+
const stopPoll = startLiveStatePoller({ phrenPath, shell, repaint });
|
|
110
|
+
await repaint();
|
|
111
|
+
rl.on("line", async (line) => {
|
|
112
|
+
try {
|
|
113
|
+
const keep = await shell.handleInput(line);
|
|
114
|
+
if (!keep) {
|
|
115
|
+
shell.close();
|
|
116
|
+
rl.close();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
process.stdout.write(`\n${style.red("Error:")} ${String(errorMessage(err))}\n`);
|
|
122
|
+
}
|
|
123
|
+
await repaint();
|
|
124
|
+
});
|
|
125
|
+
rl.on("SIGINT", () => { stopPoll(); shell.close(); rl.close(); });
|
|
126
|
+
rl.on("close", () => { stopPoll(); });
|
|
127
|
+
await new Promise((resolve) => { rl.on("close", () => { shell.close(); resolve(); }); });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
process.stdin.setRawMode(true);
|
|
131
|
+
process.stdin.resume();
|
|
132
|
+
process.stdin.setEncoding("utf8");
|
|
133
|
+
process.stdout.write("\x1b[?1049h");
|
|
134
|
+
let exiting = false;
|
|
135
|
+
let cleanedUp = false;
|
|
136
|
+
const repaint = async () => { clearScreen(); process.stdout.write(await shell.render()); clearToEnd(); };
|
|
137
|
+
let done;
|
|
138
|
+
const exitPromise = new Promise((resolve) => { done = resolve; });
|
|
139
|
+
const restoreTerminal = () => {
|
|
140
|
+
// Shutdown cleanup is intentionally silent: terminal restoration is best-effort
|
|
141
|
+
// cleanup, not a user-requested write path.
|
|
142
|
+
try {
|
|
143
|
+
process.stdin.setRawMode(false);
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
try {
|
|
147
|
+
process.stdin.pause();
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
try {
|
|
151
|
+
process.stdout.write("\x1b[?1049l");
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
};
|
|
155
|
+
const onData = async (key) => {
|
|
156
|
+
if (exiting)
|
|
157
|
+
return;
|
|
158
|
+
try {
|
|
159
|
+
const keep = await shell.handleRawKey(key);
|
|
160
|
+
if (!keep) {
|
|
161
|
+
exiting = true;
|
|
162
|
+
finish();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
await repaint();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
shell.setMessage(`Error: ${errorMessage(err)}`);
|
|
169
|
+
await repaint();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const onResize = async () => { if (!exiting)
|
|
173
|
+
await repaint(); };
|
|
174
|
+
const onSignal = () => {
|
|
175
|
+
if (exiting)
|
|
176
|
+
return;
|
|
177
|
+
exiting = true;
|
|
178
|
+
finish();
|
|
179
|
+
};
|
|
180
|
+
const onProcessExit = () => { restoreTerminal(); };
|
|
181
|
+
const stopPoll = startLiveStatePoller({ phrenPath, shell, repaint, isExiting: () => exiting });
|
|
182
|
+
const finish = () => {
|
|
183
|
+
if (cleanedUp)
|
|
184
|
+
return;
|
|
185
|
+
cleanedUp = true;
|
|
186
|
+
stopPoll();
|
|
187
|
+
process.stdin.removeListener("data", onData);
|
|
188
|
+
process.stdout.removeListener("resize", onResize);
|
|
189
|
+
process.removeListener("SIGINT", onSignal);
|
|
190
|
+
process.removeListener("SIGTERM", onSignal);
|
|
191
|
+
process.removeListener("exit", onProcessExit);
|
|
192
|
+
restoreTerminal();
|
|
193
|
+
shell.close();
|
|
194
|
+
done();
|
|
195
|
+
};
|
|
196
|
+
process.stdin.on("data", onData);
|
|
197
|
+
process.stdout.on("resize", onResize);
|
|
198
|
+
process.once("SIGINT", onSignal);
|
|
199
|
+
process.once("SIGTERM", onSignal);
|
|
200
|
+
process.once("exit", onProcessExit);
|
|
201
|
+
try {
|
|
202
|
+
await playStartupIntro(phrenPath);
|
|
203
|
+
await repaint();
|
|
204
|
+
await exitPromise;
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
finish();
|
|
208
|
+
}
|
|
209
|
+
}
|