eklavya 1.24.2 → 1.25.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/dist/assets/dashboard.html +1012 -255
- package/dist/assets/tutor/references/focus-and-level.md +7 -0
- package/dist/claude-mem.js +9 -7
- package/dist/claude-mem.js.map +1 -1
- package/dist/cli-memory.js +786 -0
- package/dist/cli-memory.js.map +1 -0
- package/dist/cli.js +95 -773
- package/dist/cli.js.map +1 -1
- package/dist/config.js +79 -13
- package/dist/config.js.map +1 -1
- package/dist/dashboard.js +289 -15
- package/dist/dashboard.js.map +1 -1
- package/dist/db.js +33 -6
- package/dist/db.js.map +1 -1
- package/dist/hooks/capture-lib.js +55 -0
- package/dist/hooks/capture-lib.js.map +1 -0
- package/dist/hooks/capture-tool.js +1 -1
- package/dist/hooks/capture-tool.js.map +1 -1
- package/dist/hooks/commit-lib.js +255 -0
- package/dist/hooks/commit-lib.js.map +1 -0
- package/dist/hooks/lib.js +37 -5
- package/dist/hooks/lib.js.map +1 -1
- package/dist/hooks/memory-lib.js +93 -78
- package/dist/hooks/memory-lib.js.map +1 -1
- package/dist/hooks/pre-tool-gate.js +5 -10
- package/dist/hooks/pre-tool-gate.js.map +1 -1
- package/dist/hooks/prompt-submit-nudge.js +26 -1
- package/dist/hooks/prompt-submit-nudge.js.map +1 -1
- package/dist/hooks/session-start.js +13 -4
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/install.js +280 -67
- package/dist/install.js.map +1 -1
- package/dist/memory/capture.js +25 -5
- package/dist/memory/capture.js.map +1 -1
- package/dist/memory/privacy.js +105 -7
- package/dist/memory/privacy.js.map +1 -1
- package/dist/memory/provider.js +13 -3
- package/dist/memory/provider.js.map +1 -1
- package/dist/memory/recall.js +18 -6
- package/dist/memory/recall.js.map +1 -1
- package/dist/memory/spool.js +105 -23
- package/dist/memory/spool.js.map +1 -1
- package/dist/memory/store.js +32 -2
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/summarize.js +5 -5
- package/dist/memory/summarize.js.map +1 -1
- package/dist/memory/worker.js +71 -10
- package/dist/memory/worker.js.map +1 -1
- package/dist/migrate.js +60 -14
- package/dist/migrate.js.map +1 -1
- package/dist/paths.js +49 -0
- package/dist/paths.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/cli/CLAUDE.md +16 -4
- package/dist/plugin/hooks/CLAUDE.md +33 -3
- package/dist/plugin/hooks/run.mjs +69 -5
- package/dist/plugin/scripts/install-git-hook.sh +114 -24
- package/dist/plugin/skills/CLAUDE.md +1 -1
- package/dist/plugin/skills/setup/SKILL.md +9 -2
- package/dist/plugin/skills/tutor/references/focus-and-level.md +7 -0
- package/dist/safe-write.js +146 -0
- package/dist/safe-write.js.map +1 -0
- package/dist/slug.js +4 -1
- package/dist/slug.js.map +1 -1
- package/dist/srs.js +33 -1
- package/dist/srs.js.map +1 -1
- package/dist/store.js +29 -19
- package/dist/store.js.map +1 -1
- package/dist/tools/config_tools.js +23 -1
- package/dist/tools/config_tools.js.map +1 -1
- package/dist/tools/get_session_quiz_plan.js +51 -30
- package/dist/tools/get_session_quiz_plan.js.map +1 -1
- package/dist/tools/log_session_concepts.js +30 -23
- package/dist/tools/log_session_concepts.js.map +1 -1
- package/dist/tools/record_attempt.js +18 -8
- package/dist/tools/record_attempt.js.map +1 -1
- package/dist/tools/types.js +29 -0
- package/dist/tools/types.js.map +1 -1
- package/dist/tools/upsert_concepts.js +12 -10
- package/dist/tools/upsert_concepts.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +46 -21
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -7,38 +7,32 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { openDb } from './db.js';
|
|
11
10
|
import { dbPath, eklavyaHome } from './paths.js';
|
|
12
11
|
import { readStdinBounded, stripBom, STATUSLINE_STDIN } from './stdin.js';
|
|
13
|
-
import { loadConfig, writeConfigFile, readConfigFile, findRepoConfig, mainRepoRoot, migrateLegacyRepoConfig, } from './config.js';
|
|
12
|
+
import { loadConfig, writeConfigFile, readConfigFile, configFileProblem, findRepoConfig, mainRepoRoot, migrateLegacyRepoConfig, } from './config.js';
|
|
14
13
|
import { isKnownKey, knownKeys, parseValue, patchFor } from './config-path.js';
|
|
15
|
-
import {
|
|
16
|
-
import { levelStanding, projectKey } from './store.js';
|
|
14
|
+
import { levelStanding } from './store.js';
|
|
17
15
|
import { statusLine } from './statusline.js';
|
|
18
16
|
import { isSessionOff } from './session.js';
|
|
19
17
|
import { START_LEVEL } from './srs.js';
|
|
20
18
|
import Database from 'better-sqlite3';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import { backlogSummary, countEntries, discardBacklog, entryById, entryEvents, entryTags, pendingEventCount, quarantineBacklog, receiptTotals, restoreBacklog, resumePaused, timeline, } from './memory/store.js';
|
|
29
|
-
import { search } from './memory/search.js';
|
|
30
|
-
import { pull, push, syncStatus } from './memory/sync.js';
|
|
31
|
-
import { pruneEvidence, queueDepth, summarizerFor, superviseWorker } from './memory/worker.js';
|
|
32
|
-
import { replayProject, transcriptDirFor, transcriptsFor } from './memory/replay.js';
|
|
33
|
-
import { droppedCount } from './memory/spool.js';
|
|
34
|
-
import { savingsFrom, savingsLine } from './memory/tokens.js';
|
|
35
|
-
import { inventory, exportPayload, restoreExport, EXPORT_SCHEMA_VERSION, ImportError, IMPORTED_TABLES, verifyImport, } from './memory/import.js';
|
|
19
|
+
import { check, dim, heading, verdict } from './theme.js';
|
|
20
|
+
// Everything heavier is imported inside the command that needs it: the
|
|
21
|
+
// database opener (migrations, seed, packs), install, the dashboard and the
|
|
22
|
+
// whole memory half (`cli-memory.ts`). An ES import is paid whether or not the
|
|
23
|
+
// command runs, and `eklavya statusline` runs on every status-bar refresh —
|
|
24
|
+
// loading all of that there more than doubled its start-up for nothing.
|
|
25
|
+
// `test/hook-isolation.test.ts` pins it.
|
|
36
26
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
37
27
|
const USAGE = `eklavya — local learning state for agent-assisted development
|
|
38
28
|
|
|
39
29
|
Usage:
|
|
40
30
|
eklavya serve Run the MCP server on stdio (what Claude Code starts)
|
|
41
31
|
eklavya install Install Eklavya into Claude Code, runtime included
|
|
32
|
+
--memory eklavya|claude-mem when Claude Mem is installed, pick
|
|
33
|
+
which one records without being asked: eklavya imports its
|
|
34
|
+
history and retires it; claude-mem keeps it and turns Eklavya
|
|
35
|
+
memory off
|
|
42
36
|
eklavya uninstall [--purge] Remove it (--purge also deletes your learning history)
|
|
43
37
|
eklavya export-rules [--out <file>] Write the tutor pedagogy as a Cursor rules file
|
|
44
38
|
eklavya config get Show the effective configuration
|
|
@@ -72,11 +66,15 @@ Memory:
|
|
|
72
66
|
or set aside, delete or bring back the ones you select with
|
|
73
67
|
--helpers, --project <key>, --session <id> or --batch <id>
|
|
74
68
|
eklavya memory prune Delete raw evidence past memory.retention_days
|
|
75
|
-
eklavya memory import <source.db> Import a Claude Mem database [--dry-run] [--resume]
|
|
69
|
+
eklavya memory import <source.db> Import a Claude Mem database [--dry-run] [--verify] [--resume]
|
|
76
70
|
--dry-run reads the source and reports; it writes nothing
|
|
71
|
+
--verify checks every source row by id is here and where each
|
|
72
|
+
project was filed; writes nothing, exits 1 if any are missing
|
|
77
73
|
--map <source>=<path> file that source project under a checkout
|
|
78
74
|
--map-here <source> the same, for the checkout you are in
|
|
79
|
-
eklavya memory export <file>
|
|
75
|
+
eklavya memory export <file> [--force]
|
|
76
|
+
Versioned JSON of entries, tags, evidence links and receipts,
|
|
77
|
+
readable only by you. Refuses to replace a file without --force
|
|
80
78
|
eklavya memory restore <file> Read that file back in. Additive and idempotent — a second
|
|
81
79
|
restore adds nothing, and no attempt, mastery or gate row is
|
|
82
80
|
touched. Refuses a schema version it does not understand
|
|
@@ -247,6 +245,14 @@ function configCommand(args) {
|
|
|
247
245
|
process.stdout.write(`${JSON.stringify(resolved.config, null, 2)}\n`);
|
|
248
246
|
process.stdout.write(`\nglobal: ${resolved.globalPath}\n`);
|
|
249
247
|
process.stdout.write(`project: ${resolved.projectPath ?? '(none — not in a git repository)'}\n`);
|
|
248
|
+
if (resolved.ignored.length) {
|
|
249
|
+
process.stdout.write(`ignored in the project file (global-only, set them without --project): ${resolved.ignored.join(', ')}\n`);
|
|
250
|
+
}
|
|
251
|
+
// Defaults are standing in for a file that would not parse; say so, or the
|
|
252
|
+
// output above reads as the developer's settings when it is not.
|
|
253
|
+
const problem = configFileProblem();
|
|
254
|
+
if (problem)
|
|
255
|
+
process.stderr.write(`warning: ${problem}\n`);
|
|
250
256
|
return;
|
|
251
257
|
}
|
|
252
258
|
if (action !== 'set')
|
|
@@ -319,7 +325,18 @@ function safely(fn, fallback) {
|
|
|
319
325
|
return fallback;
|
|
320
326
|
}
|
|
321
327
|
}
|
|
322
|
-
function doctor() {
|
|
328
|
+
async function doctor() {
|
|
329
|
+
// Loaded here rather than at the top of the file: see the note on the imports.
|
|
330
|
+
const [{ openDb }, { applyPacks, loadPacks }, { health, commandOnPath, eklavyaGateHook }, { countEntries, pendingEventCount }, { queueDepth }, { syncStatus }, { droppedCount }, { workerLine },] = await Promise.all([
|
|
331
|
+
import('./db.js'),
|
|
332
|
+
import('./packs.js'),
|
|
333
|
+
import('./install.js'),
|
|
334
|
+
import('./memory/store.js'),
|
|
335
|
+
import('./memory/worker.js'),
|
|
336
|
+
import('./memory/sync.js'),
|
|
337
|
+
import('./memory/spool.js'),
|
|
338
|
+
import('./cli-memory.js'),
|
|
339
|
+
]);
|
|
323
340
|
const file = dbPath();
|
|
324
341
|
// `doctor` is where somebody goes when something is not taking effect, so it
|
|
325
342
|
// is the second place the legacy move runs -- a settings file still sitting
|
|
@@ -335,6 +352,9 @@ function doctor() {
|
|
|
335
352
|
// install` repairs a broken install and cannot do a thing about a paused
|
|
336
353
|
// queue. A memory failure still exits non-zero; it just names its own fix.
|
|
337
354
|
let memoryOk = true;
|
|
355
|
+
// Its own flag for the same reason: `eklavya install` does not put jq or
|
|
356
|
+
// sqlite3 on anybody's PATH, and a config file only its owner can fix.
|
|
357
|
+
let fixByHand = true;
|
|
338
358
|
add('ok', 'home', eklavyaHome());
|
|
339
359
|
// The install checks come first because they are what someone is looking for
|
|
340
360
|
// when Eklavya has gone quiet. Everything below reads fine on an install that
|
|
@@ -354,7 +374,11 @@ function doctor() {
|
|
|
354
374
|
add('ok', 'surfaces', 'Claude Code CLI and the Code tab in Claude Desktop (checked above)');
|
|
355
375
|
add('skip', 'cowork', dim('keeps a separate plugin list — install it there from Customize → Plugins;'));
|
|
356
376
|
add('skip', 'cowork', dim('this command cannot see it. Your learning history is shared either way.'));
|
|
357
|
-
|
|
377
|
+
// `openDb` below creates a missing file, so the row says what happened, not
|
|
378
|
+
// what was true a moment before it happened.
|
|
379
|
+
const dbExisted = fs.existsSync(file);
|
|
380
|
+
const dbRow = rows.length;
|
|
381
|
+
add(dbExisted ? 'ok' : 'warn', 'database', `${file}${dbExisted ? '' : dim(' (does not exist)')}`);
|
|
358
382
|
let edgesDropped = 0;
|
|
359
383
|
// Held open past the try so the memory section below can read it, and can
|
|
360
384
|
// still report when it is null -- the spool drop count is exactly the number
|
|
@@ -367,6 +391,8 @@ function doctor() {
|
|
|
367
391
|
// recovery for the edit a fingerprint cannot see (a same-size write that
|
|
368
392
|
// preserves the mtime). `doctor` is where someone goes when a pack is not
|
|
369
393
|
// taking effect, so `doctor` is what makes it take effect.
|
|
394
|
+
if (!dbExisted)
|
|
395
|
+
rows[dbRow] = ['ok', 'database', `${file} ${dim('(created now — it did not exist)')}`];
|
|
370
396
|
edgesDropped = applyPacks(db).edgesDropped;
|
|
371
397
|
const concepts = db.prepare('SELECT count(*) n FROM concepts').get().n;
|
|
372
398
|
const attempts = db.prepare('SELECT count(*) n FROM attempts').get().n;
|
|
@@ -475,6 +501,30 @@ function doctor() {
|
|
|
475
501
|
}
|
|
476
502
|
if (resolved.projectPath)
|
|
477
503
|
add('ok', 'project', resolved.projectPath);
|
|
504
|
+
// A settings file that stopped parsing is read as absent, so every dial
|
|
505
|
+
// above is a default wearing the developer's name. Only `doctor` says so.
|
|
506
|
+
const configProblem = safely(() => configFileProblem(), null);
|
|
507
|
+
if (configProblem) {
|
|
508
|
+
fixByHand = false;
|
|
509
|
+
add('fail', 'config', `FAILED — ${configProblem}`);
|
|
510
|
+
}
|
|
511
|
+
// The terminal gate (`cli/eklavya-gate`) shells out to jq and sqlite3 and
|
|
512
|
+
// fails OPEN without either: every commit goes through, and the only trace
|
|
513
|
+
// is one stderr line per commit. Installed here, that is a gate that is not
|
|
514
|
+
// gating; enforced with no terminal hook, it is a warning for later.
|
|
515
|
+
const gateHook = safely(() => eklavyaGateHook(), null);
|
|
516
|
+
if (gateHook)
|
|
517
|
+
add('ok', 'git hook', gateHook);
|
|
518
|
+
if (gateHook || q.enforced) {
|
|
519
|
+
const missing = ['jq', 'sqlite3'].filter((tool) => !commandOnPath(tool));
|
|
520
|
+
if (missing.length && gateHook) {
|
|
521
|
+
fixByHand = false;
|
|
522
|
+
add('fail', 'gate', `FAILED — ${missing.join(' and ')} not on PATH; the commit gate lets every commit through`);
|
|
523
|
+
}
|
|
524
|
+
else if (missing.length) {
|
|
525
|
+
add('warn', 'gate', `${missing.join(' and ')} not on PATH ${dim('— the terminal commit gate needs them if you install it')}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
478
528
|
// Packs, and the one place a broken one is visible. `loadPacks` never throws
|
|
479
529
|
// -- a malformed file in ~/.eklavya/packs/ makes one pack unavailable, not
|
|
480
530
|
// Eklavya -- so without this line the failure is a domain that quietly never
|
|
@@ -520,8 +570,14 @@ function doctor() {
|
|
|
520
570
|
const same = prev?.[1] === label;
|
|
521
571
|
check(same && prev[0] === mark ? null : mark, same ? '' : label, detail);
|
|
522
572
|
});
|
|
523
|
-
verdict(!ok
|
|
524
|
-
|
|
573
|
+
verdict(!ok
|
|
574
|
+
? 'Something is broken. Run: eklavya install'
|
|
575
|
+
: !memoryOk
|
|
576
|
+
? 'Memory needs attention — see above'
|
|
577
|
+
: !fixByHand
|
|
578
|
+
? 'Needs a fix by hand — see above'
|
|
579
|
+
: null, 'ALL CLEAR · Eklavya is wired up');
|
|
580
|
+
if (!ok || !memoryOk || !fixByHand)
|
|
525
581
|
process.exit(1);
|
|
526
582
|
}
|
|
527
583
|
/**
|
|
@@ -610,7 +666,7 @@ async function statuslineCommand(argv) {
|
|
|
610
666
|
/* A status bar with nothing to say says nothing. */
|
|
611
667
|
}
|
|
612
668
|
}
|
|
613
|
-
function dashboardCommand(argv) {
|
|
669
|
+
async function dashboardCommand(argv) {
|
|
614
670
|
const i = argv.indexOf('--port');
|
|
615
671
|
const port = i === -1 ? undefined : Number(argv[i + 1]);
|
|
616
672
|
if (port !== undefined && !Number.isInteger(port)) {
|
|
@@ -621,6 +677,10 @@ function dashboardCommand(argv) {
|
|
|
621
677
|
// dashboard you open once; `--no-open` is for a headless box, or for an agent
|
|
622
678
|
// that only wants the URL to hand back.
|
|
623
679
|
const open = !argv.includes('--no-open');
|
|
680
|
+
const [{ openDb }, { startDashboard, openInBrowser }] = await Promise.all([
|
|
681
|
+
import('./db.js'),
|
|
682
|
+
import('./dashboard.js'),
|
|
683
|
+
]);
|
|
624
684
|
startDashboard(openDb(), { port }).then(({ url }) => {
|
|
625
685
|
process.stdout.write(`Eklavya dashboard on ${url}\nReading ${dbPath()} — press Ctrl+C to stop.\n`);
|
|
626
686
|
if (open) {
|
|
@@ -632,749 +692,7 @@ function dashboardCommand(argv) {
|
|
|
632
692
|
process.exit(1);
|
|
633
693
|
});
|
|
634
694
|
}
|
|
635
|
-
|
|
636
|
-
* `eklavya memory <subcommand>` — the memory half, outside a session.
|
|
637
|
-
*
|
|
638
|
-
* Project-scoped by default on every read, like the retrieval layer it sits on:
|
|
639
|
-
* another repository's work is noise, and `--all-projects` is the explicit way
|
|
640
|
-
* to ask for it.
|
|
641
|
-
*/
|
|
642
|
-
const MEMORY_USAGE = 'Usage: eklavya memory status|search|timeline|show|replay|process|stop|backlog|prune|import|export|restore|sync\n' +
|
|
643
|
-
' run `eklavya --help` for the full list\n';
|
|
644
|
-
function flag(argv, name, fallback) {
|
|
645
|
-
const i = argv.indexOf(name);
|
|
646
|
-
if (i === -1)
|
|
647
|
-
return fallback;
|
|
648
|
-
const value = argv[i + 1];
|
|
649
|
-
if (value === undefined || value.startsWith('--'))
|
|
650
|
-
fail(`${name} needs a value.`);
|
|
651
|
-
return value;
|
|
652
|
-
}
|
|
653
|
-
function numberFlag(argv, name, fallback) {
|
|
654
|
-
const raw = flag(argv, name);
|
|
655
|
-
if (raw === undefined)
|
|
656
|
-
return fallback;
|
|
657
|
-
const n = Number(raw);
|
|
658
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
659
|
-
fail(`${name} needs a positive number.`);
|
|
660
|
-
return Math.floor(n);
|
|
661
|
-
}
|
|
662
|
-
/** The project key the memory tables use — the same one the hooks record under. */
|
|
663
|
-
function currentProject() {
|
|
664
|
-
return identityFor({ cwd: process.cwd(), sessionId: 'cli' }).project;
|
|
665
|
-
}
|
|
666
|
-
/**
|
|
667
|
-
* How a migration is checked: what arrived here, and what arrived under a
|
|
668
|
-
* Claude Mem project name no checkout matches -- history that only surfaces
|
|
669
|
-
* under --all-projects until it is placed.
|
|
670
|
-
*/
|
|
671
|
-
function importedLines(db, project) {
|
|
672
|
-
const rows = db
|
|
673
|
-
.prepare(`SELECT project, COUNT(*) AS n FROM memory_entries
|
|
674
|
-
WHERE import_source IS NOT NULL AND deleted_at IS NULL GROUP BY project`)
|
|
675
|
-
.all();
|
|
676
|
-
if (!rows.length)
|
|
677
|
-
return [];
|
|
678
|
-
const here = rows.find((r) => r.project === project)?.n ?? 0;
|
|
679
|
-
const bare = rows.filter((r) => !path.isAbsolute(r.project));
|
|
680
|
-
const unplaced = bare.reduce((sum, r) => sum + r.n, 0);
|
|
681
|
-
return [
|
|
682
|
-
`imported: ${here} here from Claude Mem`,
|
|
683
|
-
...(unplaced
|
|
684
|
-
? [
|
|
685
|
-
`unplaced: ${unplaced} under ${bare.length} name(s) no checkout matches — ${bare.map((r) => r.project).slice(0, 5).join(', ')}${bare.length > 5 ? ', …' : ''}`,
|
|
686
|
-
' place them: eklavya memory import ~/.claude-mem.retired/claude-mem.db [--map <name>=<checkout>]',
|
|
687
|
-
]
|
|
688
|
-
: []),
|
|
689
|
-
];
|
|
690
|
-
}
|
|
691
|
-
/** "2m 05s" — elapsed since an ISO stamp. */
|
|
692
|
-
function since(iso, now = Date.now()) {
|
|
693
|
-
if (!iso)
|
|
694
|
-
return '?';
|
|
695
|
-
const secs = Math.max(0, Math.round((now - Date.parse(iso)) / 1000));
|
|
696
|
-
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, '0')}s`;
|
|
697
|
-
}
|
|
698
|
-
/** The one background worker, or that there is none — for `memory status` and `doctor`. */
|
|
699
|
-
function workerLine(db) {
|
|
700
|
-
const w = workerStatus(db);
|
|
701
|
-
if (!w)
|
|
702
|
-
return 'none running';
|
|
703
|
-
return [
|
|
704
|
-
w.pid ? `pid ${w.pid}` : 'starting',
|
|
705
|
-
w.started ? `up ${since(w.started)}` : null,
|
|
706
|
-
w.generation ? `hand-off ${w.generation}` : null,
|
|
707
|
-
w.child ? `claude pid ${w.child}` : null,
|
|
708
|
-
w.job ? `job #${w.job} for ${since(w.jobStarted)}` : null,
|
|
709
|
-
w.stale ? `STALE — no heartbeat for ${since(w.heartbeat)}, being stopped` : `heartbeat ${since(w.heartbeat)} ago`,
|
|
710
|
-
]
|
|
711
|
-
.filter(Boolean)
|
|
712
|
-
.join(' · ');
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* Why the queue is paused, by class — never `last_error`, which is the
|
|
716
|
-
* provider's own prose and has carried a URL with a token in it.
|
|
717
|
-
*/
|
|
718
|
-
function pauseLine(db) {
|
|
719
|
-
const rows = db
|
|
720
|
-
.prepare(`SELECT error_class, COUNT(*) AS n, MAX(updated_at) AS at FROM memory_jobs
|
|
721
|
-
WHERE status = 'paused' GROUP BY error_class ORDER BY at DESC`)
|
|
722
|
-
.all();
|
|
723
|
-
if (!rows.length)
|
|
724
|
-
return null;
|
|
725
|
-
const why = {
|
|
726
|
-
auth: 'claude is not logged in',
|
|
727
|
-
quota: 'usage limit reached',
|
|
728
|
-
missing: 'claude is not on the PATH',
|
|
729
|
-
};
|
|
730
|
-
return rows
|
|
731
|
-
.map((r) => `${r.n} on ${r.error_class ?? 'unclassified'}${why[r.error_class ?? ''] ? ` (${why[r.error_class]})` : ''}, since ${r.at}`)
|
|
732
|
-
.join('; ');
|
|
733
|
-
}
|
|
734
|
-
function memoryStatus() {
|
|
735
|
-
const db = openDb();
|
|
736
|
-
try {
|
|
737
|
-
const { config } = loadConfig();
|
|
738
|
-
const project = currentProject();
|
|
739
|
-
const queue = queueDepth(db);
|
|
740
|
-
const totals = receiptTotals(db);
|
|
741
|
-
const savings = savingsFrom({
|
|
742
|
-
baseTokens: totals.base,
|
|
743
|
-
deliveredTokens: totals.delivered,
|
|
744
|
-
delivery: totals.confirmed > 0 ? 'confirmed' : 'unknown',
|
|
745
|
-
});
|
|
746
|
-
const lines = [
|
|
747
|
-
`project: ${project}`,
|
|
748
|
-
`capture: ${config.memory.enabled ? config.memory.capture : 'off (memory.enabled is false)'}`,
|
|
749
|
-
`entries: ${countEntries(db, project)} here, ${countEntries(db)} in total`,
|
|
750
|
-
...importedLines(db, project),
|
|
751
|
-
`pending: ${pendingEventCount(db, project)} evidence events here, ${pendingEventCount(db)} in total`,
|
|
752
|
-
`queue: ${queue.pending} pending · ${queue.paused} paused · ${queue.failed} failed${queue.quarantined ? ` · ${queue.quarantined} quarantined` : ''}`,
|
|
753
|
-
`oldest job: ${queue.oldest ?? '—'}`,
|
|
754
|
-
...(pauseLine(db) ? [`paused: ${pauseLine(db)} — fix it, then: eklavya memory process`] : []),
|
|
755
|
-
`worker: ${workerLine(db)}`,
|
|
756
|
-
// Named separately from the summarizer because they answer different
|
|
757
|
-
// questions: one is "will anything leave this machine", the other is
|
|
758
|
-
// "what is actually writing the observations right now".
|
|
759
|
-
`provider: ${config.providers.observer
|
|
760
|
-
? `${config.providers.observer.kind}:${config.providers.observer.model} (via claude -p, on your subscription)`
|
|
761
|
-
: 'none — nothing leaves this machine'}`,
|
|
762
|
-
`summarizer: ${summarizerFor(config).id}`,
|
|
763
|
-
`spool drops: ${droppedCount()}`,
|
|
764
|
-
`receipts: ${totals.receipts} (${totals.confirmed} confirmed) · base ${totals.base} → delivered ${totals.delivered} tokens`,
|
|
765
|
-
savingsLine(savings),
|
|
766
|
-
];
|
|
767
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
768
|
-
}
|
|
769
|
-
finally {
|
|
770
|
-
db.close();
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
function memorySearch(argv) {
|
|
774
|
-
const query = argv.filter((a, i) => !a.startsWith('--') && !argv[i - 1]?.match(/^--(mode|limit)$/)).join(' ');
|
|
775
|
-
if (!query.trim())
|
|
776
|
-
fail('Usage: eklavya memory search <query> [--mode keyword|semantic|hybrid] [--limit <n>] [--all-projects]');
|
|
777
|
-
const { config } = loadConfig();
|
|
778
|
-
const mode = (flag(argv, '--mode', config.retrieval.mode) ?? 'hybrid');
|
|
779
|
-
if (mode !== 'keyword' && mode !== 'semantic' && mode !== 'hybrid') {
|
|
780
|
-
fail('--mode must be keyword, semantic or hybrid.');
|
|
781
|
-
}
|
|
782
|
-
const db = openDb();
|
|
783
|
-
try {
|
|
784
|
-
const hits = search(db, query, mode, {
|
|
785
|
-
project: currentProject(),
|
|
786
|
-
allProjects: argv.includes('--all-projects'),
|
|
787
|
-
limit: numberFlag(argv, '--limit', 10),
|
|
788
|
-
});
|
|
789
|
-
if (!hits.length) {
|
|
790
|
-
process.stdout.write('No matches.\n');
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
for (const hit of hits) {
|
|
794
|
-
process.stdout.write(`#${hit.entry.id} ${hit.entry.occurred_at.slice(0, 16).replace('T', ' ')} ${hit.entry.title}\n` +
|
|
795
|
-
` ${hit.entry.type ?? hit.entry.kind} · score ${hit.score.toFixed(3)} · ${hit.via}${hit.entry.import_source ? ` · imported from ${hit.entry.import_source}` : ''}\n`);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
finally {
|
|
799
|
-
db.close();
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
function memoryTimeline(argv) {
|
|
803
|
-
const db = openDb();
|
|
804
|
-
try {
|
|
805
|
-
const rows = timeline(db, {
|
|
806
|
-
project: currentProject(),
|
|
807
|
-
limit: numberFlag(argv, '--limit', 20),
|
|
808
|
-
since: flag(argv, '--since') ?? null,
|
|
809
|
-
});
|
|
810
|
-
if (!rows.length) {
|
|
811
|
-
process.stdout.write('Nothing recorded for this project yet.\n');
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
for (const row of rows) {
|
|
815
|
-
process.stdout.write(`#${row.id} ${row.occurred_at.slice(0, 16).replace('T', ' ')} ${row.kind} ${row.title}\n`);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
finally {
|
|
819
|
-
db.close();
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
function memoryShow(argv) {
|
|
823
|
-
const id = Number(argv[0]);
|
|
824
|
-
if (!Number.isInteger(id))
|
|
825
|
-
fail('Usage: eklavya memory show <id>');
|
|
826
|
-
const db = openDb();
|
|
827
|
-
try {
|
|
828
|
-
const entry = entryById(db, id);
|
|
829
|
-
if (!entry)
|
|
830
|
-
fail(`No memory entry #${id}.`);
|
|
831
|
-
const tags = entryTags(db, id);
|
|
832
|
-
const lines = [
|
|
833
|
-
`#${entry.id} ${entry.title}`,
|
|
834
|
-
`kind: ${entry.kind}${entry.type ? ` / ${entry.type}` : ''}`,
|
|
835
|
-
`project: ${entry.project}`,
|
|
836
|
-
`occurred: ${entry.occurred_at}`,
|
|
837
|
-
`generator: ${entry.generator}`,
|
|
838
|
-
...(entry.import_source ? [`imported: from ${entry.import_source} (unassessed — no mastery, no attempts)`] : []),
|
|
839
|
-
...(entry.superseded_by ? [`superseded by #${entry.superseded_by}`] : []),
|
|
840
|
-
...(tags.length ? [`tags: ${tags.join(', ')}`] : []),
|
|
841
|
-
...(entry.files ? [`files: ${JSON.parse(entry.files).join(', ')}`] : []),
|
|
842
|
-
'',
|
|
843
|
-
entry.narrative || '(no narrative)',
|
|
844
|
-
];
|
|
845
|
-
const facts = entry.facts ? JSON.parse(entry.facts) : [];
|
|
846
|
-
if (facts.length)
|
|
847
|
-
lines.push('', 'Facts:', ...facts.map((f) => ` - ${f}`));
|
|
848
|
-
const events = entryEvents(db, id);
|
|
849
|
-
lines.push('', `Evidence (${events.length}):`);
|
|
850
|
-
for (const event of events) {
|
|
851
|
-
lines.push(` ${event.occurred_at.slice(0, 16).replace('T', ' ')} ${event.kind}${event.tool ? `/${event.tool}` : ''} ${event.body.slice(0, 120).replace(/\s+/g, ' ')}`);
|
|
852
|
-
}
|
|
853
|
-
if (!events.length)
|
|
854
|
-
lines.push(' (none linked — imported or hand-written entries carry no local evidence)');
|
|
855
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
856
|
-
}
|
|
857
|
-
finally {
|
|
858
|
-
db.close();
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
function memoryProcess(argv) {
|
|
862
|
-
// A summariser's own session must never start a worker: that is the loop
|
|
863
|
-
// that took 165 of them to stop (`reservation.ts`).
|
|
864
|
-
if (isInternalObserver())
|
|
865
|
-
return;
|
|
866
|
-
const db = openDb();
|
|
867
|
-
const { config } = loadConfig();
|
|
868
|
-
// Running this command *is* the "I have fixed the credential" signal: it is
|
|
869
|
-
// what `doctor` tells the developer to run, and nothing else takes a job off
|
|
870
|
-
// 'paused'. Resuming here rather than in the worker keeps it an explicit act
|
|
871
|
-
// — a hook that resumed by itself would spend a rejected key every session.
|
|
872
|
-
// Validate before resuming. `numberFlag` exits on a bad value, and resuming
|
|
873
|
-
// is not undoable: a refused run that had already emptied the pause would
|
|
874
|
-
// tell the developer nothing happened while the queue quietly went back to
|
|
875
|
-
// spending a credential that may still be rejected.
|
|
876
|
-
const maxJobs = numberFlag(argv, '--max', 10);
|
|
877
|
-
const background = argv.includes('--no-resume');
|
|
878
|
-
// One worker per installation, manual runs included. A hook that spawned us
|
|
879
|
-
// already won the slot and hands over its token; anyone else competes for it.
|
|
880
|
-
const handed = flag(argv, '--worker-token');
|
|
881
|
-
let token;
|
|
882
|
-
try {
|
|
883
|
-
token = handed
|
|
884
|
-
? renewWorker(db, handed, { pid: process.pid })
|
|
885
|
-
? handed
|
|
886
|
-
: null
|
|
887
|
-
: reserveWorker(db, process.pid);
|
|
888
|
-
}
|
|
889
|
-
catch {
|
|
890
|
-
// A database too busy to reserve in is one to leave alone: the slot, if
|
|
891
|
-
// this launch held it, lapses on its own.
|
|
892
|
-
token = null;
|
|
893
|
-
}
|
|
894
|
-
if (!token) {
|
|
895
|
-
const holder = workerStatus(db);
|
|
896
|
-
if (!background) {
|
|
897
|
-
process.stdout.write(`another memory worker is running${holder?.pid ? ` (pid ${holder.pid})` : ''} — its queue is this queue, so nothing to do.\n`);
|
|
898
|
-
}
|
|
899
|
-
db.close();
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
// The hooks' background drain passes --no-resume: a hook that resumed by
|
|
903
|
-
// itself is exactly the retry loop the comment above rules out.
|
|
904
|
-
const resumed = background ? 0 : resumePaused(db);
|
|
905
|
-
// SIGHUP too: a closed terminal is a stop, and the call it left running is
|
|
906
|
-
// exactly the orphan this has to prevent.
|
|
907
|
-
const stop = new AbortController();
|
|
908
|
-
const onSignal = () => stop.abort();
|
|
909
|
-
for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP'])
|
|
910
|
-
process.once(sig, onSignal);
|
|
911
|
-
superviseWorker(db, token, config, {
|
|
912
|
-
maxJobs,
|
|
913
|
-
signal: stop.signal,
|
|
914
|
-
loadConfig: () => loadConfig().config,
|
|
915
|
-
}).then((result) => {
|
|
916
|
-
if (!background) {
|
|
917
|
-
process.stdout.write(`${resumed ? `resumed ${resumed} paused · ` : ''}processed ${result.processed} · entries ${result.entries} · failed ${result.failed} · skipped ${result.skipped}${result.handedOff ? ' · more queued, continuing in the background' : ''}\n`);
|
|
918
|
-
}
|
|
919
|
-
db.close();
|
|
920
|
-
}, (err) => {
|
|
921
|
-
db.close();
|
|
922
|
-
fail(`eklavya memory process: ${err.message}`);
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* `eklavya memory backlog [quarantine|discard|restore] [selector]`: look at the
|
|
927
|
-
* unfinished queue before letting a provider loose on it, and set aside or
|
|
928
|
-
* delete what an incident put there. A change needs a selector — there is no
|
|
929
|
-
* "all", because the queue also holds real work.
|
|
930
|
-
*/
|
|
931
|
-
function memoryBacklog(argv) {
|
|
932
|
-
const action = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'list';
|
|
933
|
-
const batch = flag(argv, '--batch');
|
|
934
|
-
const sel = {
|
|
935
|
-
helpers: argv.includes('--helpers'),
|
|
936
|
-
project: flag(argv, '--project'),
|
|
937
|
-
session: flag(argv, '--session'),
|
|
938
|
-
batch: batch === undefined ? undefined : Number(batch),
|
|
939
|
-
};
|
|
940
|
-
if (sel.batch !== undefined && !Number.isInteger(sel.batch))
|
|
941
|
-
fail('--batch needs a batch id.');
|
|
942
|
-
const selected = sel.helpers || sel.project !== undefined || sel.session !== undefined || sel.batch !== undefined;
|
|
943
|
-
const db = openDb();
|
|
944
|
-
try {
|
|
945
|
-
if (action === 'list') {
|
|
946
|
-
const groups = backlogSummary(db, sel);
|
|
947
|
-
if (!groups.length) {
|
|
948
|
-
process.stdout.write('No unfinished jobs.\n');
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
for (const g of groups) {
|
|
952
|
-
process.stdout.write(`${String(g.batches).padStart(6)} ${g.status.padEnd(11)} ${g.project}${g.helper ? ' [observer helper sessions]' : ''}\n` +
|
|
953
|
-
` ${g.events} events · ${g.oldest.slice(0, 16).replace('T', ' ')} → ${g.newest.slice(0, 16).replace('T', ' ')}\n`);
|
|
954
|
-
}
|
|
955
|
-
if (groups.some((g) => g.helper)) {
|
|
956
|
-
process.stdout.write('\nHelper sessions are the observer summarising its own runs — noise. Set them aside or delete them:\n' +
|
|
957
|
-
' eklavya memory backlog quarantine --helpers\n eklavya memory backlog discard --helpers\n');
|
|
958
|
-
}
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
if (!selected)
|
|
962
|
-
fail(`eklavya memory backlog ${action} needs --helpers, --project <key>, --session <id> or --batch <id>.`);
|
|
963
|
-
if (action === 'quarantine') {
|
|
964
|
-
process.stdout.write(`quarantined ${quarantineBacklog(db, sel)} job(s) — kept, and never processed until restored.\n`);
|
|
965
|
-
}
|
|
966
|
-
else if (action === 'restore') {
|
|
967
|
-
process.stdout.write(`restored ${restoreBacklog(db, sel)} job(s) to the queue.\n`);
|
|
968
|
-
}
|
|
969
|
-
else if (action === 'discard') {
|
|
970
|
-
const gone = discardBacklog(db, sel);
|
|
971
|
-
process.stdout.write(`discarded ${gone.batches} batch(es) and ${gone.events} evidence event(s).\n`);
|
|
972
|
-
}
|
|
973
|
-
else {
|
|
974
|
-
fail('Usage: eklavya memory backlog [list|quarantine|discard|restore] [--helpers] [--project <key>] [--session <id>] [--batch <id>]');
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
finally {
|
|
978
|
-
db.close();
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* `eklavya memory stop`: ends the running worker and its provider call, and
|
|
983
|
-
* nothing that is not theirs. The job in flight goes back to the queue.
|
|
984
|
-
*/
|
|
985
|
-
function memoryStop() {
|
|
986
|
-
if (isInternalObserver())
|
|
987
|
-
return;
|
|
988
|
-
const db = openDb();
|
|
989
|
-
void stopWorker(db).then((outcome) => {
|
|
990
|
-
const { config } = loadConfig();
|
|
991
|
-
if (!outcome.stopped) {
|
|
992
|
-
process.stdout.write('no memory worker is running.\n');
|
|
993
|
-
}
|
|
994
|
-
else {
|
|
995
|
-
process.stdout.write(`stopped the memory worker${outcome.pid ? ` (pid ${outcome.pid})` : ''}${outcome.child ? ` and its claude call (pid ${outcome.child})` : ''}${outcome.forced ? ' — it had to be killed' : ''}. Unfinished jobs stay queued.\n` +
|
|
996
|
-
(outcome.released ? '' : 'something it started would not exit; the slot stays held until it does.\n'));
|
|
997
|
-
}
|
|
998
|
-
if (config.providers.observer) {
|
|
999
|
-
process.stdout.write('the next session seam starts a new one. To keep it stopped: eklavya config set providers.observer null\n');
|
|
1000
|
-
}
|
|
1001
|
-
db.close();
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
/**
|
|
1005
|
-
* Backfills from Claude Code's own transcripts.
|
|
1006
|
-
*
|
|
1007
|
-
* The hooks only see sessions that happened after Eklavya was installed. This
|
|
1008
|
-
* is for the ones before it, and for a session where a hook was misconfigured:
|
|
1009
|
-
* the transcript is on disk either way, and it goes through the same privacy
|
|
1010
|
-
* filter and converges with whatever the hooks already captured.
|
|
1011
|
-
*/
|
|
1012
|
-
function memoryReplay(argv) {
|
|
1013
|
-
const db = openDb();
|
|
1014
|
-
try {
|
|
1015
|
-
const { config } = loadConfig();
|
|
1016
|
-
if (!config.memory.enabled) {
|
|
1017
|
-
process.stdout.write('memory.enabled is false, so there is nowhere to replay into.\n');
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
const cwd = process.cwd();
|
|
1021
|
-
const files = transcriptsFor(cwd);
|
|
1022
|
-
if (!files.length) {
|
|
1023
|
-
process.stdout.write(`No Claude Code transcripts found for this checkout.\nLooked in: ${transcriptDirFor(cwd)}\n`);
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
const limit = Number(flag(argv, '--limit', '20'));
|
|
1027
|
-
const results = replayProject(db, config, cwd, { limit });
|
|
1028
|
-
const total = results.reduce((sum, r) => ({
|
|
1029
|
-
read: sum.read + r.read,
|
|
1030
|
-
captured: sum.captured + r.captured,
|
|
1031
|
-
duplicates: sum.duplicates + r.duplicates,
|
|
1032
|
-
excluded: sum.excluded + r.excluded,
|
|
1033
|
-
}), { read: 0, captured: 0, duplicates: 0, excluded: 0 });
|
|
1034
|
-
process.stdout.write([
|
|
1035
|
-
`transcripts: ${results.length} of ${files.length}`,
|
|
1036
|
-
`lines read: ${total.read}`,
|
|
1037
|
-
`captured: ${total.captured}`,
|
|
1038
|
-
`already had: ${total.duplicates}`,
|
|
1039
|
-
`excluded: ${total.excluded} (privacy filter, or capture set to minimal)`,
|
|
1040
|
-
'',
|
|
1041
|
-
'Run `eklavya memory process` to summarise what was captured.',
|
|
1042
|
-
'',
|
|
1043
|
-
].join('\n'));
|
|
1044
|
-
}
|
|
1045
|
-
finally {
|
|
1046
|
-
db.close();
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
function memoryPrune() {
|
|
1050
|
-
const db = openDb();
|
|
1051
|
-
try {
|
|
1052
|
-
const { config } = loadConfig();
|
|
1053
|
-
if (!config.memory.retention_days) {
|
|
1054
|
-
process.stdout.write('memory.retention_days is not set, so raw evidence is kept until deleted by hand.\n');
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
const removed = pruneEvidence(db, config);
|
|
1058
|
-
process.stdout.write(`Deleted ${removed} raw evidence events older than ${config.memory.retention_days} days.\n`);
|
|
1059
|
-
}
|
|
1060
|
-
finally {
|
|
1061
|
-
db.close();
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
/** The field-disposition report, printed before anything is written. */
|
|
1065
|
-
function dispositionReport(fields) {
|
|
1066
|
-
const lines = [];
|
|
1067
|
-
for (const kind of ['mapped', 'dropped', 'unrecognised']) {
|
|
1068
|
-
const group = fields.filter((f) => f.kind === kind);
|
|
1069
|
-
if (!group.length)
|
|
1070
|
-
continue;
|
|
1071
|
-
lines.push('', `${kind} (${group.length}):`);
|
|
1072
|
-
for (const f of group) {
|
|
1073
|
-
lines.push(` ${f.table}.${f.field}${f.to ? ` -> ${f.to}` : ''}${f.reason ? ` — ${f.reason}` : ''}`);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
return lines.join('\n');
|
|
1077
|
-
}
|
|
1078
|
-
/**
|
|
1079
|
-
* Reads `--map source=/path` and `--map-here source` into a project map.
|
|
1080
|
-
*
|
|
1081
|
-
* Eklavya keys a project by the checkout's absolute realpath; Claude Mem keys
|
|
1082
|
-
* it by a bare name. Without a mapping the import is honest and useless at the
|
|
1083
|
-
* moment it matters -- every row lands in a scope no session queries, so a
|
|
1084
|
-
* search in the very repository the history came from finds nothing. The
|
|
1085
|
-
* importer cannot guess which checkout `eklavya` meant, so this is a flag.
|
|
1086
|
-
*/
|
|
1087
|
-
function projectMapFrom(argv) {
|
|
1088
|
-
const map = {};
|
|
1089
|
-
for (let i = 0; i < argv.length; i++) {
|
|
1090
|
-
if (argv[i] === '--map') {
|
|
1091
|
-
const pair = argv[i + 1] ?? '';
|
|
1092
|
-
const eq = pair.indexOf('=');
|
|
1093
|
-
if (eq <= 0)
|
|
1094
|
-
fail('Usage: --map <source-project>=<path-to-checkout>');
|
|
1095
|
-
map[pair.slice(0, eq)] = projectKey(findRepoConfig(pair.slice(eq + 1)).repoRoot ?? pair.slice(eq + 1));
|
|
1096
|
-
i++;
|
|
1097
|
-
}
|
|
1098
|
-
else if (argv[i] === '--map-here') {
|
|
1099
|
-
const name = argv[i + 1];
|
|
1100
|
-
if (!name || name.startsWith('--'))
|
|
1101
|
-
fail('Usage: --map-here <source-project>');
|
|
1102
|
-
const here = findRepoConfig(process.cwd()).repoRoot;
|
|
1103
|
-
// "Here" has to be somewhere. Without a checkout `projectKey` answers with
|
|
1104
|
-
// the global bucket, so the flag would file every row under a scope no
|
|
1105
|
-
// session queries -- silently, permanently, and to say it had mapped them.
|
|
1106
|
-
if (!here)
|
|
1107
|
-
fail(`--map-here needs a checkout: ${process.cwd()} is not inside a git repository.`);
|
|
1108
|
-
map[name] = projectKey(here);
|
|
1109
|
-
i++;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
return map;
|
|
1113
|
-
}
|
|
1114
|
-
/** The `--verify` report: every source row by id, then where each project landed. */
|
|
1115
|
-
function verifyLines(r) {
|
|
1116
|
-
const missing = r.tables.reduce((n, t) => n + t.missing.length, 0);
|
|
1117
|
-
const width = Math.max(0, ...r.projects.map((p) => p.project.length));
|
|
1118
|
-
return [
|
|
1119
|
-
`verify: ${r.sourcePath}`,
|
|
1120
|
-
...r.tables.map((t) => ` ${t.table.padEnd(18)} ${String(t.source).padStart(6)} in source · ${String(t.present).padStart(6)} in Eklavya · ${t.missing.length} missing${t.missing.length ? ` (ids ${t.missing.slice(0, 10).join(', ')}${t.missing.length > 10 ? ', …' : ''})` : ''}`),
|
|
1121
|
-
` changed since import: ${r.changed} entr${r.changed === 1 ? 'y' : 'ies'}`,
|
|
1122
|
-
'placement:',
|
|
1123
|
-
...r.projects.flatMap((p) => Object.entries(p.filedUnder).map(([to, n]) => {
|
|
1124
|
-
const placed = path.isAbsolute(to);
|
|
1125
|
-
return ` ${String(n).padStart(6)} ${p.project.padEnd(width)} ${placed ? `→ ${to}` : 'not placed — searchable only with --all-projects; --map it to a checkout'}`;
|
|
1126
|
-
})),
|
|
1127
|
-
missing
|
|
1128
|
-
? `INCOMPLETE: ${missing} source row(s) are not in Eklavya — re-run without --verify to import them.`
|
|
1129
|
-
: 'complete: every source row is in Eklavya.',
|
|
1130
|
-
];
|
|
1131
|
-
}
|
|
1132
|
-
async function memoryImport(argv) {
|
|
1133
|
-
const flagValues = new Set(argv.flatMap((a, i) => (a === '--map' || a === '--map-here' ? [argv[i + 1] ?? ''] : [])));
|
|
1134
|
-
const source = argv.find((a) => !a.startsWith('--') && !flagValues.has(a));
|
|
1135
|
-
if (!source)
|
|
1136
|
-
fail('Usage: eklavya memory import <path-to-claude-mem.db> [--dry-run] [--verify] [--resume] [--map <src>=<path>]');
|
|
1137
|
-
const dryRun = argv.includes('--dry-run');
|
|
1138
|
-
const explicit = projectMapFrom(argv);
|
|
1139
|
-
try {
|
|
1140
|
-
if (argv.includes('--verify')) {
|
|
1141
|
-
const db = openDb();
|
|
1142
|
-
try {
|
|
1143
|
-
const report = verifyImport(db, source);
|
|
1144
|
-
process.stdout.write(`${verifyLines(report).join('\n')}\n`);
|
|
1145
|
-
if (report.tables.some((t) => t.missing.length))
|
|
1146
|
-
process.exit(1);
|
|
1147
|
-
}
|
|
1148
|
-
finally {
|
|
1149
|
-
db.close();
|
|
1150
|
-
}
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
const found = inventory(source);
|
|
1154
|
-
// Placed the way `eklavya install` places them -- off Claude Code's
|
|
1155
|
-
// transcripts -- so a re-run by hand files history where install would
|
|
1156
|
-
// have. A --map names what the transcripts cannot, and wins.
|
|
1157
|
-
const unsure = {};
|
|
1158
|
-
const projectMap = { ...guessProjectMap(source, claudeHome(), unsure), ...explicit };
|
|
1159
|
-
for (const name of Object.keys(explicit))
|
|
1160
|
-
delete unsure[name];
|
|
1161
|
-
const unsureLines = Object.entries(unsure).map(([name, paths]) => ` ${name}: ${paths.length} checkouts carry this name — pick one: --map ${name}=<path> (${paths.join(', ')})`);
|
|
1162
|
-
const lines = [
|
|
1163
|
-
`source: ${found.sourcePath}`,
|
|
1164
|
-
`schema: ${found.schemaVersion ?? 'unversioned'} (this importer understands up to ${found.supportedMax})`,
|
|
1165
|
-
`range: ${found.dateRange.from?.slice(0, 10) ?? '—'} … ${found.dateRange.to?.slice(0, 10) ?? '—'}`,
|
|
1166
|
-
'tables:',
|
|
1167
|
-
...found.tables.map((t) => ` ${t.rows.toString().padStart(7)} ${t.name}${t.known ? '' : ' (unrecognised)'}`),
|
|
1168
|
-
'projects:',
|
|
1169
|
-
...found.projects.map((p) => ` ${p.entries.toString().padStart(7)} ${p.project}`),
|
|
1170
|
-
dispositionReport(found.fields),
|
|
1171
|
-
];
|
|
1172
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
1173
|
-
if (!found.supported)
|
|
1174
|
-
fail(`\n${found.problem ?? 'Unsupported source database.'}`);
|
|
1175
|
-
if (dryRun) {
|
|
1176
|
-
const planned = Object.entries(projectMap);
|
|
1177
|
-
const unmapped = found.projects.map((p) => p.project).filter((p) => !(p in projectMap));
|
|
1178
|
-
process.stdout.write([
|
|
1179
|
-
'',
|
|
1180
|
-
...planned.map(([from, to]) => `would map: ${from} -> ${to}`),
|
|
1181
|
-
...(unmapped.length ? [`would keep as-is: ${unmapped.join(', ')}`] : []),
|
|
1182
|
-
...unsureLines,
|
|
1183
|
-
'Dry run: nothing was written, and the source was opened read-only.',
|
|
1184
|
-
'',
|
|
1185
|
-
].join('\n'));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
{
|
|
1189
|
-
process.stdout.write('\n');
|
|
1190
|
-
const { report, verified } = await spin('import', 'importing…', () => importOffThread({ dbFile: dbPath(), source, opts: { resume: argv.includes('--resume'), projectMap } }));
|
|
1191
|
-
const rows = IMPORTED_TABLES.map((t) => ` ${t.padEnd(18)} read ${report.read[t]} · imported ${report.imported[t]} · already present ${report.skipped[t]}`);
|
|
1192
|
-
process.stdout.write([
|
|
1193
|
-
'',
|
|
1194
|
-
`snapshot: ${report.snapshot}`,
|
|
1195
|
-
...rows,
|
|
1196
|
-
` concept candidates: ${report.candidates} (all unassessed — no mastery, no attempts, no gate touched)`,
|
|
1197
|
-
` evidence links: ${report.links} (drill-down from an entry to the prompts and tool uses behind it)`,
|
|
1198
|
-
` re-indexed: ${report.reindexed} entries`,
|
|
1199
|
-
...(report.rehomed ? [` re-homed: ${report.rehomed} entries an earlier run left under a bare project name`] : []),
|
|
1200
|
-
` validation: ${report.validation.ok ? 'ok' : `FAILED — ${report.validation.notes.join('; ')}`}`,
|
|
1201
|
-
...report.projectsMapped.map((p) => ` mapped: ${p.from} -> ${p.to}`),
|
|
1202
|
-
// The unmapped list is the useful half: those rows only ever surface
|
|
1203
|
-
// under --all-projects until somebody maps them.
|
|
1204
|
-
...(report.projectsKept.length
|
|
1205
|
-
? [
|
|
1206
|
-
` kept as-is: ${report.projectsKept.join(', ')}`,
|
|
1207
|
-
' (unmapped projects are searchable only with --all-projects; re-run with --map to file them under a checkout)',
|
|
1208
|
-
]
|
|
1209
|
-
: []),
|
|
1210
|
-
'',
|
|
1211
|
-
].join('\n'));
|
|
1212
|
-
if (unsureLines.length)
|
|
1213
|
-
process.stdout.write(`${unsureLines.join('\n')}\n`);
|
|
1214
|
-
if (!report.validation.ok)
|
|
1215
|
-
process.exit(1);
|
|
1216
|
-
// Counts agreeing is what validation proves; this proves every source
|
|
1217
|
-
// row by id, and shows where each project's history is filed.
|
|
1218
|
-
process.stdout.write(`\n${verifyLines(verified).join('\n')}\n`);
|
|
1219
|
-
if (verified.tables.some((t) => t.missing.length))
|
|
1220
|
-
process.exit(1);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
catch (err) {
|
|
1224
|
-
if (err instanceof ImportError)
|
|
1225
|
-
fail(err.message);
|
|
1226
|
-
// The source is a hand-typed path, so pointing it at the wrong file is the
|
|
1227
|
-
// likeliest mistake there is. The missing-file case was already handled and
|
|
1228
|
-
// `restore` says "is not readable JSON" for the same mistake; only this path
|
|
1229
|
-
// let a driver error out with a stack through node_modules.
|
|
1230
|
-
fail(`eklavya memory import: cannot read ${source} — ${err.message}`);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
function memoryExport(argv) {
|
|
1234
|
-
const out = argv.find((a) => !a.startsWith('--'));
|
|
1235
|
-
if (!out)
|
|
1236
|
-
fail('Usage: eklavya memory export <path>');
|
|
1237
|
-
const db = openDb();
|
|
1238
|
-
try {
|
|
1239
|
-
const payload = exportPayload(db);
|
|
1240
|
-
fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true });
|
|
1241
|
-
fs.writeFileSync(out, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
1242
|
-
process.stdout.write(`Wrote ${out} — ${payload.entries.length} entries, schema version ${EXPORT_SCHEMA_VERSION}\n`);
|
|
1243
|
-
}
|
|
1244
|
-
finally {
|
|
1245
|
-
db.close();
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* `eklavya memory restore <file>` — the other half of the backup pair.
|
|
1250
|
-
*
|
|
1251
|
-
* Without it `export` writes a file nothing on the machine can read, which
|
|
1252
|
-
* makes the rollback drill in the migration guide unrunnable. It is additive
|
|
1253
|
-
* and idempotent, so it is also how a second device is brought up to date from
|
|
1254
|
-
* a file rather than a shared folder.
|
|
1255
|
-
*/
|
|
1256
|
-
function memoryRestore(argv) {
|
|
1257
|
-
const from = argv.find((a) => !a.startsWith('--'));
|
|
1258
|
-
if (!from)
|
|
1259
|
-
fail('Usage: eklavya memory restore <file>');
|
|
1260
|
-
const db = openDb();
|
|
1261
|
-
try {
|
|
1262
|
-
const r = restoreExport(db, path.resolve(from));
|
|
1263
|
-
process.stdout.write([
|
|
1264
|
-
`Restored ${from} (export schema version ${r.schemaVersion}):`,
|
|
1265
|
-
` entries: ${r.entries.restored} restored, ${r.entries.skipped} already here`,
|
|
1266
|
-
` evidence: ${r.evidence.restored} restored, ${r.evidence.skipped} already here`,
|
|
1267
|
-
` links: ${r.tags} tag(s), ${r.links} evidence link(s)`,
|
|
1268
|
-
` receipts: ${r.receipts.restored} restored, ${r.receipts.skipped} already here (${r.receiptItems} item(s))`,
|
|
1269
|
-
` reindexed: ${r.reindexed} entries — search index and vectors rebuilt`,
|
|
1270
|
-
'Learning history was not touched: no attempt, mastery or gate row is written by a restore.',
|
|
1271
|
-
'',
|
|
1272
|
-
].join('\n'));
|
|
1273
|
-
}
|
|
1274
|
-
catch (err) {
|
|
1275
|
-
if (err instanceof ImportError)
|
|
1276
|
-
fail(err.message);
|
|
1277
|
-
throw err;
|
|
1278
|
-
}
|
|
1279
|
-
finally {
|
|
1280
|
-
db.close();
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* `eklavya memory sync push|pull|status [--target <dir>]` (ADR-09).
|
|
1285
|
-
*
|
|
1286
|
-
* The directory is the whole protocol, so the command has no host, no token and
|
|
1287
|
-
* no network error to report — only what it wrote and what it read back.
|
|
1288
|
-
* `--target` overrides `sync.target` for one run; it does not override
|
|
1289
|
-
* `sync.enabled`, because "point it somewhere for a second" is still a decision
|
|
1290
|
-
* to publish this machine's memory.
|
|
1291
|
-
*/
|
|
1292
|
-
function memorySync(argv) {
|
|
1293
|
-
const [sub] = argv;
|
|
1294
|
-
if (sub !== 'push' && sub !== 'pull' && sub !== 'status') {
|
|
1295
|
-
fail('Usage: eklavya memory sync <push|pull|status> [--target <dir>]');
|
|
1296
|
-
}
|
|
1297
|
-
const target = flag(argv, '--target') ?? null;
|
|
1298
|
-
const db = openDb();
|
|
1299
|
-
try {
|
|
1300
|
-
const { config } = loadConfig();
|
|
1301
|
-
if (sub === 'status') {
|
|
1302
|
-
const s = syncStatus(db, config, { target });
|
|
1303
|
-
const lines = [
|
|
1304
|
-
`sync: ${s.enabled ? 'on' : 'off (set sync.enabled)'}`,
|
|
1305
|
-
`target: ${s.target ?? '— (set sync.target, or pass --target)'}`,
|
|
1306
|
-
`device: ${s.device_id ?? '—'}`,
|
|
1307
|
-
`revision: ${s.local_revision}`,
|
|
1308
|
-
`pending: ${s.pending} local change${s.pending === 1 ? '' : 's'} to push`,
|
|
1309
|
-
`conflicts: ${s.open_conflicts} quarantined`,
|
|
1310
|
-
`peers: ${s.peers.length
|
|
1311
|
-
? s.peers.map((p) => `${p.device_id}@${p.last_revision}`).join(', ')
|
|
1312
|
-
: 'none seen yet'}`,
|
|
1313
|
-
];
|
|
1314
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
const result = sub === 'push' ? push(db, config, { target }) : pull(db, config, { target });
|
|
1318
|
-
if (!result.ok) {
|
|
1319
|
-
fail(result.reason === 'disabled'
|
|
1320
|
-
? 'Sync is off. Set sync.enabled to true in ~/.eklavya/config.json.'
|
|
1321
|
-
: 'No sync target. Set sync.target to a folder your devices share, or pass --target.');
|
|
1322
|
-
}
|
|
1323
|
-
if (sub === 'push') {
|
|
1324
|
-
const r = result;
|
|
1325
|
-
process.stdout.write(`Pushed to ${r.target} as ${r.device_id}: ${r.staged} new revision${r.staged === 1 ? '' : 's'}, ${r.written} record${r.written === 1 ? '' : 's'} written, ${r.already} already there.\n`);
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
const r = result;
|
|
1329
|
-
process.stdout.write(`Pulled from ${r.target}: ${r.applied} applied (${r.tombstones} deletion${r.tombstones === 1 ? '' : 's'}), ${r.skipped} already known, ${r.conflicts} quarantined.\n`);
|
|
1330
|
-
if (r.conflicts) {
|
|
1331
|
-
process.stdout.write('Quarantined versions are kept whole in sync_conflicts — nothing was overwritten.\n');
|
|
1332
|
-
}
|
|
1333
|
-
if (r.stalled.length) {
|
|
1334
|
-
process.stdout.write(`Stopped early on an unreadable record from: ${r.stalled.join(', ')} — likely still being written. Try again.\n`);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
finally {
|
|
1338
|
-
db.close();
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
function memoryCommand(argv) {
|
|
1342
|
-
const [sub, ...rest] = argv;
|
|
1343
|
-
switch (sub) {
|
|
1344
|
-
case 'status':
|
|
1345
|
-
return memoryStatus();
|
|
1346
|
-
case 'search':
|
|
1347
|
-
return memorySearch(rest);
|
|
1348
|
-
case 'timeline':
|
|
1349
|
-
return memoryTimeline(rest);
|
|
1350
|
-
case 'show':
|
|
1351
|
-
return memoryShow(rest);
|
|
1352
|
-
case 'replay':
|
|
1353
|
-
return memoryReplay(rest);
|
|
1354
|
-
case 'process':
|
|
1355
|
-
return memoryProcess(rest);
|
|
1356
|
-
case 'stop':
|
|
1357
|
-
return memoryStop();
|
|
1358
|
-
case 'backlog':
|
|
1359
|
-
return memoryBacklog(rest);
|
|
1360
|
-
case 'prune':
|
|
1361
|
-
return memoryPrune();
|
|
1362
|
-
case 'import':
|
|
1363
|
-
// Async only so the spinner turns: the import itself runs on a worker.
|
|
1364
|
-
void memoryImport(rest);
|
|
1365
|
-
return;
|
|
1366
|
-
case 'export':
|
|
1367
|
-
return memoryExport(rest);
|
|
1368
|
-
case 'restore':
|
|
1369
|
-
return memoryRestore(rest);
|
|
1370
|
-
case 'sync':
|
|
1371
|
-
return memorySync(rest);
|
|
1372
|
-
default:
|
|
1373
|
-
process.stderr.write(MEMORY_USAGE);
|
|
1374
|
-
process.exit(1);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
function main() {
|
|
695
|
+
async function main() {
|
|
1378
696
|
const [command, ...rest] = process.argv.slice(2);
|
|
1379
697
|
switch (command) {
|
|
1380
698
|
case 'serve':
|
|
@@ -1386,15 +704,17 @@ function main() {
|
|
|
1386
704
|
process.exit(1);
|
|
1387
705
|
});
|
|
1388
706
|
return;
|
|
1389
|
-
case 'install':
|
|
707
|
+
case 'install': {
|
|
708
|
+
const { install } = await import('./install.js');
|
|
1390
709
|
// Async only because the settings walk waits on a terminal.
|
|
1391
710
|
install(rest).catch((err) => {
|
|
1392
711
|
process.stderr.write(`eklavya install: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1393
712
|
process.exit(1);
|
|
1394
713
|
});
|
|
1395
714
|
return;
|
|
715
|
+
}
|
|
1396
716
|
case 'uninstall':
|
|
1397
|
-
return uninstall(rest);
|
|
717
|
+
return (await import('./install.js')).uninstall(rest);
|
|
1398
718
|
case 'export-rules':
|
|
1399
719
|
return exportRules(rest);
|
|
1400
720
|
case 'config':
|
|
@@ -1405,7 +725,7 @@ function main() {
|
|
|
1405
725
|
case 'dashboard':
|
|
1406
726
|
return dashboardCommand(rest);
|
|
1407
727
|
case 'memory':
|
|
1408
|
-
return memoryCommand(rest);
|
|
728
|
+
return (await import('./cli-memory.js')).memoryCommand(rest);
|
|
1409
729
|
case 'doctor':
|
|
1410
730
|
return doctor();
|
|
1411
731
|
case 'db-path':
|
|
@@ -1421,5 +741,7 @@ function main() {
|
|
|
1421
741
|
process.exit(1);
|
|
1422
742
|
}
|
|
1423
743
|
}
|
|
1424
|
-
|
|
744
|
+
// Awaited, so an error thrown by a command surfaces exactly as it did when
|
|
745
|
+
// `main()` was synchronous: as the module's own failure, stack and exit 1.
|
|
746
|
+
await main();
|
|
1425
747
|
//# sourceMappingURL=cli.js.map
|