@pcircle/memesh 4.5.1 → 4.6.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/.claude-plugin/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +6 -4
- package/AGENTS.md +116 -0
- package/README.de.md +141 -48
- package/README.md +173 -48
- package/README.zh-TW.md +142 -48
- package/dashboard/dist/index.html +15 -14
- package/dist/cli/view-live.js +3 -3
- package/dist/core/analytics.d.ts +9 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +36 -18
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/auto-tagger.d.ts.map +1 -1
- package/dist/core/auto-tagger.js +4 -9
- package/dist/core/auto-tagger.js.map +1 -1
- package/dist/core/briefing.d.ts +8 -0
- package/dist/core/briefing.d.ts.map +1 -0
- package/dist/core/briefing.js +92 -0
- package/dist/core/briefing.js.map +1 -0
- package/dist/core/capture-flag.d.ts +5 -0
- package/dist/core/capture-flag.d.ts.map +1 -0
- package/dist/core/capture-flag.js +10 -0
- package/dist/core/capture-flag.js.map +1 -0
- package/dist/core/config.d.ts +0 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-candidates.d.ts +20 -0
- package/dist/core/conflict-candidates.d.ts.map +1 -0
- package/dist/core/conflict-candidates.js +79 -0
- package/dist/core/conflict-candidates.js.map +1 -0
- package/dist/core/conflict-judge.d.ts +47 -0
- package/dist/core/conflict-judge.d.ts.map +1 -0
- package/dist/core/conflict-judge.js +189 -0
- package/dist/core/conflict-judge.js.map +1 -0
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +1 -1
- package/dist/core/demo.js.map +1 -1
- package/dist/core/digest-validator.d.ts.map +1 -1
- package/dist/core/digest-validator.js +3 -5
- package/dist/core/digest-validator.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +59 -62
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +5 -2
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +329 -25
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/embedder.d.ts +8 -4
- package/dist/core/embedder.d.ts.map +1 -1
- package/dist/core/embedder.js +82 -24
- package/dist/core/embedder.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +7 -12
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/graph.d.ts +12 -0
- package/dist/core/graph.d.ts.map +1 -1
- package/dist/core/graph.js +56 -1
- package/dist/core/graph.js.map +1 -1
- package/dist/core/guards.d.ts +20 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +103 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/install-channel.d.ts +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +16 -5
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-hooks.d.ts +5 -0
- package/dist/core/install-hooks.d.ts.map +1 -1
- package/dist/core/install-hooks.js +0 -0
- package/dist/core/install-hooks.js.map +1 -1
- package/dist/core/json-utils.d.ts +1 -0
- package/dist/core/json-utils.d.ts.map +1 -1
- package/dist/core/json-utils.js +19 -10
- package/dist/core/json-utils.js.map +1 -1
- package/dist/core/kg-backfill.d.ts +5 -2
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js +155 -5
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +14 -21
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +4 -4
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts +13 -2
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +115 -28
- package/dist/core/operations.js.map +1 -1
- package/dist/core/prompt-safety.d.ts +1 -0
- package/dist/core/prompt-safety.d.ts.map +1 -1
- package/dist/core/prompt-safety.js +7 -0
- package/dist/core/prompt-safety.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +31 -0
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +8 -0
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/setup.d.ts +29 -0
- package/dist/core/setup.d.ts.map +1 -0
- package/dist/core/setup.js +127 -0
- package/dist/core/setup.js.map +1 -0
- package/dist/core/task-state-store.d.ts +17 -0
- package/dist/core/task-state-store.d.ts.map +1 -0
- package/dist/core/task-state-store.js +45 -0
- package/dist/core/task-state-store.js.map +1 -0
- package/dist/core/task-state.d.ts +19 -0
- package/dist/core/task-state.d.ts.map +1 -0
- package/dist/core/task-state.js +91 -0
- package/dist/core/task-state.js.map +1 -0
- package/dist/core/time-utils.d.ts +2 -0
- package/dist/core/time-utils.d.ts.map +1 -0
- package/dist/core/time-utils.js +14 -0
- package/dist/core/time-utils.js.map +1 -0
- package/dist/core/title.d.ts +5 -0
- package/dist/core/title.d.ts.map +1 -0
- package/dist/core/title.js +14 -0
- package/dist/core/title.js.map +1 -0
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +2 -3
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/why.d.ts +54 -0
- package/dist/core/why.d.ts.map +1 -0
- package/dist/core/why.js +168 -0
- package/dist/core/why.js.map +1 -0
- package/dist/core/work-topology.d.ts +36 -0
- package/dist/core/work-topology.d.ts.map +1 -0
- package/dist/core/work-topology.js +192 -0
- package/dist/core/work-topology.js.map +1 -0
- package/dist/db.d.ts +33 -11
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +307 -315
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +1 -0
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +50 -40
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/skills-manifest.json +62 -22
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +2 -7
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/fts-index.d.ts +4 -2
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +16 -4
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/schema.d.ts +20 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +274 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transports/cli/cli.d.ts +1 -4
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +579 -66
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +242 -303
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +46 -0
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +59 -4
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +29 -10
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +33 -8
- package/dist/transports/schemas.js.map +1 -1
- package/hooks/hooks.json +10 -0
- package/llms-install.md +138 -0
- package/package.json +14 -9
- package/scripts/hooks/_generated/capture-flag.js +17 -0
- package/scripts/hooks/_generated/fts-index.js +16 -4
- package/scripts/hooks/_generated/guards.js +110 -0
- package/scripts/hooks/_generated/schema.js +281 -0
- package/scripts/hooks/_generated/sqlite.js +1 -1
- package/scripts/hooks/_generated/task-state.js +98 -0
- package/scripts/hooks/_generated/time-utils.js +21 -0
- package/scripts/hooks/_generated/title.js +21 -0
- package/scripts/hooks/_generated/work-topology.js +199 -0
- package/scripts/hooks/_shared.js +197 -480
- package/scripts/hooks/guard-check.js +76 -0
- package/scripts/hooks/post-commit.js +31 -1
- package/scripts/hooks/pre-compact.js +13 -1
- package/scripts/hooks/pre-edit-recall.js +158 -120
- package/scripts/hooks/session-start.js +169 -82
- package/scripts/hooks/session-summary.js +78 -90
- package/skills/memesh/SKILL.md +108 -76
- package/README.es.md +0 -467
- package/README.fr.md +0 -459
- package/README.ja.md +0 -467
- package/README.ko.md +0 -467
- package/README.pt.md +0 -459
- package/README.th.md +0 -460
- package/README.vi.md +0 -459
- package/README.zh-CN.md +0 -466
- package/dist/cli/view.d.ts +0 -3
- package/dist/cli/view.d.ts.map +0 -1
- package/dist/cli/view.js +0 -523
- package/dist/cli/view.js.map +0 -1
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import { openDatabase, closeDatabase, getDatabase, reindexFts,
|
|
7
|
+
import { openDatabase, closeDatabase, getDatabase, reindexFts, readVectorGeneration, generationRowIds, discardVectorGeneration, } from '../../db.js';
|
|
8
8
|
import { remember, recallWithConflicts, forget, exportMemories, importMemories, learn, reindex, setPinned } from '../../core/operations.js';
|
|
9
9
|
import { readConfig, writeConfig, maskApiKey, detectCapabilities } from '../../core/config.js';
|
|
10
10
|
import { MAX_LANGUAGE_LENGTH, languageValueError } from '../../core/output-language.js';
|
|
11
|
-
import { getDbPath, redactSecrets, redactUserPaths } from '../../core/paths.js';
|
|
11
|
+
import { getDbPath, getProjectName, homeDir, redactSecrets, redactUserPaths } from '../../core/paths.js';
|
|
12
12
|
import { flushPendingEmbeddings, canRefillVectorIndex } from '../../core/embedder.js';
|
|
13
13
|
import { NAMESPACES } from '../../core/types.js';
|
|
14
|
+
import { assembleBriefing } from '../../core/briefing.js';
|
|
15
|
+
import { inspectHosts, allWired } from '../../core/setup.js';
|
|
16
|
+
import { installHooks } from '../../core/install-hooks.js';
|
|
17
|
+
import { getTaskState, setTaskState } from '../../core/task-state-store.js';
|
|
18
|
+
import { TASK_STATE_FIELDS, taskStateLines } from '../../core/task-state.js';
|
|
14
19
|
async function withDatabase(fn) {
|
|
15
20
|
openDatabase();
|
|
16
21
|
try {
|
|
@@ -26,13 +31,34 @@ function requireOneOf(value, allowed, flag) {
|
|
|
26
31
|
console.error(`Error: ${flag} "${value}" is not valid. Use one of: ${allowed.join(', ')}.`);
|
|
27
32
|
process.exit(1);
|
|
28
33
|
}
|
|
34
|
+
function isOnPath(tool) {
|
|
35
|
+
const exts = process.platform === 'win32'
|
|
36
|
+
? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
|
|
37
|
+
: [''];
|
|
38
|
+
for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
|
|
39
|
+
if (!dir)
|
|
40
|
+
continue;
|
|
41
|
+
for (const ext of exts) {
|
|
42
|
+
try {
|
|
43
|
+
fs.accessSync(path.join(dir, tool + ext), fs.constants.X_OK);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function wireUserHooks() {
|
|
52
|
+
const r = installHooks({ pluginRoot: packageRoot, pluginVersion: pkg.version, scope: 'user' });
|
|
53
|
+
return `hooks: added ${r.added}, skipped ${r.skipped} already-installed${r.backupPath ? ` (backup: ${r.backupPath})` : ''}`;
|
|
54
|
+
}
|
|
29
55
|
const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../package.json');
|
|
30
56
|
const packageRoot = path.dirname(packageJsonPath);
|
|
31
57
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
32
58
|
const program = new Command();
|
|
33
59
|
program
|
|
34
60
|
.name('memesh')
|
|
35
|
-
.description('MeMesh —
|
|
61
|
+
.description('MeMesh — Agentic memory for coding agents')
|
|
36
62
|
.version(pkg.version)
|
|
37
63
|
.allowExcessArguments(true)
|
|
38
64
|
.showSuggestionAfterError(true);
|
|
@@ -42,6 +68,7 @@ program
|
|
|
42
68
|
.description('Store knowledge as an entity (use flags for explicit form, or positional text for quick capture)')
|
|
43
69
|
.option('--name <name>', 'Entity name')
|
|
44
70
|
.option('--type <type>', 'Entity type')
|
|
71
|
+
.option('--title <title>', 'Short human-readable label shown as the headline (name stays the stable machine key)')
|
|
45
72
|
.option('--obs <observations...>', 'Observations (space-separated)')
|
|
46
73
|
.option('--tags <tags...>', 'Tags (space-separated)')
|
|
47
74
|
.option('--namespace <namespace>', 'Namespace: personal, team, or global. On a NEW memory this places it (default personal); on one that already exists it MOVES it out of the scope it is in — omit the flag to leave it alone.')
|
|
@@ -60,6 +87,8 @@ program
|
|
|
60
87
|
const suffix = randomBytes(3).toString('hex');
|
|
61
88
|
opts.name = `quick-${date}-${slug || 'note'}-${suffix}`;
|
|
62
89
|
opts.type = 'note';
|
|
90
|
+
if (!opts.title)
|
|
91
|
+
opts.title = String(text).slice(0, 200);
|
|
63
92
|
if (!opts.obs || opts.obs.length === 0)
|
|
64
93
|
opts.obs = [String(text)];
|
|
65
94
|
else
|
|
@@ -85,6 +114,7 @@ program
|
|
|
85
114
|
const result = remember({
|
|
86
115
|
name: opts.name,
|
|
87
116
|
type: opts.type,
|
|
117
|
+
title: opts.title,
|
|
88
118
|
observations: opts.obs,
|
|
89
119
|
tags: opts.tags,
|
|
90
120
|
namespace: opts.namespace,
|
|
@@ -129,7 +159,7 @@ program
|
|
|
129
159
|
.action(async (query, opts) => {
|
|
130
160
|
requireOneOf(opts.namespace, NAMESPACES, '--namespace');
|
|
131
161
|
await withDatabase(async () => {
|
|
132
|
-
const { entities, conflicts } = await recallWithConflicts({
|
|
162
|
+
const { entities, conflicts, retrieval } = await recallWithConflicts({
|
|
133
163
|
query: query || undefined,
|
|
134
164
|
tag: opts.tag,
|
|
135
165
|
limit: parseInt(opts.limit),
|
|
@@ -138,12 +168,7 @@ program
|
|
|
138
168
|
cross_project: opts.crossProject,
|
|
139
169
|
});
|
|
140
170
|
if (opts.json) {
|
|
141
|
-
|
|
142
|
-
console.log(JSON.stringify({ entities, conflicts }));
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
console.log(JSON.stringify(entities));
|
|
146
|
-
}
|
|
171
|
+
console.log(JSON.stringify(conflicts.length > 0 ? { entities, retrieval, conflicts } : { entities, retrieval }));
|
|
147
172
|
}
|
|
148
173
|
else if (entities.length === 0) {
|
|
149
174
|
console.log('No results found.');
|
|
@@ -173,7 +198,11 @@ program
|
|
|
173
198
|
console.log(` ... +${e.observations.length - 3} more`);
|
|
174
199
|
}
|
|
175
200
|
}
|
|
176
|
-
|
|
201
|
+
const truncatedNote = retrieval.truncated ? ' (limit reached — more may exist)' : '';
|
|
202
|
+
console.log(`\n${entities.length} result(s)${truncatedNote}`);
|
|
203
|
+
if (retrieval.degraded) {
|
|
204
|
+
console.log('Warning: semantic search unavailable right now — keyword-only results (degraded). Run `memesh doctor` to see why.');
|
|
205
|
+
}
|
|
177
206
|
if (conflicts.length > 0) {
|
|
178
207
|
console.log('\nWarning: Conflicts detected:');
|
|
179
208
|
for (const c of conflicts) {
|
|
@@ -400,6 +429,242 @@ program
|
|
|
400
429
|
}
|
|
401
430
|
});
|
|
402
431
|
});
|
|
432
|
+
program
|
|
433
|
+
.command('briefing')
|
|
434
|
+
.description('The assembled work topology for a project — task state, decisions, lessons, knowledge, recent activity')
|
|
435
|
+
.option('--project <name>', 'Project name (default: the current directory’s project)')
|
|
436
|
+
.option('--json', 'Output as JSON')
|
|
437
|
+
.action(async (opts) => {
|
|
438
|
+
await withDatabase(() => {
|
|
439
|
+
const result = assembleBriefing(opts.project);
|
|
440
|
+
if (opts.json) {
|
|
441
|
+
console.log(JSON.stringify(result));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!result.text) {
|
|
445
|
+
console.log(`No memories for "${result.project}" yet.\n` +
|
|
446
|
+
`Capture happens automatically as you work; or set the task state: memesh task --goal "…"`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
console.log(result.text);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
const WHY_ABSTENTION_TEXT = {
|
|
453
|
+
git_unavailable: 'git is not installed or not on PATH — commit attribution unavailable.',
|
|
454
|
+
not_a_git_repo: 'Not inside a git repository — commit attribution unavailable.',
|
|
455
|
+
file_not_tracked: 'File is not tracked by git — commit attribution unavailable.',
|
|
456
|
+
history_unreadable: "git could not read this file's history (too much output, too slow, or the repository has no commits yet) — nothing is listed because the question went unanswered, not because no commit touched the file.",
|
|
457
|
+
no_commits_supplied: 'No commit hashes were supplied — only the file-tag half of this answer ran.',
|
|
458
|
+
line_out_of_range: 'That line does not exist in the tracked file.',
|
|
459
|
+
line_uncommitted: 'That line is not committed yet — nothing to attribute.',
|
|
460
|
+
no_commit_entity: 'memesh has no memory of this commit (it predates capture, or was made without hooks / on another machine).',
|
|
461
|
+
no_session_link: 'This commit was captured before commits recorded their session — the session link does not exist.',
|
|
462
|
+
};
|
|
463
|
+
program
|
|
464
|
+
.command('why')
|
|
465
|
+
.description('Explain a file: commits memesh remembers touching it, their sessions, and related memories')
|
|
466
|
+
.argument('<file>', 'File path (relative to the current directory or absolute)')
|
|
467
|
+
.option('--line <n>', 'Attribute one line via git blame instead of file history')
|
|
468
|
+
.option('--limit <n>', 'Max commits to inspect', '10')
|
|
469
|
+
.option('--json', 'Output as JSON')
|
|
470
|
+
.action(async (file, opts) => {
|
|
471
|
+
await withDatabase(async () => {
|
|
472
|
+
const { resolveFileCommits, explainCommits } = await import('../../core/why.js');
|
|
473
|
+
const cwd = process.cwd();
|
|
474
|
+
const limit = parseInt(opts.limit, 10);
|
|
475
|
+
const line = opts.line !== undefined ? parseInt(opts.line, 10) : undefined;
|
|
476
|
+
for (const [flag, value] of [['--limit', limit], ['--line', line]]) {
|
|
477
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1)) {
|
|
478
|
+
console.error(`Error: ${flag} needs a whole number of 1 or more.`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const resolved = resolveFileCommits(cwd, file, { line, limit });
|
|
483
|
+
const result = explainCommits(getDatabase(), {
|
|
484
|
+
file,
|
|
485
|
+
commits: resolved.commits,
|
|
486
|
+
project: getProjectName(cwd),
|
|
487
|
+
limit,
|
|
488
|
+
abstentions: resolved.abstention ? [resolved.abstention] : [],
|
|
489
|
+
});
|
|
490
|
+
if (opts.json) {
|
|
491
|
+
console.log(JSON.stringify(result, null, 2));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
console.log(`why ${result.file} (project: ${result.project ?? 'all'})`);
|
|
495
|
+
for (const code of result.abstentions) {
|
|
496
|
+
console.log(` ! ${WHY_ABSTENTION_TEXT[code] ?? code}`);
|
|
497
|
+
}
|
|
498
|
+
if (result.commits.length === 0 && result.abstentions.length === 0) {
|
|
499
|
+
console.log(' No commits touch this file.');
|
|
500
|
+
}
|
|
501
|
+
for (const c of result.commits) {
|
|
502
|
+
const head = [c.commit.hash.slice(0, 10), c.commit.date, c.commit.subject].filter(Boolean).join(' ');
|
|
503
|
+
console.log(`\n ${head}`);
|
|
504
|
+
if (c.entity) {
|
|
505
|
+
console.log(` memory: ${c.entity.name} [mem:${c.entity.id}]`);
|
|
506
|
+
for (const obs of c.entity.observations.slice(0, 3))
|
|
507
|
+
console.log(` - ${obs}`);
|
|
508
|
+
}
|
|
509
|
+
if (c.session) {
|
|
510
|
+
console.log(` session: ${c.session.session_id}`);
|
|
511
|
+
for (const s of c.session.entities)
|
|
512
|
+
console.log(` - ${s.name} (${s.type})`);
|
|
513
|
+
if (c.session.truncated) {
|
|
514
|
+
console.log(` … first ${c.session.entities.length} of a larger session — more exist`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const code of c.abstentions) {
|
|
518
|
+
console.log(` ! ${WHY_ABSTENTION_TEXT[code] ?? code}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const fm = result.file_memories.entities;
|
|
522
|
+
if (fm.length > 0) {
|
|
523
|
+
console.log(`\n Related by file tag (file:${result.basename} — associated, not commit-derived):`);
|
|
524
|
+
for (const e of fm)
|
|
525
|
+
console.log(` - ${e.name} (${e.type})${e.title ? `: ${e.title}` : ''}`);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
program
|
|
530
|
+
.command('setup')
|
|
531
|
+
.description('Detect Claude Code / Codex / Gemini on this machine, wire memesh into each, and verify')
|
|
532
|
+
.option('--check', 'Only report wiring status per host; change nothing (exit 1 if a present host is unwired)')
|
|
533
|
+
.option('--yes', 'Apply every wiring action without asking')
|
|
534
|
+
.action(async (opts) => {
|
|
535
|
+
const { spawnSync, execFileSync } = await import('child_process');
|
|
536
|
+
const runSeam = (cmd, args) => {
|
|
537
|
+
try {
|
|
538
|
+
let target = cmd;
|
|
539
|
+
let useShell = false;
|
|
540
|
+
if (process.platform === 'win32') {
|
|
541
|
+
const resolved = execFileSync('where', [cmd], { encoding: 'utf8' }).split(/\r?\n/)[0]?.trim();
|
|
542
|
+
if (resolved) {
|
|
543
|
+
target = resolved;
|
|
544
|
+
useShell = /\.(cmd|bat)$/i.test(resolved);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const r = spawnSync(target, args, { encoding: 'utf8', shell: useShell });
|
|
548
|
+
return { status: r.status, stderr: r.stderr ?? '' };
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
return { status: null, stderr: err instanceof Error ? err.message : String(err) };
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const seams = { home: () => homeDir(), isOnPath, run: runSeam };
|
|
555
|
+
const render = (statuses) => {
|
|
556
|
+
for (const st of statuses) {
|
|
557
|
+
if (!st.present) {
|
|
558
|
+
console.log(` ${st.title}: not found (looked for ${st.presenceDetail}) — skipped`);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const mark = st.wired === true ? '✅' : st.wired === false ? '❌' : '❓';
|
|
562
|
+
console.log(`${mark} ${st.title}: ${st.wiredDetail}`);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
let statuses = inspectHosts(seams);
|
|
566
|
+
if (opts.check) {
|
|
567
|
+
render(statuses);
|
|
568
|
+
process.exit(allWired(statuses) ? 0 : 1);
|
|
569
|
+
}
|
|
570
|
+
const pending = statuses.filter((st) => st.present && st.actions.length > 0);
|
|
571
|
+
if (pending.length === 0) {
|
|
572
|
+
render(statuses);
|
|
573
|
+
console.log(allWired(statuses)
|
|
574
|
+
? '\nEverything present is wired. Nothing to do.'
|
|
575
|
+
: '\nNothing to wire automatically — see the lines above.');
|
|
576
|
+
process.exit(allWired(statuses) ? 0 : 1);
|
|
577
|
+
}
|
|
578
|
+
render(statuses);
|
|
579
|
+
console.log('\nPlanned actions:');
|
|
580
|
+
for (const st of pending)
|
|
581
|
+
for (const a of st.actions) {
|
|
582
|
+
console.log(` • [${st.title}] ${a.label}${a.cmd ? `\n ${a.cmd} ${(a.args ?? []).join(' ')}` : ''}`);
|
|
583
|
+
}
|
|
584
|
+
if (!opts.yes && !process.stdin.isTTY) {
|
|
585
|
+
console.error('\nNot a terminal and --yes not given — nothing was changed. Re-run with: memesh setup --yes');
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
const confirmAll = Boolean(opts.yes);
|
|
589
|
+
let rl = null;
|
|
590
|
+
if (!confirmAll) {
|
|
591
|
+
const { createInterface } = await import('node:readline/promises');
|
|
592
|
+
rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
593
|
+
}
|
|
594
|
+
let failed = false;
|
|
595
|
+
for (const st of pending) {
|
|
596
|
+
for (const action of st.actions) {
|
|
597
|
+
if (!confirmAll && rl) {
|
|
598
|
+
const answer = (await rl.question(`\n[${st.title}] ${action.label} — proceed? [y/N] `)).trim().toLowerCase();
|
|
599
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
600
|
+
console.log(' skipped');
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (action.kind === 'install-hooks') {
|
|
605
|
+
console.log(` ✅ ${wireUserHooks()}`);
|
|
606
|
+
}
|
|
607
|
+
else if (action.cmd) {
|
|
608
|
+
const r = runSeam(action.cmd, action.args ?? []);
|
|
609
|
+
if (r.status === 0)
|
|
610
|
+
console.log(` ✅ done (${action.cmd} ${(action.args ?? []).join(' ')})`);
|
|
611
|
+
else {
|
|
612
|
+
console.error(` ❌ ${action.cmd} exited ${r.status ?? 'without running'}${r.stderr ? `: ${r.stderr.trim()}` : ''}`);
|
|
613
|
+
failed = true;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
rl?.close();
|
|
619
|
+
console.log('\nAfter wiring:');
|
|
620
|
+
statuses = inspectHosts(seams);
|
|
621
|
+
render(statuses);
|
|
622
|
+
process.exit(failed || !allWired(statuses) ? 1 : 0);
|
|
623
|
+
});
|
|
624
|
+
program
|
|
625
|
+
.command('task')
|
|
626
|
+
.description('Show or update where the work stands on this project')
|
|
627
|
+
.option('--project <name>', 'Project name (default: the current directory’s project)')
|
|
628
|
+
.option('--goal <text>', 'What this work is FOR — the outcome being aimed at')
|
|
629
|
+
.option('--next <text>', 'The next concrete step')
|
|
630
|
+
.option('--blocked <text>', 'What is standing in the way (pass "" to clear it once resolved)')
|
|
631
|
+
.option('--done <text>', 'What was just finished')
|
|
632
|
+
.option('--json', 'Output as JSON')
|
|
633
|
+
.action(async (opts) => {
|
|
634
|
+
await withDatabase(() => {
|
|
635
|
+
const patch = {};
|
|
636
|
+
for (const field of TASK_STATE_FIELDS) {
|
|
637
|
+
if (opts[field] !== undefined)
|
|
638
|
+
patch[field] = opts[field];
|
|
639
|
+
}
|
|
640
|
+
if (Object.keys(patch).length === 0) {
|
|
641
|
+
const { project, state } = getTaskState(opts.project);
|
|
642
|
+
if (opts.json) {
|
|
643
|
+
console.log(JSON.stringify({ project, state }));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const lines = taskStateLines(state, project);
|
|
647
|
+
if (lines.length === 0) {
|
|
648
|
+
console.log(`Nothing recorded for "${project}" yet.\n` +
|
|
649
|
+
`Set it with: memesh task --goal "…" --next "…"`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
console.log(lines.join('\n'));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const result = setTaskState({ project: opts.project, patch, sourceHost: 'cli' });
|
|
656
|
+
if (opts.json) {
|
|
657
|
+
console.log(JSON.stringify(result));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (result.changed.length === 0) {
|
|
661
|
+
console.log(`No change — "${result.project}" already said exactly that.`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
console.log(`Updated ${result.changed.join(', ')} for "${result.project}".`);
|
|
665
|
+
console.log(taskStateLines(result.state, result.project).join('\n'));
|
|
666
|
+
});
|
|
667
|
+
});
|
|
403
668
|
const configCmd = program.command('config').description('Manage configuration');
|
|
404
669
|
configCmd
|
|
405
670
|
.command('list')
|
|
@@ -426,12 +691,12 @@ const ALLOWED_KEYS = new Set([
|
|
|
426
691
|
'llm.apiKey',
|
|
427
692
|
'llm.model',
|
|
428
693
|
'embedder.provider',
|
|
429
|
-
'embedder.model',
|
|
430
694
|
'autoUpdate',
|
|
431
695
|
'sessionLimit',
|
|
432
696
|
'autoCapture',
|
|
433
697
|
'llmFallbacks',
|
|
434
698
|
'language',
|
|
699
|
+
'transcriptMining',
|
|
435
700
|
]);
|
|
436
701
|
const KEY_VALIDATORS = {
|
|
437
702
|
'llm.provider': (v) => ['anthropic', 'openai', 'ollama'].includes(v) ? null : `must be one of: anthropic, openai, ollama`,
|
|
@@ -498,7 +763,7 @@ function formatConfigValue(key, raw) {
|
|
|
498
763
|
return JSON.stringify(raw);
|
|
499
764
|
return String(raw);
|
|
500
765
|
}
|
|
501
|
-
|
|
766
|
+
function buildConfigListing(config) {
|
|
502
767
|
const rows = [];
|
|
503
768
|
for (const key of Array.from(ALLOWED_KEYS).sort()) {
|
|
504
769
|
const raw = getNested(config, key.split('.'));
|
|
@@ -555,6 +820,9 @@ configCmd
|
|
|
555
820
|
if (canonical === 'autoCapture') {
|
|
556
821
|
coerced = value === 'true' || value === '1';
|
|
557
822
|
}
|
|
823
|
+
if (canonical === 'transcriptMining') {
|
|
824
|
+
coerced = value === 'true' || value === '1';
|
|
825
|
+
}
|
|
558
826
|
const config = readConfig();
|
|
559
827
|
setNested(config, canonical.split('.'), coerced);
|
|
560
828
|
writeConfig(config);
|
|
@@ -680,6 +948,53 @@ program
|
|
|
680
948
|
process.exit(1);
|
|
681
949
|
}
|
|
682
950
|
});
|
|
951
|
+
program
|
|
952
|
+
.command('upgrade-plugin')
|
|
953
|
+
.description('Upgrade the Claude Code plugin install (finds and runs its bundled upgrade script)')
|
|
954
|
+
.action(async () => {
|
|
955
|
+
const { spawnSync } = await import('child_process');
|
|
956
|
+
const cacheRoot = path.join(homeDir(), '.claude', 'plugins', 'cache', 'pcircle-memesh', 'memesh');
|
|
957
|
+
let versions = [];
|
|
958
|
+
try {
|
|
959
|
+
versions = fs.readdirSync(cacheRoot, { withFileTypes: true })
|
|
960
|
+
.filter((entry) => entry.isDirectory() && /^\d+\.\d+\.\d+/.test(entry.name))
|
|
961
|
+
.map((entry) => entry.name);
|
|
962
|
+
}
|
|
963
|
+
catch { }
|
|
964
|
+
if (versions.length === 0) {
|
|
965
|
+
console.error('No Claude Code plugin install found (looked in ~/.claude/plugins/cache/pcircle-memesh).');
|
|
966
|
+
console.error('If you installed via npm, upgrade with: memesh update');
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
versions.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
970
|
+
const newest = versions[versions.length - 1];
|
|
971
|
+
const script = path.join(cacheRoot, newest, 'scripts', 'upgrade-plugin.sh');
|
|
972
|
+
if (!fs.existsSync(script)) {
|
|
973
|
+
console.error(`Plugin install found (v${newest}), but it has no scripts/upgrade-plugin.sh — plugin versions before 4.2.5 shipped without it.`);
|
|
974
|
+
console.error('Reinstall once from the Claude Code /plugin UI, or run the npm-global copy directly:');
|
|
975
|
+
console.error(' bash "$(npm prefix -g)/lib/node_modules/@pcircle/memesh/scripts/upgrade-plugin.sh"');
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
const installHints = {
|
|
979
|
+
node: 'node is required by the upgrade script. Install Node.js from https://nodejs.org',
|
|
980
|
+
npm: 'npm is required by the upgrade script. It ships with Node.js — reinstall from https://nodejs.org',
|
|
981
|
+
rsync: 'rsync is required by the upgrade script. macOS: already installed; Debian/Ubuntu: sudo apt install rsync',
|
|
982
|
+
};
|
|
983
|
+
const missing = Object.keys(installHints).filter((tool) => !isOnPath(tool));
|
|
984
|
+
if (missing.length > 0) {
|
|
985
|
+
for (const tool of missing)
|
|
986
|
+
console.error(installHints[tool]);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
const run = spawnSync('bash', [script], { stdio: 'inherit' });
|
|
990
|
+
if (run.error) {
|
|
991
|
+
console.error(`Could not run the upgrade script: ${run.error.message}`);
|
|
992
|
+
console.error('bash is required to run it. If bash is available under another name, run it yourself:');
|
|
993
|
+
console.error(` bash ${script}`);
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
process.exit(run.status ?? 1);
|
|
997
|
+
});
|
|
683
998
|
program
|
|
684
999
|
.command('telemetry')
|
|
685
1000
|
.description('Show LLM call telemetry (per-flow scorecard for the last N days)')
|
|
@@ -765,7 +1080,8 @@ kgCmd
|
|
|
765
1080
|
.option('--session-cooccurrence', 'Rule 3: link high-signal orphans co-created in the same session')
|
|
766
1081
|
.option('--name-tokens', 'Rule 4: link orphans sharing ≥3 name content tokens (or Jaccard ≥ 0.50)')
|
|
767
1082
|
.option('--min-jaccard <n>', 'Jaccard threshold for name similarity (default 0.50)', parseFloat)
|
|
768
|
-
.option('--all-rules', 'Enable all heuristic rules (Rules 1–
|
|
1083
|
+
.option('--all-rules', 'Enable all heuristic rules (Rules 1–5)')
|
|
1084
|
+
.option('--no-evidence-links', 'Disable Rule 5: evidence → work-item links via shared session id (on by default — these edges feed the graph\'s evidence badges)')
|
|
769
1085
|
.option('--reset-idempotency', 'Clear the persistent "already-attempted" orphan cache before running (use after schema changes or to reconsider every orphan)')
|
|
770
1086
|
.option('--json', 'Output as JSON')
|
|
771
1087
|
.action(async (opts) => {
|
|
@@ -780,6 +1096,7 @@ kgCmd
|
|
|
780
1096
|
dryRun: !!opts.dryRun,
|
|
781
1097
|
includeSessionCooccurrence: allRules || !!opts.sessionCooccurrence,
|
|
782
1098
|
includeNameTokenSimilarity: allRules || !!opts.nameTokens,
|
|
1099
|
+
includeEvidenceLinks: opts.evidenceLinks !== false,
|
|
783
1100
|
minNameJaccard: opts.minJaccard,
|
|
784
1101
|
resetIdempotency: !!opts.resetIdempotency,
|
|
785
1102
|
};
|
|
@@ -819,6 +1136,7 @@ kgCmd
|
|
|
819
1136
|
console.log(` project clustering: ${result.byRule.projectClustering}`);
|
|
820
1137
|
console.log(` session co-occurrence: ${result.byRule.sessionCooccurrence}`);
|
|
821
1138
|
console.log(` name token similarity: ${result.byRule.nameTokenSimilarity}`);
|
|
1139
|
+
console.log(` evidence links: ${result.byRule.evidenceLinks}`);
|
|
822
1140
|
if (result.candidatesProposed > result.edgesWritten) {
|
|
823
1141
|
console.log(` (${result.candidatesProposed - result.edgesWritten} candidates were already-existing edges; INSERT OR IGNORE skipped them.)`);
|
|
824
1142
|
}
|
|
@@ -908,15 +1226,82 @@ program
|
|
|
908
1226
|
.option('--probe-http', 'Also probe the local HTTP server health endpoint')
|
|
909
1227
|
.option('--probe', 'Make one small live call to the configured LLM to confirm it actually answers')
|
|
910
1228
|
.option('--url <url>', 'Base URL for --probe-http', 'http://127.0.0.1:3737')
|
|
1229
|
+
.option('--fix', 'Apply the whitelisted fixes doctor prescribes (asks per fix; --yes skips asking)')
|
|
1230
|
+
.option('--yes', 'With --fix: apply without asking')
|
|
911
1231
|
.action(async (opts) => {
|
|
912
1232
|
const { formatDoctorReport, runDoctor } = await import('../../core/doctor.js');
|
|
913
|
-
|
|
1233
|
+
let result = await runDoctor({
|
|
914
1234
|
packageRoot,
|
|
915
1235
|
packageVersion: pkg.version,
|
|
916
1236
|
probeHttp: opts.probeHttp,
|
|
917
1237
|
probeCapabilities: opts.probe,
|
|
918
1238
|
httpBaseUrl: opts.url,
|
|
919
1239
|
});
|
|
1240
|
+
if (opts.fix) {
|
|
1241
|
+
const FIX_ACTIONS = {
|
|
1242
|
+
'install-hooks': wireUserHooks,
|
|
1243
|
+
'fts-rebuild': () => {
|
|
1244
|
+
openDatabase();
|
|
1245
|
+
try {
|
|
1246
|
+
return `keyword index rebuilt (${reindexFts().entities} entities)`;
|
|
1247
|
+
}
|
|
1248
|
+
finally {
|
|
1249
|
+
closeDatabase();
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
'chmod-db': () => {
|
|
1253
|
+
fs.chmodSync(getDbPath(), 0o600);
|
|
1254
|
+
return `permissions restored: chmod 600 ${getDbPath()}`;
|
|
1255
|
+
},
|
|
1256
|
+
};
|
|
1257
|
+
const fixable = result.checks.filter((c) => c.fixId && (c.status === 'warn' || c.status === 'fail'));
|
|
1258
|
+
if (fixable.length === 0) {
|
|
1259
|
+
console.log('Nothing on the --fix whitelist to apply.');
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
if (!opts.yes && !process.stdin.isTTY) {
|
|
1263
|
+
console.error('Not a terminal and --yes not given — nothing was changed. Re-run with: memesh doctor --fix --yes');
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
let rl = null;
|
|
1267
|
+
if (!opts.yes) {
|
|
1268
|
+
const { createInterface } = await import('node:readline/promises');
|
|
1269
|
+
rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1270
|
+
}
|
|
1271
|
+
for (const check of fixable) {
|
|
1272
|
+
console.log(`\n${check.label}: ${check.summary}`);
|
|
1273
|
+
if (rl) {
|
|
1274
|
+
const answer = (await rl.question(`Apply fix (${check.fixId})? [y/N] `)).trim().toLowerCase();
|
|
1275
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
1276
|
+
console.log(' skipped');
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
console.log(` ✅ ${FIX_ACTIONS[check.fixId]()}`);
|
|
1282
|
+
}
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
console.error(` ❌ ${err instanceof Error ? err.message : String(err)}`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
rl?.close();
|
|
1288
|
+
console.log('\nAfter fixes:');
|
|
1289
|
+
const before = new Map(result.checks.map((c) => [c.id, c.status]));
|
|
1290
|
+
const fixedIds = new Set(fixable.map((c) => c.id));
|
|
1291
|
+
result = await runDoctor({
|
|
1292
|
+
packageRoot,
|
|
1293
|
+
packageVersion: pkg.version,
|
|
1294
|
+
probeHttp: false,
|
|
1295
|
+
probeCapabilities: false,
|
|
1296
|
+
httpBaseUrl: opts.url,
|
|
1297
|
+
});
|
|
1298
|
+
for (const c of result.checks) {
|
|
1299
|
+
const was = before.get(c.id);
|
|
1300
|
+
if (fixedIds.has(c.id) && was && was !== c.status)
|
|
1301
|
+
console.log(` ${c.label}: ${was} → ${c.status}`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
920
1305
|
if (opts.json) {
|
|
921
1306
|
console.log(JSON.stringify(result, null, 2));
|
|
922
1307
|
}
|
|
@@ -1151,7 +1536,8 @@ dreamCmd
|
|
|
1151
1536
|
console.log(`${proposals.length} ${opts.status} proposal(s):`);
|
|
1152
1537
|
console.log('');
|
|
1153
1538
|
for (const p of proposals) {
|
|
1154
|
-
const srcLabel = p.
|
|
1539
|
+
const srcLabel = p.kind === 'relation' ? ' (conflict)'
|
|
1540
|
+
: p.source_kind === 'transcript' ? ' (transcript)' : '';
|
|
1155
1541
|
console.log(` #${p.id} [${p.project}/${p.cluster_key}]${srcLabel} ${p.source_count} source(s) → "${p.digest_name}"`);
|
|
1156
1542
|
if (p.digest_observations_preview !== null) {
|
|
1157
1543
|
console.log(` ${p.digest_observations_preview}`);
|
|
@@ -1162,6 +1548,49 @@ dreamCmd
|
|
|
1162
1548
|
console.log(`Inspect: memesh dream show <id> | Apply: memesh dream accept <id> | Reject: memesh dream reject <id>`);
|
|
1163
1549
|
});
|
|
1164
1550
|
});
|
|
1551
|
+
dreamCmd
|
|
1552
|
+
.command('conflicts')
|
|
1553
|
+
.description('Judge semantically-close memory pairs for contradiction / supersession / duplication (LLM) and stage relation proposals for review')
|
|
1554
|
+
.option('--max-pairs <n>', 'Judge at most N of the tightest candidate pairs this run (default 20)', (v) => parseInt(v, 10))
|
|
1555
|
+
.option('--dry-run', 'Show how many candidates are queued without calling an LLM or writing anything')
|
|
1556
|
+
.action(async (opts) => {
|
|
1557
|
+
await withDatabase(async () => {
|
|
1558
|
+
const cfg = readConfig();
|
|
1559
|
+
const llm = detectCapabilities().llm;
|
|
1560
|
+
if (!llm) {
|
|
1561
|
+
console.error('No LLM configured. Run `memesh config set llm.provider <anthropic|openai|ollama>` first (or set ANTHROPIC_API_KEY / OPENAI_API_KEY).');
|
|
1562
|
+
console.error('LLM is required for `memesh dream conflicts` because "do these two entries disagree" is a semantic judgement, not a rule.');
|
|
1563
|
+
process.exit(1);
|
|
1564
|
+
}
|
|
1565
|
+
const { judgeConflicts, CONFLICT_JUDGE_MAX_PAIRS } = await import('../../core/conflict-judge.js');
|
|
1566
|
+
const { getDatabase } = await import('../../db.js');
|
|
1567
|
+
const result = await judgeConflicts(getDatabase(), llm, {
|
|
1568
|
+
maxPairs: typeof opts.maxPairs === 'number' && !Number.isNaN(opts.maxPairs) ? opts.maxPairs : CONFLICT_JUDGE_MAX_PAIRS,
|
|
1569
|
+
dryRun: !!opts.dryRun,
|
|
1570
|
+
fallbacks: cfg.llmFallbacks,
|
|
1571
|
+
});
|
|
1572
|
+
console.log(`${opts.dryRun ? '[dry-run] ' : ''}Conflict pass complete in ${result.durationMs}ms`);
|
|
1573
|
+
console.log(` candidate pairs available: ${result.candidatesAvailable}`);
|
|
1574
|
+
if (opts.dryRun) {
|
|
1575
|
+
console.log(' (dry run — no LLM called, nothing judged or written)');
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
console.log(` LLM calls: ${result.llmCalls}`);
|
|
1579
|
+
console.log(` judged: ${result.judged} (${result.unrelated} unrelated, remembered so they are never re-bought)`);
|
|
1580
|
+
console.log(` staged: ${result.staged} relation proposal(s)`);
|
|
1581
|
+
if (result.llmFailures > 0) {
|
|
1582
|
+
console.log(` failed: ${result.llmFailures} (unparseable or errored LLM responses; those pairs stay at the head of the candidate list and are retried next run)`);
|
|
1583
|
+
}
|
|
1584
|
+
if (result.aborted) {
|
|
1585
|
+
console.error(` ABORTED after the work above: ${result.aborted}`);
|
|
1586
|
+
process.exit(1);
|
|
1587
|
+
}
|
|
1588
|
+
if (result.staged > 0) {
|
|
1589
|
+
console.log('');
|
|
1590
|
+
console.log('Review: memesh dream list | Apply: memesh dream accept <id> | Reject: memesh dream reject <id>');
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1165
1594
|
dreamCmd
|
|
1166
1595
|
.command('show <id>')
|
|
1167
1596
|
.description('Show a proposal in full — name, type, ALL observations, tags, source — so you can review the whole thing before accepting')
|
|
@@ -1183,6 +1612,31 @@ dreamCmd
|
|
|
1183
1612
|
console.log(`Proposal #${detail.id} [${detail.project}/${detail.cluster_key}] source: ${detail.source_kind} status: ${detail.status}`);
|
|
1184
1613
|
console.log(`created: ${detail.created_at}`);
|
|
1185
1614
|
console.log('');
|
|
1615
|
+
if (detail.kind === 'relation') {
|
|
1616
|
+
const rel = detail.relation;
|
|
1617
|
+
if (!rel) {
|
|
1618
|
+
console.error('relation payload is corrupt — reject this proposal');
|
|
1619
|
+
process.exit(1);
|
|
1620
|
+
}
|
|
1621
|
+
const [fromE, toE] = rel.relation_type === 'supersedes' && rel.direction === 'b_supersedes_a' ? [rel.b, rel.a] : [rel.a, rel.b];
|
|
1622
|
+
console.log(`verdict: ${rel.verdict} (severity: ${rel.severity ?? 'unknown'})`);
|
|
1623
|
+
console.log(`accepting creates: ${fromE?.name} —${rel.relation_type}→ ${toE?.name}`);
|
|
1624
|
+
if (rel.relation_type === 'supersedes') {
|
|
1625
|
+
console.log(` survivor: ${fromE?.name} (the arrow points from the surviving claim to the obsolete one)`);
|
|
1626
|
+
}
|
|
1627
|
+
console.log(`rationale: ${rel.rationale ?? '(none given)'}`);
|
|
1628
|
+
if (rel.recommended_action)
|
|
1629
|
+
console.log(`recommended action: ${rel.recommended_action}`);
|
|
1630
|
+
if (rel.excerpts?.a || rel.excerpts?.b) {
|
|
1631
|
+
console.log(`excerpt A (${rel.a?.name}): ${rel.excerpts?.a ?? ''}`);
|
|
1632
|
+
console.log(`excerpt B (${rel.b?.name}): ${rel.excerpts?.b ?? ''}`);
|
|
1633
|
+
}
|
|
1634
|
+
if (typeof rel.cosine_distance === 'number')
|
|
1635
|
+
console.log(`cosine distance: ${rel.cosine_distance.toFixed(3)}`);
|
|
1636
|
+
console.log('');
|
|
1637
|
+
console.log(`Accept: memesh dream accept ${detail.id} | Reject: memesh dream reject ${detail.id}`);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1186
1640
|
console.log(`name: ${detail.digest.name}`);
|
|
1187
1641
|
console.log(`type: ${detail.digest.type}`);
|
|
1188
1642
|
console.log(`observations (${detail.digest.observations.length}):`);
|
|
@@ -1350,7 +1804,7 @@ program
|
|
|
1350
1804
|
}
|
|
1351
1805
|
}
|
|
1352
1806
|
body = redactUserPaths(redactSecrets(body));
|
|
1353
|
-
const url = `https://github.com/PCIRCLE-AI/memesh
|
|
1807
|
+
const url = `https://github.com/PCIRCLE-AI/memesh/issues/new?title=${encodeURIComponent(`[${typeLabel}] `)}&body=${encodeURIComponent(body)}&labels=${encodeURIComponent(labels)}`;
|
|
1354
1808
|
if (opts.open === false) {
|
|
1355
1809
|
console.log(url);
|
|
1356
1810
|
return;
|
|
@@ -1383,17 +1837,60 @@ program
|
|
|
1383
1837
|
.description('Regenerate vector embeddings for all entities (--fts rebuilds the keyword index instead)')
|
|
1384
1838
|
.option('--namespace <namespace>', 'Reindex only entities in this namespace')
|
|
1385
1839
|
.option('--fts', 'Rebuild the full-text keyword index instead of the vector index')
|
|
1386
|
-
.option('--
|
|
1387
|
-
'stored embedding first. Needed only when switching embedding providers.')
|
|
1840
|
+
.option('--discard-generation', 'Throw away a half-built vector index left by an interrupted rebuild, without rebuilding')
|
|
1388
1841
|
.option('--json', 'Output as JSON')
|
|
1842
|
+
.addOption(new Option('--vectors').hideHelp())
|
|
1389
1843
|
.action(async (opts) => {
|
|
1844
|
+
if (opts.vectors) {
|
|
1845
|
+
if (opts.json) {
|
|
1846
|
+
console.log(JSON.stringify({
|
|
1847
|
+
refused: true,
|
|
1848
|
+
reason: 'the --vectors flag is retired',
|
|
1849
|
+
fix: 'run plain memesh reindex; to change provider first: memesh config set embedder.provider ollama|openai',
|
|
1850
|
+
indexTouched: false,
|
|
1851
|
+
}));
|
|
1852
|
+
process.exit(1);
|
|
1853
|
+
}
|
|
1854
|
+
console.error('`memesh reindex --vectors` has been retired.');
|
|
1855
|
+
console.error('');
|
|
1856
|
+
console.error('It existed to consent to dropping every stored embedding before a refill.');
|
|
1857
|
+
console.error('A rebuild now happens beside the live index and replaces it only when');
|
|
1858
|
+
console.error('complete, so there is nothing left to consent to.');
|
|
1859
|
+
console.error('');
|
|
1860
|
+
console.error('Run plain `memesh reindex`. To change embedding provider first:');
|
|
1861
|
+
console.error(' memesh config set embedder.provider ollama (or openai)');
|
|
1862
|
+
process.exit(1);
|
|
1863
|
+
}
|
|
1390
1864
|
requireOneOf(opts.namespace, NAMESPACES, '--namespace');
|
|
1865
|
+
if (opts.discardGeneration) {
|
|
1866
|
+
await withDatabase(async () => {
|
|
1867
|
+
const read = readVectorGeneration();
|
|
1868
|
+
const staged = generationRowIds().size;
|
|
1869
|
+
if (read.state === 'none' && staged === 0) {
|
|
1870
|
+
if (opts.json)
|
|
1871
|
+
console.log(JSON.stringify({ discarded: false, staged: 0 }));
|
|
1872
|
+
else
|
|
1873
|
+
console.log('Nothing to discard: there is no half-built vector index.');
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const describe = read.state === 'open'
|
|
1877
|
+
? `${read.info.dimension}-dim, provider ${read.info.provider}, started ${read.info.startedAt}`
|
|
1878
|
+
: read.state === 'unreadable'
|
|
1879
|
+
? `marker unreadable (${read.detail})`
|
|
1880
|
+
: 'no marker';
|
|
1881
|
+
discardVectorGeneration();
|
|
1882
|
+
if (opts.json) {
|
|
1883
|
+
console.log(JSON.stringify({ discarded: true, staged, generation: read, liveIndexTouched: false }));
|
|
1884
|
+
}
|
|
1885
|
+
else {
|
|
1886
|
+
console.log(`Discarded a half-built vector index: ${staged} staged vectors (${describe}).\n` +
|
|
1887
|
+
' Your live index was not touched. Run `memesh reindex` to build a new one.');
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1391
1892
|
try {
|
|
1392
1893
|
if (opts.fts) {
|
|
1393
|
-
if (opts.vectors) {
|
|
1394
|
-
console.error('❌ --fts and --vectors rebuild different indexes. Run them separately.');
|
|
1395
|
-
process.exit(1);
|
|
1396
|
-
}
|
|
1397
1894
|
await withDatabase(async () => {
|
|
1398
1895
|
const { entities } = reindexFts();
|
|
1399
1896
|
if (opts.json) {
|
|
@@ -1405,68 +1902,84 @@ program
|
|
|
1405
1902
|
});
|
|
1406
1903
|
return;
|
|
1407
1904
|
}
|
|
1408
|
-
if (opts.
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1905
|
+
if (!opts.namespace && !(await canRefillVectorIndex())) {
|
|
1906
|
+
const embedder = detectCapabilities().embeddings;
|
|
1907
|
+
const written = readConfig().embedder?.provider;
|
|
1908
|
+
const known = embedder === 'openai' || embedder === 'ollama';
|
|
1909
|
+
const invalid = written !== undefined && !known;
|
|
1910
|
+
const unconfigured = written === undefined && !known;
|
|
1911
|
+
const reason = invalid
|
|
1912
|
+
? `embedder.provider is set to '${written}', which is not a provider MeMesh knows`
|
|
1913
|
+
: unconfigured
|
|
1914
|
+
? 'no embedding provider is configured, so there is nothing to build vectors with'
|
|
1915
|
+
: 'could not produce a test embedding at the configured vector width';
|
|
1916
|
+
const fix = invalid || unconfigured
|
|
1917
|
+
? (invalid ? 'Set it to one MeMesh knows: ' : 'Pick one first: ') +
|
|
1918
|
+
'`memesh config set embedder.provider ollama` (local, needs `ollama serve`) ' +
|
|
1919
|
+
'or `memesh config set embedder.provider openai` (needs OPENAI_API_KEY). Until then, recall ' +
|
|
1920
|
+
'runs on keyword search, which needs no rebuild.'
|
|
1921
|
+
: 'Check that Ollama is running (or that your OpenAI API key is valid), then run this again. ' +
|
|
1922
|
+
'`memesh doctor` reports which provider is configured.';
|
|
1923
|
+
if (opts.json) {
|
|
1924
|
+
console.log(JSON.stringify({ refused: true, reason, fix, indexTouched: false }));
|
|
1416
1925
|
}
|
|
1417
|
-
|
|
1418
|
-
console.error(
|
|
1419
|
-
'
|
|
1420
|
-
|
|
1421
|
-
' Check that Ollama is running (or that your OpenAI API key is valid) —\n' +
|
|
1422
|
-
' then run this again. `memesh doctor` reports which provider is configured.');
|
|
1423
|
-
process.exit(1);
|
|
1926
|
+
else {
|
|
1927
|
+
console.error(`❌ Nothing was rebuilt: ${reason}.\n` +
|
|
1928
|
+
' Your existing index is untouched and still answering queries.\n' +
|
|
1929
|
+
` ${fix}`);
|
|
1424
1930
|
}
|
|
1931
|
+
process.exit(1);
|
|
1425
1932
|
}
|
|
1426
1933
|
await withDatabase(async () => {
|
|
1427
1934
|
const result = await reindex({ namespace: opts.namespace });
|
|
1428
|
-
const incomplete = result.missingVectors > 0
|
|
1935
|
+
const incomplete = result.missingVectors > 0
|
|
1936
|
+
|| result.failed > 0
|
|
1937
|
+
|| result.generationSwapped === false
|
|
1938
|
+
|| result.abortedAfter !== null;
|
|
1429
1939
|
if (opts.json) {
|
|
1430
1940
|
console.log(JSON.stringify(result));
|
|
1431
1941
|
}
|
|
1432
|
-
else
|
|
1433
|
-
console.log(`⚠️ Reindex incomplete:`);
|
|
1942
|
+
else {
|
|
1943
|
+
console.log(incomplete ? `⚠️ Reindex incomplete:` : `✅ Reindex complete:`);
|
|
1434
1944
|
console.log(` Processed: ${result.processed}`);
|
|
1435
1945
|
console.log(` Embedded: ${result.embedded}`);
|
|
1436
1946
|
console.log(` Skipped: ${result.skipped}`);
|
|
1437
|
-
if (
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1947
|
+
if (incomplete) {
|
|
1948
|
+
if (result.abortedAfter !== null) {
|
|
1949
|
+
console.log(` Stopped early after ${result.abortedAfter} entities: the provider failed ` +
|
|
1950
|
+
`repeatedly. Everything embedded so far is kept — run this again to continue.`);
|
|
1951
|
+
}
|
|
1952
|
+
if (result.generationSwapped === false) {
|
|
1953
|
+
console.log(` The new index was NOT switched in, so your existing index is untouched ` +
|
|
1954
|
+
`and still answering queries.`);
|
|
1955
|
+
}
|
|
1956
|
+
if (result.missingVectors > 0) {
|
|
1957
|
+
console.log(` Still without a vector: ${result.missingVectors}`);
|
|
1958
|
+
}
|
|
1959
|
+
if (result.failed > 0 && result.missingVectors === 0) {
|
|
1960
|
+
console.log(` Could not be regenerated: ${result.failed} ` +
|
|
1961
|
+
`(these still hold their previous embedding)`);
|
|
1962
|
+
}
|
|
1963
|
+
for (const [outcome, count] of Object.entries(result.outcomes)) {
|
|
1964
|
+
if (outcome !== 'stored' && count > 0)
|
|
1965
|
+
console.log(` ${outcome}: ${count}`);
|
|
1966
|
+
}
|
|
1443
1967
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1968
|
+
else if (!result.pendingReindexCleared) {
|
|
1969
|
+
console.log(` Note: ${result.missingVectorsDatabaseWide} memories in other namespaces still ` +
|
|
1970
|
+
`have no vector, so the reindex-needed flag stays set.`);
|
|
1447
1971
|
}
|
|
1448
1972
|
}
|
|
1449
|
-
else if (!result.pendingReindexCleared) {
|
|
1450
|
-
console.log(`✅ Reindex complete:`);
|
|
1451
|
-
console.log(` Processed: ${result.processed}`);
|
|
1452
|
-
console.log(` Embedded: ${result.embedded}`);
|
|
1453
|
-
console.log(` Skipped: ${result.skipped}`);
|
|
1454
|
-
console.log(` Note: ${result.missingVectorsDatabaseWide} memories in other namespaces still ` +
|
|
1455
|
-
`have no vector, so the reindex-needed flag stays set.`);
|
|
1456
|
-
}
|
|
1457
|
-
else {
|
|
1458
|
-
console.log(`✅ Reindex complete:`);
|
|
1459
|
-
console.log(` Processed: ${result.processed}`);
|
|
1460
|
-
console.log(` Embedded: ${result.embedded}`);
|
|
1461
|
-
console.log(` Skipped: ${result.skipped}`);
|
|
1462
|
-
}
|
|
1463
1973
|
if (incomplete)
|
|
1464
1974
|
process.exitCode = 1;
|
|
1465
1975
|
});
|
|
1466
1976
|
}
|
|
1467
1977
|
catch (err) {
|
|
1468
1978
|
if (err instanceof Error) {
|
|
1469
|
-
|
|
1979
|
+
if (opts.json)
|
|
1980
|
+
console.log(JSON.stringify({ refused: true, reason: err.message, indexTouched: false }));
|
|
1981
|
+
else
|
|
1982
|
+
console.error(`❌ Reindex failed: ${err.message}`);
|
|
1470
1983
|
process.exit(1);
|
|
1471
1984
|
}
|
|
1472
1985
|
throw err;
|