@phren/cli 0.0.11 → 0.0.12
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 +9 -9
- package/mcp/dist/capabilities/cli.js +1 -1
- package/mcp/dist/capabilities/mcp.js +1 -1
- package/mcp/dist/capabilities/vscode.js +1 -1
- package/mcp/dist/capabilities/web-ui.js +1 -1
- package/mcp/dist/cli-actions.js +54 -67
- package/mcp/dist/cli-config.js +4 -5
- package/mcp/dist/cli-extract.js +3 -2
- package/mcp/dist/cli-graph.js +17 -3
- package/mcp/dist/cli-hooks-output.js +1 -1
- package/mcp/dist/cli-hooks-session.js +1 -1
- package/mcp/dist/cli-hooks.js +5 -3
- package/mcp/dist/cli.js +1 -1
- package/mcp/dist/content-archive.js +21 -12
- package/mcp/dist/content-citation.js +13 -2
- package/mcp/dist/content-learning.js +6 -4
- package/mcp/dist/content-metadata.js +10 -0
- package/mcp/dist/core-finding.js +1 -1
- package/mcp/dist/data-access.js +10 -31
- package/mcp/dist/data-tasks.js +5 -26
- package/mcp/dist/embedding.js +0 -1
- package/mcp/dist/entrypoint.js +4 -0
- package/mcp/dist/finding-impact.js +1 -32
- package/mcp/dist/finding-journal.js +1 -1
- package/mcp/dist/finding-lifecycle.js +2 -7
- package/mcp/dist/governance-locks.js +6 -0
- package/mcp/dist/governance-policy.js +1 -7
- package/mcp/dist/governance-scores.js +1 -7
- package/mcp/dist/hooks.js +23 -0
- package/mcp/dist/init-config.js +1 -1
- package/mcp/dist/init-preferences.js +1 -1
- package/mcp/dist/init-setup.js +1 -50
- package/mcp/dist/init-shared.js +53 -1
- package/mcp/dist/init.js +21 -6
- package/mcp/dist/link-context.js +1 -1
- package/mcp/dist/link-doctor.js +11 -54
- package/mcp/dist/link.js +4 -53
- package/mcp/dist/mcp-extract-facts.js +11 -6
- package/mcp/dist/mcp-finding.js +10 -14
- package/mcp/dist/mcp-graph.js +6 -6
- package/mcp/dist/mcp-hooks.js +1 -1
- package/mcp/dist/mcp-search.js +3 -8
- package/mcp/dist/mcp-session.js +12 -2
- package/mcp/dist/memory-ui-assets.js +1 -36
- package/mcp/dist/memory-ui-graph.js +152 -50
- package/mcp/dist/memory-ui-page.js +7 -5
- package/mcp/dist/memory-ui-scripts.js +42 -36
- package/mcp/dist/phren-core.js +2 -0
- package/mcp/dist/phren-paths.js +1 -2
- package/mcp/dist/proactivity.js +5 -5
- package/mcp/dist/project-config.js +1 -1
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/query-correlation.js +22 -19
- package/mcp/dist/session-checkpoints.js +14 -14
- package/mcp/dist/shared-data-utils.js +28 -0
- package/mcp/dist/shared-fragment-graph.js +11 -11
- package/mcp/dist/shared-governance.js +1 -1
- package/mcp/dist/shared-retrieval.js +2 -10
- package/mcp/dist/shared-search-fallback.js +2 -12
- package/mcp/dist/shared.js +2 -3
- package/mcp/dist/shell-entry.js +1 -1
- package/mcp/dist/shell-input.js +62 -52
- package/mcp/dist/shell-palette.js +6 -1
- package/mcp/dist/shell-render.js +9 -5
- package/mcp/dist/shell-state-store.js +1 -4
- package/mcp/dist/shell-view.js +4 -4
- package/mcp/dist/shell.js +4 -54
- package/mcp/dist/status.js +2 -8
- package/mcp/dist/utils.js +1 -1
- package/package.json +1 -2
- package/skills/docs.md +11 -11
- package/starter/README.md +1 -1
- package/starter/global/CLAUDE.md +2 -2
- package/starter/global/skills/audit.md +10 -10
- package/mcp/dist/cli-hooks-retrieval.js +0 -2
- package/mcp/dist/impact-scoring.js +0 -22
|
@@ -110,21 +110,21 @@ export function clearTaskCheckpoint(phrenPath, args) {
|
|
|
110
110
|
debugLog(`checkpoint clear ${filePath}: ${errorMessage(err)}`);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
if (args.taskLine) {
|
|
114
|
+
const allProjectCheckpoints = listTaskCheckpoints(phrenPath, args.project);
|
|
115
|
+
for (const checkpoint of allProjectCheckpoints) {
|
|
116
|
+
if (checkpoint.taskLine !== args.taskLine)
|
|
117
|
+
continue;
|
|
118
|
+
const filePath = checkpointPath(phrenPath, checkpoint.project, checkpoint.taskId);
|
|
119
|
+
try {
|
|
120
|
+
if (fs.existsSync(filePath)) {
|
|
121
|
+
fs.unlinkSync(filePath);
|
|
122
|
+
removed++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
debugLog(`checkpoint clear scan ${filePath}: ${errorMessage(err)}`);
|
|
124
127
|
}
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
debugLog(`checkpoint clear scan ${filePath}: ${errorMessage(err)}`);
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
return removed;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { phrenErr, PhrenError, phrenOk, } from "./shared.js";
|
|
4
|
+
import { withFileLock as withFileLockRaw } from "./shared-governance.js";
|
|
5
|
+
import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
|
|
6
|
+
export function withSafeLock(filePath, fn) {
|
|
7
|
+
try {
|
|
8
|
+
return withFileLockRaw(filePath, fn);
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
const msg = errorMessage(err);
|
|
12
|
+
if (msg.includes("could not acquire lock")) {
|
|
13
|
+
return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
|
|
14
|
+
}
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function ensureProject(phrenPath, project) {
|
|
19
|
+
if (!isValidProjectName(project))
|
|
20
|
+
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
21
|
+
const dir = safeProjectPath(phrenPath, project);
|
|
22
|
+
if (!dir)
|
|
23
|
+
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
24
|
+
if (!fs.existsSync(dir)) {
|
|
25
|
+
return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
|
|
26
|
+
}
|
|
27
|
+
return phrenOk(dir);
|
|
28
|
+
}
|
|
@@ -22,7 +22,7 @@ export function escapeLike(s) { return s.replace(/[%_\\]/g, '\\$&'); }
|
|
|
22
22
|
* found" -> suggest adding it).
|
|
23
23
|
*/
|
|
24
24
|
export function logFragmentMiss(phrenPath, name, context, project) {
|
|
25
|
-
if (!process.env.PHREN_DEBUG
|
|
25
|
+
if (!process.env.PHREN_DEBUG)
|
|
26
26
|
return;
|
|
27
27
|
if (!name || name.length <= 2)
|
|
28
28
|
return;
|
|
@@ -116,7 +116,7 @@ function getOrCreateFragment(db, name, type) {
|
|
|
116
116
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [name, type, new Date().toISOString().slice(0, 10)]);
|
|
117
117
|
}
|
|
118
118
|
catch (err) {
|
|
119
|
-
if (process.env.PHREN_DEBUG
|
|
119
|
+
if (process.env.PHREN_DEBUG)
|
|
120
120
|
process.stderr.write(`[phren] fragmentInsert: ${errorMessage(err)}\n`);
|
|
121
121
|
}
|
|
122
122
|
const result = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [name, type]);
|
|
@@ -139,7 +139,7 @@ export function ensureGlobalEntitiesTable(db) {
|
|
|
139
139
|
)`);
|
|
140
140
|
}
|
|
141
141
|
catch (err) {
|
|
142
|
-
if (process.env.PHREN_DEBUG
|
|
142
|
+
if (process.env.PHREN_DEBUG)
|
|
143
143
|
process.stderr.write(`[phren] ensureGlobalEntitiesTable: ${errorMessage(err)}\n`);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -186,7 +186,7 @@ export function beginUserFragmentBuildCache(phrenPath, projects) {
|
|
|
186
186
|
_buildUserFragmentCache.set(cacheKey, loaded.fragments);
|
|
187
187
|
}
|
|
188
188
|
catch (err) {
|
|
189
|
-
if (process.env.PHREN_DEBUG
|
|
189
|
+
if (process.env.PHREN_DEBUG)
|
|
190
190
|
process.stderr.write(`[phren] beginUserFragmentBuildCache: ${errorMessage(err)}\n`);
|
|
191
191
|
_buildUserFragmentCache.set(cacheKey, []);
|
|
192
192
|
}
|
|
@@ -225,7 +225,7 @@ function parseUserDefinedFragments(phrenPath, project) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
catch (err) {
|
|
228
|
-
if (process.env.PHREN_DEBUG
|
|
228
|
+
if (process.env.PHREN_DEBUG)
|
|
229
229
|
process.stderr.write(`[phren] parseUserDefinedFragments statCheck: ${errorMessage(err)}\n`);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
@@ -236,7 +236,7 @@ function parseUserDefinedFragments(phrenPath, project) {
|
|
|
236
236
|
return loaded.fragments;
|
|
237
237
|
}
|
|
238
238
|
catch (err) {
|
|
239
|
-
if (process.env.PHREN_DEBUG
|
|
239
|
+
if (process.env.PHREN_DEBUG)
|
|
240
240
|
process.stderr.write(`[phren] parseUserDefinedFragments: ${errorMessage(err)}\n`);
|
|
241
241
|
return [];
|
|
242
242
|
}
|
|
@@ -353,7 +353,7 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
|
|
|
353
353
|
db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [docFragmentId, fragmentId, "mentions", sourceDoc]);
|
|
354
354
|
}
|
|
355
355
|
catch (err) {
|
|
356
|
-
if (process.env.PHREN_DEBUG
|
|
356
|
+
if (process.env.PHREN_DEBUG)
|
|
357
357
|
process.stderr.write(`[phren] fragmentLinksInsert: ${errorMessage(err)}\n`);
|
|
358
358
|
}
|
|
359
359
|
// Write to global_entities for cross-project queries
|
|
@@ -362,7 +362,7 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
|
|
|
362
362
|
db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [name, project, sourceDoc]);
|
|
363
363
|
}
|
|
364
364
|
catch (err) {
|
|
365
|
-
if (process.env.PHREN_DEBUG
|
|
365
|
+
if (process.env.PHREN_DEBUG)
|
|
366
366
|
process.stderr.write(`[phren] globalFragmentsInsert: ${errorMessage(err)}\n`);
|
|
367
367
|
}
|
|
368
368
|
}
|
|
@@ -391,7 +391,7 @@ export function queryFragmentLinks(db, name) {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
catch (err) {
|
|
394
|
-
if (process.env.PHREN_DEBUG
|
|
394
|
+
if (process.env.PHREN_DEBUG)
|
|
395
395
|
process.stderr.write(`[phren] queryFragmentLinks: ${errorMessage(err)}\n`);
|
|
396
396
|
}
|
|
397
397
|
return { related };
|
|
@@ -427,7 +427,7 @@ export function queryCrossProjectFragments(db, fragmentName, excludeProject) {
|
|
|
427
427
|
}
|
|
428
428
|
}
|
|
429
429
|
catch (err) {
|
|
430
|
-
if (process.env.PHREN_DEBUG
|
|
430
|
+
if (process.env.PHREN_DEBUG)
|
|
431
431
|
process.stderr.write(`[phren] queryCrossProjectFragments: ${errorMessage(err)}\n`);
|
|
432
432
|
}
|
|
433
433
|
return results;
|
|
@@ -448,7 +448,7 @@ export function getFragmentBoostDocs(db, query) {
|
|
|
448
448
|
return boostDocs;
|
|
449
449
|
}
|
|
450
450
|
catch (err) {
|
|
451
|
-
if (process.env.PHREN_DEBUG
|
|
451
|
+
if (process.env.PHREN_DEBUG)
|
|
452
452
|
process.stderr.write(`[phren] getFragmentBoostDocs: ${errorMessage(err)}\n`);
|
|
453
453
|
return new Set();
|
|
454
454
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from './governance-policy.js';
|
|
2
2
|
export * from './governance-scores.js';
|
|
3
3
|
export * from './governance-audit.js';
|
|
4
|
-
export { withFileLock } from './governance-locks.js';
|
|
4
|
+
export { withFileLock, isFiniteNumber, hasValidSchemaVersion } from './governance-locks.js';
|
|
@@ -8,7 +8,7 @@ import { buildFtsQueryVariants, buildRelaxedFtsQuery, isFeatureEnabled, STOP_WOR
|
|
|
8
8
|
import * as fs from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
10
|
import { getProjectGlobBoost } from "./cli-hooks-globs.js";
|
|
11
|
-
import { vectorFallback } from "./shared-search-fallback.js";
|
|
11
|
+
import { vectorFallback, deterministicSeed } from "./shared-search-fallback.js";
|
|
12
12
|
import { getOllamaUrl, getCloudEmbeddingUrl } from "./shared-ollama.js";
|
|
13
13
|
import { keywordFallbackSearch } from "./core-search.js";
|
|
14
14
|
import { debugLog } from "./shared.js";
|
|
@@ -161,14 +161,6 @@ function docOverlapScore(queryTokens, doc) {
|
|
|
161
161
|
const corpus = `${doc.project} ${doc.filename} ${doc.type} ${doc.path}\n${doc.content.slice(0, 5000)}`;
|
|
162
162
|
return overlapScore(queryTokens, corpus);
|
|
163
163
|
}
|
|
164
|
-
function semanticFallbackSeed(text) {
|
|
165
|
-
let hash = 2166136261;
|
|
166
|
-
for (let i = 0; i < text.length; i++) {
|
|
167
|
-
hash ^= text.charCodeAt(i);
|
|
168
|
-
hash = Math.imul(hash, 16777619);
|
|
169
|
-
}
|
|
170
|
-
return hash >>> 0;
|
|
171
|
-
}
|
|
172
164
|
function loadSemanticFallbackWindow(db, startRowid, limit, project, wrapBefore) {
|
|
173
165
|
const where = [
|
|
174
166
|
project ? "project = ?" : "",
|
|
@@ -250,7 +242,7 @@ function semanticFallbackDocs(db, prompt, project) {
|
|
|
250
242
|
const windowCount = Math.min(SEMANTIC_FALLBACK_WINDOW_COUNT, cappedLimit);
|
|
251
243
|
const perWindow = Math.max(1, Math.ceil(cappedLimit / windowCount));
|
|
252
244
|
const stride = Math.max(1, Math.floor(span / windowCount));
|
|
253
|
-
const seed =
|
|
245
|
+
const seed = deterministicSeed(`${project ?? "*"}\n${terms.join(" ")}`);
|
|
254
246
|
for (let i = 0; i < windowCount && docs.length < cappedLimit; i++) {
|
|
255
247
|
const offset = (seed + i * stride) % span;
|
|
256
248
|
const startRowid = minRowid + offset;
|
|
@@ -72,7 +72,7 @@ function cachedTokenize(text) {
|
|
|
72
72
|
tokenCache.set(key, tokens);
|
|
73
73
|
return tokens;
|
|
74
74
|
}
|
|
75
|
-
function deterministicSeed(text) {
|
|
75
|
+
export function deterministicSeed(text) {
|
|
76
76
|
let hash = 2166136261;
|
|
77
77
|
for (let i = 0; i < text.length; i++) {
|
|
78
78
|
hash ^= text.charCodeAt(i);
|
|
@@ -153,18 +153,8 @@ function tfidfCosine(docs, query, corpusN) {
|
|
|
153
153
|
return termTf * idf;
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
|
-
function cosine(a, b) {
|
|
157
|
-
let dot = 0, normA = 0, normB = 0;
|
|
158
|
-
for (let i = 0; i < a.length; i++) {
|
|
159
|
-
dot += a[i] * b[i];
|
|
160
|
-
normA += a[i] * a[i];
|
|
161
|
-
normB += b[i] * b[i];
|
|
162
|
-
}
|
|
163
|
-
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
164
|
-
return denom === 0 ? 0 : dot / denom;
|
|
165
|
-
}
|
|
166
156
|
const queryVec = buildVector(queryTokens);
|
|
167
|
-
return docTokenLists.map(docTokens =>
|
|
157
|
+
return docTokenLists.map(docTokens => cosineSimilarity(queryVec, buildVector(docTokens)));
|
|
168
158
|
}
|
|
169
159
|
/**
|
|
170
160
|
* Cosine fallback search: when FTS5 returns fewer than COSINE_FALLBACK_THRESHOLD results,
|
package/mcp/dist/shared.js
CHANGED
|
@@ -3,10 +3,9 @@ import * as path from "path";
|
|
|
3
3
|
import { debugLog, runtimeFile } from "./phren-paths.js";
|
|
4
4
|
import { errorMessage } from "./utils.js";
|
|
5
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";
|
|
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, RESERVED_PROJECT_DIR_NAMES, } from "./phren-core.js";
|
|
7
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
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
9
|
const MEMORY_SCOPE_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
11
10
|
export function normalizeMemoryScope(scope) {
|
|
12
11
|
if (typeof scope !== "string")
|
|
@@ -81,7 +80,7 @@ export function appendAuditLog(phrenPath, event, details) {
|
|
|
81
80
|
if (stat.size > 1_000_000) {
|
|
82
81
|
const content = fs.readFileSync(logPath, "utf8");
|
|
83
82
|
const lines = content.split("\n");
|
|
84
|
-
fs.writeFileSync(logPath, lines.slice(-500).join("\n"));
|
|
83
|
+
fs.writeFileSync(logPath, lines.slice(-500).join("\n") + "\n");
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
else {
|
package/mcp/dist/shell-entry.js
CHANGED
|
@@ -147,7 +147,7 @@ export async function startShell(phrenPath, profile) {
|
|
|
147
147
|
const shell = new PhrenShell(phrenPath, profile);
|
|
148
148
|
if (!process.stdin.isTTY) {
|
|
149
149
|
const { createInterface } = await import("readline");
|
|
150
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal:
|
|
150
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY ?? false });
|
|
151
151
|
const repaint = async () => { clearScreen(); process.stdout.write(await shell.render()); rl.setPrompt(`\n${style.boldCyan(":phren>")} `); rl.prompt(); };
|
|
152
152
|
const stopPoll = startLiveStatePoller({ phrenPath, shell, repaint });
|
|
153
153
|
await repaint();
|
package/mcp/dist/shell-input.js
CHANGED
|
@@ -740,6 +740,67 @@ function showCursorPosition(host) {
|
|
|
740
740
|
const short = label.length > 50 ? label.slice(0, 48) + "…" : label;
|
|
741
741
|
host.setMessage(` ${style.dim(`${cursor + 1} / ${count}`)}${short ? ` ${style.dimItalic(short)}` : ""}`);
|
|
742
742
|
}
|
|
743
|
+
// ── View shortcut keys (shared between handleInput text mode and handleNavigateKey) ─
|
|
744
|
+
/**
|
|
745
|
+
* Handle p/b/l/m/s/k/h shortcut keys that switch the active view.
|
|
746
|
+
* Returns true if the key was handled.
|
|
747
|
+
*/
|
|
748
|
+
export function applyViewShortcut(host, key) {
|
|
749
|
+
if (key === "p") {
|
|
750
|
+
host.setView("Projects");
|
|
751
|
+
host.setMessage(` ${TAB_ICONS.Projects} Projects`);
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
if (key === "b") {
|
|
755
|
+
if (!host.state.project) {
|
|
756
|
+
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
host.setView("Tasks");
|
|
760
|
+
host.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
if (key === "l") {
|
|
764
|
+
if (!host.state.project) {
|
|
765
|
+
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
host.setView("Findings");
|
|
769
|
+
host.setMessage(` ${TAB_ICONS.Findings} Findings`);
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
if (key === "m") {
|
|
773
|
+
if (!host.state.project) {
|
|
774
|
+
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
host.setView("Review Queue");
|
|
778
|
+
host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
if (key === "s") {
|
|
782
|
+
if (!host.state.project) {
|
|
783
|
+
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
host.setView("Skills");
|
|
787
|
+
host.setMessage(` ${TAB_ICONS.Skills} Skills`);
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
if (key === "k") {
|
|
791
|
+
host.setView("Hooks");
|
|
792
|
+
host.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
if (key === "h") {
|
|
796
|
+
host.prevHealthView = host.state.view === "Health" ? host.prevHealthView : host.state.view;
|
|
797
|
+
host.healthCache = undefined;
|
|
798
|
+
host.setView("Health");
|
|
799
|
+
host.setMessage(` ${TAB_ICONS.Health} Health ${style.dim("(esc to return)")}`);
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
743
804
|
// ── Navigate-mode key handler ─────────────────────────────────────────────────
|
|
744
805
|
export async function handleNavigateKey(host, key) {
|
|
745
806
|
if (key === "\x1b[A") {
|
|
@@ -836,59 +897,8 @@ export async function handleNavigateKey(host, key) {
|
|
|
836
897
|
}
|
|
837
898
|
return true;
|
|
838
899
|
}
|
|
839
|
-
if (key
|
|
840
|
-
host.setView("Projects");
|
|
841
|
-
host.setMessage(` ${TAB_ICONS.Projects} Projects`);
|
|
842
|
-
return true;
|
|
843
|
-
}
|
|
844
|
-
if (key === "b") {
|
|
845
|
-
if (!host.state.project) {
|
|
846
|
-
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
847
|
-
return true;
|
|
848
|
-
}
|
|
849
|
-
host.setView("Tasks");
|
|
850
|
-
host.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
|
|
900
|
+
if (applyViewShortcut(host, key))
|
|
851
901
|
return true;
|
|
852
|
-
}
|
|
853
|
-
if (key === "l") {
|
|
854
|
-
if (!host.state.project) {
|
|
855
|
-
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
856
|
-
return true;
|
|
857
|
-
}
|
|
858
|
-
host.setView("Findings");
|
|
859
|
-
host.setMessage(` ${TAB_ICONS.Findings} Fragments`);
|
|
860
|
-
return true;
|
|
861
|
-
}
|
|
862
|
-
if (key === "m") {
|
|
863
|
-
if (!host.state.project) {
|
|
864
|
-
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
865
|
-
return true;
|
|
866
|
-
}
|
|
867
|
-
host.setView("Review Queue");
|
|
868
|
-
host.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
|
|
869
|
-
return true;
|
|
870
|
-
}
|
|
871
|
-
if (key === "s") {
|
|
872
|
-
if (!host.state.project) {
|
|
873
|
-
host.setMessage(style.dim(" Select a project first (↵)"));
|
|
874
|
-
return true;
|
|
875
|
-
}
|
|
876
|
-
host.setView("Skills");
|
|
877
|
-
host.setMessage(` ${TAB_ICONS.Skills} Skills`);
|
|
878
|
-
return true;
|
|
879
|
-
}
|
|
880
|
-
if (key === "k") {
|
|
881
|
-
host.setView("Hooks");
|
|
882
|
-
host.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
|
|
883
|
-
return true;
|
|
884
|
-
}
|
|
885
|
-
if (key === "h") {
|
|
886
|
-
host.prevHealthView = host.state.view === "Health" ? host.prevHealthView : host.state.view;
|
|
887
|
-
host.healthCache = undefined;
|
|
888
|
-
host.setView("Health");
|
|
889
|
-
host.setMessage(` ${TAB_ICONS.Health} Health ${style.dim("(esc to return)")}`);
|
|
890
|
-
return true;
|
|
891
|
-
}
|
|
892
902
|
if (key === "i" && host.state.view === "Projects") {
|
|
893
903
|
const next = host.state.introMode === "always" ? "once-per-version" : host.state.introMode === "off" ? "always" : "off";
|
|
894
904
|
host.state.introMode = next;
|
|
@@ -7,7 +7,12 @@ import { EXEC_TIMEOUT_MS, } from "./shared.js";
|
|
|
7
7
|
export function resultMsg(r) {
|
|
8
8
|
if (!r.ok)
|
|
9
9
|
return r.error;
|
|
10
|
-
|
|
10
|
+
if (typeof r.data === "string")
|
|
11
|
+
return r.data;
|
|
12
|
+
if (r.data && typeof r.data === "object" && "message" in r.data && typeof r.data.message === "string") {
|
|
13
|
+
return r.data.message;
|
|
14
|
+
}
|
|
15
|
+
return JSON.stringify(r.data);
|
|
11
16
|
}
|
|
12
17
|
export function editDistance(a, b) {
|
|
13
18
|
const m = a.length;
|
package/mcp/dist/shell-render.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// ── ANSI utilities ──────────────────────────────────────────────────────────
|
|
2
2
|
const ESC = "\x1b[";
|
|
3
3
|
export const RESET = `${ESC}0m`;
|
|
4
|
+
export const BOLD = `${ESC}1m`;
|
|
5
|
+
export const DIM = `${ESC}2m`;
|
|
6
|
+
export const GREEN = `${ESC}32m`;
|
|
7
|
+
export const YELLOW = `${ESC}33m`;
|
|
8
|
+
export const RED = `${ESC}31m`;
|
|
9
|
+
export const CYAN = `${ESC}36m`;
|
|
4
10
|
export const style = {
|
|
5
11
|
bold: (s) => `${ESC}1m${s}${RESET}`,
|
|
6
12
|
dim: (s) => `${ESC}2m${s}${RESET}`,
|
|
@@ -144,9 +150,7 @@ const PHREN_LOGO = [
|
|
|
144
150
|
"██║ ██║ ██║██║ ██║███████╗██║ ╚████║",
|
|
145
151
|
"╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝",
|
|
146
152
|
];
|
|
147
|
-
// Compact phren character for startup (uses PHREN_ART from phren-art.ts via import)
|
|
148
153
|
import { PHREN_ART as PHREN_STARTUP_ART } from "./phren-art.js";
|
|
149
|
-
const PHREN_STARTUP = PHREN_STARTUP_ART;
|
|
150
154
|
// ── Line-based viewport: edge-triggered scroll (stable, no jumpiness) ─────────
|
|
151
155
|
export function lineViewport(allLines, cursorFirstLine, cursorLastLine, height, prevStart) {
|
|
152
156
|
if (allLines.length === 0 || height <= 0)
|
|
@@ -181,7 +185,7 @@ export function shellHelpText() {
|
|
|
181
185
|
hdr("View-specific keys"),
|
|
182
186
|
` ${style.bold("Projects")} ${k("↵")} ${d("open project tasks")} ${k("i")} ${d("cycle intro mode")}`,
|
|
183
187
|
` ${style.bold("Tasks")} ${k("a")} ${d("add task")} ${k("d")} ${d("toggle active/queue")} ${k("↵")} ${d("mark complete")}`,
|
|
184
|
-
` ${style.bold("
|
|
188
|
+
` ${style.bold("Findings")} ${k("a")} ${d("tell phren")} ${k("d")} ${d("delete selected")}`,
|
|
185
189
|
` ${style.bold("Review Queue")} ${k("↵")} ${d("inspect selected item")} ${d("(read-only)")}`,
|
|
186
190
|
` ${style.bold("Skills")} ${k("t")} ${d("toggle enabled")} ${k("d")} ${d("remove")}`,
|
|
187
191
|
"",
|
|
@@ -221,7 +225,7 @@ export function shellStartupFrames(version) {
|
|
|
221
225
|
const versionBadge = badge(`v${version}`, style.boldBlue);
|
|
222
226
|
if (cols >= 72) {
|
|
223
227
|
// Side-by-side: phren character on left, logo text on right
|
|
224
|
-
const phrenLines =
|
|
228
|
+
const phrenLines = PHREN_STARTUP_ART;
|
|
225
229
|
const logoLines = PHREN_LOGO.map(line => gradient(line));
|
|
226
230
|
const infoLine = `${gradient("◆")} ${style.bold("phren")} ${versionBadge} ${tagline}`;
|
|
227
231
|
// Logo is 6 lines, pad to align vertically with character center
|
|
@@ -252,7 +256,7 @@ export function shellStartupFrames(version) {
|
|
|
252
256
|
];
|
|
253
257
|
}
|
|
254
258
|
// Narrow terminal: progressive text reveal with gradient
|
|
255
|
-
const stages = ["
|
|
259
|
+
const stages = ["p", "phr", "phren"];
|
|
256
260
|
const spinners = ["◜", "◠", "◝"];
|
|
257
261
|
return stages.map((stage, i) => [
|
|
258
262
|
"",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { phrenErr, PhrenError, phrenOk, shellStateFile } from "./shared.js";
|
|
4
|
-
import {
|
|
4
|
+
import { withFileLock as withFileLockRaw } from "./shared-governance.js";
|
|
5
5
|
import { errorMessage } from "./utils.js";
|
|
6
6
|
function withSafeLock(filePath, fn) {
|
|
7
7
|
try {
|
|
@@ -76,6 +76,3 @@ export function resetShellState(phrenPath) {
|
|
|
76
76
|
return phrenOk("Shell state reset.");
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
export function readRuntimeHealth(phrenPath) {
|
|
80
|
-
return getRuntimeHealth(phrenPath);
|
|
81
|
-
}
|
package/mcp/dist/shell-view.js
CHANGED
|
@@ -67,7 +67,7 @@ function renderBottomBar(state, navMode, inputCtx, inputBuf) {
|
|
|
67
67
|
Tasks: [`${k("a")} ${d("add")}`, `${k("↵")} ${d("mark done")}`, `${k("d")} ${d("toggle active")}`],
|
|
68
68
|
Findings: [`${k("a")} ${d("add")}`, `${k("d")} ${d("remove")}`],
|
|
69
69
|
"Review Queue": [`${k("↵")} ${d("inspect")}`],
|
|
70
|
-
Skills: [`${k("t")} ${d("toggle")}`, `${k("d")} ${d("remove")}`],
|
|
70
|
+
Skills: [`${k("a")} ${d("add")}`, `${k("t")} ${d("toggle")}`, `${k("d")} ${d("remove")}`],
|
|
71
71
|
Hooks: [`${k("a")} ${d("enable")}`, `${k("d")} ${d("disable")}`],
|
|
72
72
|
Health: [`${k("↑↓")} ${d("scroll")}`, `${k("esc")} ${d("back")}`],
|
|
73
73
|
};
|
|
@@ -136,9 +136,9 @@ function renderProjectsDashboard(ctx, entries, height) {
|
|
|
136
136
|
const findingsPreview = scoped
|
|
137
137
|
.filter((entry) => entry.findingCount > 0)
|
|
138
138
|
.slice(0, 3)
|
|
139
|
-
.map((entry) => `${style.bold(entry.name)} ${style.dim(`${entry.findingCount}
|
|
139
|
+
.map((entry) => `${style.bold(entry.name)} ${style.dim(`${entry.findingCount} findings`)}`);
|
|
140
140
|
const lines = [
|
|
141
|
-
` ${badge(ctx.profile || "default", style.boldBlue)} ${style.bold(String(scoped.length))} projects ${style.dim("·")} ${style.boldGreen(String(totals.active))} active ${style.dim("·")} ${style.boldYellow(String(totals.queue))} queued ${style.dim("·")} ${style.boldCyan(String(totals.findings))}
|
|
141
|
+
` ${badge(ctx.profile || "default", style.boldBlue)} ${style.bold(String(scoped.length))} projects ${style.dim("·")} ${style.boldGreen(String(totals.active))} active ${style.dim("·")} ${style.boldYellow(String(totals.queue))} queued ${style.dim("·")} ${style.boldCyan(String(totals.findings))} findings ${style.dim("·")} ${style.boldMagenta(String(totals.review))} review`,
|
|
142
142
|
ctx.state.project
|
|
143
143
|
? ` ${style.green("●")} active context ${style.boldCyan(ctx.state.project)} ${style.dim("· ↵ opens selected project tasks")}`
|
|
144
144
|
: ` ${style.dim("No project selected yet")} ${style.dim("· ↵ sets context and opens tasks")}`,
|
|
@@ -517,7 +517,7 @@ function renderSkillsView(ctx, cursor, height) {
|
|
|
517
517
|
}
|
|
518
518
|
const LIFECYCLE_HOOKS = [
|
|
519
519
|
{ event: "UserPromptSubmit", description: "inject context before each prompt" },
|
|
520
|
-
{ event: "Stop", description: "phren saves
|
|
520
|
+
{ event: "Stop", description: "phren saves findings after each response" },
|
|
521
521
|
{ event: "SessionStart", description: "git pull at session start" },
|
|
522
522
|
];
|
|
523
523
|
export function getHookEntries(phrenPath, project) {
|
package/mcp/dist/shell.js
CHANGED
|
@@ -2,11 +2,11 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { addTask, addFinding, loadShellState, saveShellState, } from "./data-access.js";
|
|
4
4
|
import { style } from "./shell-render.js";
|
|
5
|
-
import { MAX_UNDO_STACK,
|
|
5
|
+
import { MAX_UNDO_STACK, } from "./shell-types.js";
|
|
6
6
|
import { resultMsg, defaultRunHooks, defaultRunUpdate, defaultRunRelink, } from "./shell-palette.js";
|
|
7
7
|
import { runDoctor } from "./link.js";
|
|
8
8
|
import { renderShell, } from "./shell-view.js";
|
|
9
|
-
import { executePalette, completeInput as completeInputFn, getListItems, handleNavigateKey, } from "./shell-input.js";
|
|
9
|
+
import { executePalette, completeInput as completeInputFn, getListItems, handleNavigateKey, applyViewShortcut, } from "./shell-input.js";
|
|
10
10
|
import { errorMessage } from "./utils.js";
|
|
11
11
|
// ── Shell class ──────────────────────────────────────────────────────────────
|
|
12
12
|
export class PhrenShell {
|
|
@@ -16,7 +16,7 @@ export class PhrenShell {
|
|
|
16
16
|
state;
|
|
17
17
|
message = ` ${style.boldCyan("←→")} ${style.dim("tabs")} ${style.boldCyan("↑↓")} ${style.dim("move")} ${style.boldCyan("↵")} ${style.dim("activate")} ${style.boldCyan("?")} ${style.dim("help")}`;
|
|
18
18
|
healthCache;
|
|
19
|
-
prevHealthView;
|
|
19
|
+
prevHealthView = undefined;
|
|
20
20
|
showHelp = false;
|
|
21
21
|
pendingConfirm;
|
|
22
22
|
undoStack = [];
|
|
@@ -277,58 +277,8 @@ export class PhrenShell {
|
|
|
277
277
|
return true;
|
|
278
278
|
if (["q", "quit", ":q", ":quit", ":exit"].includes(input.toLowerCase()))
|
|
279
279
|
return false;
|
|
280
|
-
if (input
|
|
281
|
-
this.setView("Projects");
|
|
282
|
-
this.setMessage(` ${TAB_ICONS.Projects} Projects`);
|
|
280
|
+
if (applyViewShortcut(this.asNavigationHost(), input))
|
|
283
281
|
return true;
|
|
284
|
-
}
|
|
285
|
-
if (input === "b") {
|
|
286
|
-
if (!this.state.project) {
|
|
287
|
-
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
288
|
-
return true;
|
|
289
|
-
}
|
|
290
|
-
this.setView("Tasks");
|
|
291
|
-
this.setMessage(` ${TAB_ICONS.Tasks} Tasks`);
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
if (input === "l") {
|
|
295
|
-
if (!this.state.project) {
|
|
296
|
-
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
297
|
-
return true;
|
|
298
|
-
}
|
|
299
|
-
this.setView("Findings");
|
|
300
|
-
this.setMessage(` ${TAB_ICONS.Findings} Fragments`);
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
if (input === "m") {
|
|
304
|
-
if (!this.state.project) {
|
|
305
|
-
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
this.setView("Review Queue");
|
|
309
|
-
this.setMessage(` ${TAB_ICONS["Review Queue"]} Review Queue`);
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
if (input === "s") {
|
|
313
|
-
if (!this.state.project) {
|
|
314
|
-
this.setMessage(style.dim(" Select a project first (↵)"));
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
this.setView("Skills");
|
|
318
|
-
this.setMessage(` ${TAB_ICONS.Skills} Skills`);
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
if (input === "k") {
|
|
322
|
-
this.setView("Hooks");
|
|
323
|
-
this.setMessage(` ${TAB_ICONS.Hooks} Hooks`);
|
|
324
|
-
return true;
|
|
325
|
-
}
|
|
326
|
-
if (input === "h") {
|
|
327
|
-
this.healthCache = undefined;
|
|
328
|
-
this.setView("Health");
|
|
329
|
-
this.setMessage(` ${TAB_ICONS.Health} Health`);
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
282
|
if (input.startsWith("/")) {
|
|
333
283
|
this.setFilter(input.slice(1));
|
|
334
284
|
return true;
|
package/mcp/dist/status.js
CHANGED
|
@@ -10,6 +10,7 @@ import { runGit as runGitShared, errorMessage } from "./utils.js";
|
|
|
10
10
|
import { readRuntimeHealth, resolveTaskFilePath } from "./data-access.js";
|
|
11
11
|
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
12
12
|
import { renderPhrenArt } from "./phren-art.js";
|
|
13
|
+
import { RESET, BOLD, DIM, GREEN, YELLOW, RED, CYAN } from "./shell-render.js";
|
|
13
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
function readPackageVersion() {
|
|
15
16
|
try {
|
|
@@ -23,13 +24,6 @@ function readPackageVersion() {
|
|
|
23
24
|
return "unknown";
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
const RESET = "\x1b[0m";
|
|
27
|
-
const BOLD = "\x1b[1m";
|
|
28
|
-
const DIM = "\x1b[2m";
|
|
29
|
-
const GREEN = "\x1b[32m";
|
|
30
|
-
const YELLOW = "\x1b[33m";
|
|
31
|
-
const RED = "\x1b[31m";
|
|
32
|
-
const CYAN = "\x1b[36m";
|
|
33
27
|
function check(ok) {
|
|
34
28
|
return ok ? `${GREEN}ok${RESET}` : `${RED}missing${RESET}`;
|
|
35
29
|
}
|
|
@@ -302,7 +296,7 @@ export async function runStatus() {
|
|
|
302
296
|
totalTask += countBullets(taskPath);
|
|
303
297
|
totalQueue += countQueueItems(phrenPath, projName);
|
|
304
298
|
}
|
|
305
|
-
console.log(`\n ${DIM}phren holds${RESET} ${projectDirs.length} projects, ${totalFindings}
|
|
299
|
+
console.log(`\n ${DIM}phren holds${RESET} ${projectDirs.length} projects, ${totalFindings} findings, ${totalTask} tasks, ${totalQueue} queued`);
|
|
306
300
|
const gitTarget = manifest?.installMode === "project-local" && manifest.workspaceRoot ? manifest.workspaceRoot : phrenPath;
|
|
307
301
|
const isGitRepo = runGit(gitTarget, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
308
302
|
const hasOriginRemote = isGitRepo && Boolean(runGit(gitTarget, ["remote", "get-url", "origin"]));
|
package/mcp/dist/utils.js
CHANGED
|
@@ -241,7 +241,7 @@ export function sanitizeFts5Query(raw) {
|
|
|
241
241
|
if (raw.length > 500)
|
|
242
242
|
raw = raw.slice(0, 500);
|
|
243
243
|
// Whitelist approach: only allow alphanumeric, spaces, hyphens, apostrophes, double quotes, asterisks
|
|
244
|
-
let q = raw.replace(/[^a-zA-Z0-9 \-
|
|
244
|
+
let q = raw.replace(/[^a-zA-Z0-9 \-"*]/g, " ");
|
|
245
245
|
q = q.replace(/\s+/g, " ");
|
|
246
246
|
q = q.trim();
|
|
247
247
|
// Q83: FTS5 only accepts * as a prefix operator directly attached to a token
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phren/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Knowledge layer for AI agents. Phren learns and recalls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"glob": "^13.0.6",
|
|
19
19
|
"inquirer": "^12.10.0",
|
|
20
20
|
"js-yaml": "^4.1.1",
|
|
21
|
-
"sharp": "^0.34.5",
|
|
22
21
|
"sql.js-fts5": "^1.4.0",
|
|
23
22
|
"zod": "^4.3.6"
|
|
24
23
|
},
|