claude-mem-lite 2.69.0 → 2.70.0
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -3
- package/cli.mjs +1 -1
- package/hook-context.mjs +20 -16
- package/hook.mjs +8 -0
- package/lib/deferred-work.mjs +171 -0
- package/lib/git-state.mjs +15 -0
- package/lib/upgrade-banner.mjs +31 -0
- package/mem-cli.mjs +175 -11
- package/package.json +3 -1
- package/schema.mjs +33 -1
- package/server.mjs +127 -20
- package/source-files.mjs +7 -0
- package/tool-schemas.mjs +107 -4
package/README.md
CHANGED
|
@@ -209,14 +209,14 @@ rm -rf ~/claude-mem-lite/ # pre-v0.5 unhidden (if not auto-moved)
|
|
|
209
209
|
|
|
210
210
|
### MCP Tools (used automatically by Claude)
|
|
211
211
|
|
|
212
|
-
As of v2.
|
|
212
|
+
As of v2.70.0, the server registers 20 tools in total but only the 9 **core**
|
|
213
213
|
tools appear in `tools/list`. The 11 **hidden** tools remain callable at the
|
|
214
214
|
protocol layer (`tools/call` by exact name still routes normally); they're
|
|
215
215
|
omitted from the list response so Claude Code sessions don't load 11 extra
|
|
216
216
|
tool schemas at startup. Hidden tools are the maintenance / admin / browser
|
|
217
217
|
surface — reach them through the CLI column in the second table.
|
|
218
218
|
|
|
219
|
-
**Core (
|
|
219
|
+
**Core (9, exposed to Claude Code)**
|
|
220
220
|
|
|
221
221
|
| Tool | Description |
|
|
222
222
|
|------|-------------|
|
|
@@ -225,7 +225,10 @@ surface — reach them through the CLI column in the second table.
|
|
|
225
225
|
| `mem_recall` | Recall observations related to a file. Use before editing to surface past bugfixes and context. |
|
|
226
226
|
| `mem_timeline` | Browse observations chronologically around an anchor point. |
|
|
227
227
|
| `mem_get` | Retrieve full details for specific observation IDs (includes importance and related_ids). |
|
|
228
|
-
| `mem_save` | Manually save a memory/observation. |
|
|
228
|
+
| `mem_save` | Manually save a memory/observation. Accepts `closes_deferred` array for transactional closure of deferred work. |
|
|
229
|
+
| `mem_defer` | Mark work for a future session (v2.70+). First-class carry-forward signal, surfaced in SessionStart `### Deferred Work` block. |
|
|
230
|
+
| `mem_defer_list` | List open deferred items for the current project. |
|
|
231
|
+
| `mem_defer_drop` | Drop a deferred item without fixing it; requires a `reason` for the audit trail. |
|
|
229
232
|
|
|
230
233
|
**Hidden-but-callable (11, CLI-routed)**
|
|
231
234
|
|
package/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'help']);
|
|
2
|
+
const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'defer', 'help']);
|
|
3
3
|
const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'release']);
|
|
4
4
|
|
|
5
5
|
const cmd = process.argv[2];
|
package/hook-context.mjs
CHANGED
|
@@ -407,26 +407,30 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
407
407
|
handoffLines.push('');
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
// 5b. Deferred Work —
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
ORDER BY
|
|
410
|
+
// 5b. Deferred Work — backed by deferred_work table (v2.70.0).
|
|
411
|
+
// Replaces the prior importance≥3 obs proxy. Items shown by per-project
|
|
412
|
+
// ordinal so user can refer to "处理1" / "handle item 1" naturally; D#<id>
|
|
413
|
+
// is the stable handle for tool-layer references (closes_deferred=[N]).
|
|
414
|
+
// Quiet-hooks does NOT suppress: cross-session continuity is the whole point.
|
|
415
|
+
const deferredItems = db.prepare(`
|
|
416
|
+
SELECT id, title, priority,
|
|
417
|
+
ROW_NUMBER() OVER (
|
|
418
|
+
ORDER BY priority DESC, created_at_epoch ASC
|
|
419
|
+
) AS ordinal
|
|
420
|
+
FROM deferred_work
|
|
421
|
+
WHERE project = ? AND status = 'open'
|
|
422
|
+
ORDER BY priority DESC, created_at_epoch ASC
|
|
423
|
+
LIMIT 5
|
|
423
424
|
`).all(project);
|
|
424
425
|
|
|
425
426
|
const deferredLines = [];
|
|
426
|
-
if (
|
|
427
|
+
if (deferredItems.length > 0) {
|
|
427
428
|
deferredLines.push('### Deferred Work');
|
|
428
|
-
for (const
|
|
429
|
-
|
|
429
|
+
for (const d of deferredItems) {
|
|
430
|
+
const pTag = d.priority === 3 ? '🔴' : d.priority === 1 ? '⚪' : '🟡';
|
|
431
|
+
deferredLines.push(
|
|
432
|
+
`${d.ordinal}. ${pTag} [P${d.priority}] ${truncate(d.title, 120)} (D#${d.id})`
|
|
433
|
+
);
|
|
430
434
|
}
|
|
431
435
|
deferredLines.push('');
|
|
432
436
|
}
|
package/hook.mjs
CHANGED
|
@@ -51,6 +51,7 @@ import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection,
|
|
|
51
51
|
import { checkForUpdate } from './hook-update.mjs';
|
|
52
52
|
import { handleLLMOptimize } from './hook-optimize.mjs';
|
|
53
53
|
import { silentAutoAdopt, hasAutoAdoptMarker } from './adopt-cli.mjs';
|
|
54
|
+
import { emitV270UpgradeBanner } from './lib/upgrade-banner.mjs';
|
|
54
55
|
// plugin-cache-guard.mjs loaded dynamically — pre-2.31.2 installs that auto-upgraded
|
|
55
56
|
// from an older hook-update.mjs SOURCE_FILES (which did not list this module) would
|
|
56
57
|
// crash on static import. Degrade gracefully to no-op when the module is absent.
|
|
@@ -1073,6 +1074,13 @@ async function handleSessionStart() {
|
|
|
1073
1074
|
// CLAUDE.md by pre-v2.30 installs. Idempotent no-op afterwards.
|
|
1074
1075
|
cleanupClaudeMdLegacyBlock();
|
|
1075
1076
|
|
|
1077
|
+
// v2.70.0 one-shot upgrade banner: notify users on first SessionStart per
|
|
1078
|
+
// project that the `### Deferred Work` block now reads from the
|
|
1079
|
+
// deferred_work table (was: high-importance observations in v2.69.x).
|
|
1080
|
+
// Idempotent via marker file; subsequent SessionStarts are silent.
|
|
1081
|
+
try { emitV270UpgradeBanner({ project, runtimeDir: RUNTIME_DIR }); }
|
|
1082
|
+
catch (e) { debugCatch(e, 'session-start-v270-banner'); }
|
|
1083
|
+
|
|
1076
1084
|
// Pre-load TF-IDF vocabulary cache for this session (from DB, ~1ms)
|
|
1077
1085
|
try { getVocabulary(db); } catch (e) { debugCatch(e, 'session-start-vocab'); }
|
|
1078
1086
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// claude-mem-lite — deferred_work data layer
|
|
2
|
+
// Pure-data CRUD + ordinal resolver + transactional closure helper.
|
|
3
|
+
// Decoupled from observations table: different lifecycle, different scoring.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Insert a new open deferred_work row.
|
|
7
|
+
* @param {Database} db Opened DB
|
|
8
|
+
* @param {object} args
|
|
9
|
+
* @param {string} args.project Required project name
|
|
10
|
+
* @param {string} args.title Required one-line subject
|
|
11
|
+
* @param {number} [args.priority=2] 1=low, 2=normal, 3=urgent
|
|
12
|
+
* @param {string} [args.detail] Optional longer description
|
|
13
|
+
* @param {string[]} [args.files] Optional file paths
|
|
14
|
+
* @param {string} [args.source_session_id] Mem session id
|
|
15
|
+
* @param {number} [args.source_prompt_id] user_prompts.id
|
|
16
|
+
* @returns {{id: number}} Inserted row id
|
|
17
|
+
*/
|
|
18
|
+
export function insertDeferred(db, args) {
|
|
19
|
+
const { project, title, priority = 2, detail = null, files = null,
|
|
20
|
+
source_session_id = null, source_prompt_id = null } = args;
|
|
21
|
+
// source_session_id / source_prompt_id: forward-compat for v2.71+ defer-detector
|
|
22
|
+
// hook (anchor a deferred item to the originating prompt). v1 inserts NULL.
|
|
23
|
+
if (!project || typeof project !== 'string') throw new Error('project required');
|
|
24
|
+
if (!title || typeof title !== 'string') throw new Error('title required');
|
|
25
|
+
if (![1, 2, 3].includes(priority)) throw new Error('priority must be 1, 2, or 3');
|
|
26
|
+
const stmt = db.prepare(`
|
|
27
|
+
INSERT INTO deferred_work
|
|
28
|
+
(project, title, detail, priority, status, created_at_epoch,
|
|
29
|
+
source_session_id, source_prompt_id, files)
|
|
30
|
+
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, ?)
|
|
31
|
+
`);
|
|
32
|
+
const r = stmt.run(
|
|
33
|
+
project, title, detail, priority, Date.now(),
|
|
34
|
+
source_session_id, source_prompt_id,
|
|
35
|
+
files ? JSON.stringify(files) : null,
|
|
36
|
+
);
|
|
37
|
+
return { id: Number(r.lastInsertRowid) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List open items in a project with computed per-project ordinal.
|
|
42
|
+
* Ordinal is dynamic — recomputed each call by ROW_NUMBER over open rows
|
|
43
|
+
* sorted (priority DESC, created_at_epoch ASC). When item-1 closes, item-2
|
|
44
|
+
* becomes the new item-1.
|
|
45
|
+
* @param {Database} db
|
|
46
|
+
* @param {string} project
|
|
47
|
+
* @param {number} [limit=10]
|
|
48
|
+
* @returns {Array<{id, project, title, detail, priority, status, created_at_epoch, ordinal}>}
|
|
49
|
+
*/
|
|
50
|
+
export function listOpenWithOrdinal(db, project, limit = 10) {
|
|
51
|
+
return db.prepare(`
|
|
52
|
+
SELECT id, project, title, detail, priority, status, created_at_epoch,
|
|
53
|
+
ROW_NUMBER() OVER (ORDER BY priority DESC, created_at_epoch ASC) AS ordinal
|
|
54
|
+
FROM deferred_work
|
|
55
|
+
WHERE project = ? AND status = 'open'
|
|
56
|
+
ORDER BY priority DESC, created_at_epoch ASC
|
|
57
|
+
LIMIT ?
|
|
58
|
+
`).all(project, limit);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set status='dropped' with a non-empty reason. No-op when status is not 'open'.
|
|
63
|
+
* @returns {{changed: number}} 1 if updated, 0 if not found or not open.
|
|
64
|
+
*/
|
|
65
|
+
export function dropDeferred(db, id, reason) {
|
|
66
|
+
if (typeof reason !== 'string' || reason.trim().length === 0) {
|
|
67
|
+
throw new Error('drop reason required (non-empty string)');
|
|
68
|
+
}
|
|
69
|
+
const r = db.prepare(`
|
|
70
|
+
UPDATE deferred_work
|
|
71
|
+
SET status='dropped', closed_at_epoch=?, drop_reason=?
|
|
72
|
+
WHERE id=? AND status='open'
|
|
73
|
+
`).run(Date.now(), reason.trim(), id);
|
|
74
|
+
return { changed: r.changes };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve mixed ordinal (int) + raw-id ("D#<n>") tokens to real deferred_work
|
|
79
|
+
* ids, validated against caller project + status='open'.
|
|
80
|
+
*
|
|
81
|
+
* - bare integer N → ordinal-within-project (uses same ROW_NUMBER as listOpenWithOrdinal)
|
|
82
|
+
* - "D#<n>" string → raw deferred_work.id; must belong to caller project AND be open
|
|
83
|
+
*
|
|
84
|
+
* @param {Database} db
|
|
85
|
+
* @param {string} project Caller project (FK guard)
|
|
86
|
+
* @param {Array<number|string>} tokens Mixed input
|
|
87
|
+
* @returns {number[]} Real deferred_work ids in input order
|
|
88
|
+
* @throws {Error} On unresolvable input — error message names the offending token
|
|
89
|
+
*/
|
|
90
|
+
export function resolveDeferredIds(db, project, tokens) {
|
|
91
|
+
if (!Array.isArray(tokens)) throw new Error('tokens must be an array');
|
|
92
|
+
// Pre-load open list once for ordinal resolution (ROW_NUMBER snapshot stable
|
|
93
|
+
// within this call so [1, 2] resolves consistently).
|
|
94
|
+
const open = db.prepare(`
|
|
95
|
+
SELECT id, ROW_NUMBER() OVER (ORDER BY priority DESC, created_at_epoch ASC) AS ordinal
|
|
96
|
+
FROM deferred_work
|
|
97
|
+
WHERE project = ? AND status = 'open'
|
|
98
|
+
`).all(project);
|
|
99
|
+
const ordinalToId = new Map(open.map(r => [r.ordinal, r.id]));
|
|
100
|
+
|
|
101
|
+
const getRow = db.prepare(`SELECT id, project, status FROM deferred_work WHERE id = ?`);
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const resolved = [];
|
|
104
|
+
|
|
105
|
+
for (const t of tokens) {
|
|
106
|
+
let id;
|
|
107
|
+
if (Number.isInteger(t)) {
|
|
108
|
+
id = ordinalToId.get(t);
|
|
109
|
+
if (id === undefined) {
|
|
110
|
+
throw new Error(`ordinal ${t} has no corresponding open deferred item in project "${project}" (open count: ${open.length})`);
|
|
111
|
+
}
|
|
112
|
+
} else if (typeof t === 'string') {
|
|
113
|
+
const m = /^D#(\d+)$/.exec(t.trim());
|
|
114
|
+
if (!m) throw new Error(`invalid token "${t}" — expected D#N or integer ordinal`);
|
|
115
|
+
id = parseInt(m[1], 10);
|
|
116
|
+
const row = getRow.get(id);
|
|
117
|
+
if (!row) throw new Error(`D#${id} not found`);
|
|
118
|
+
if (row.project !== project) {
|
|
119
|
+
throw new Error(`D#${id} belongs to project "${row.project}", not "${project}"`);
|
|
120
|
+
}
|
|
121
|
+
if (row.status !== 'open') {
|
|
122
|
+
throw new Error(`D#${id} status is "${row.status}", cannot close (only 'open' items)`);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
throw new Error(`invalid token type ${typeof t} — expected D#N or integer ordinal`);
|
|
126
|
+
}
|
|
127
|
+
if (seen.has(id)) throw new Error(`duplicate token resolves to id ${id}`);
|
|
128
|
+
seen.add(id);
|
|
129
|
+
resolved.push(id);
|
|
130
|
+
}
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Close a set of deferred items by id, all-or-nothing.
|
|
136
|
+
*
|
|
137
|
+
* Wraps the UPDATE loop in an internal transaction so that any per-row failure
|
|
138
|
+
* rolls back prior rows. better-sqlite3's `.transaction()` composes with an
|
|
139
|
+
* outer caller-managed transaction via SAVEPOINT — Task 5's wider closure flow
|
|
140
|
+
* (obs INSERT + closeDeferredItems) wraps both calls in one outer transaction
|
|
141
|
+
* to guarantee atomicity across the obs row and the deferred-work UPDATEs.
|
|
142
|
+
*
|
|
143
|
+
* @param {Database} db
|
|
144
|
+
* @param {number[]} ids Already-resolved real ids (use resolveDeferredIds first)
|
|
145
|
+
* @param {number} closingObsId observations.id that proves closure
|
|
146
|
+
* @throws {Error} If any id is not currently open (lookup-based safety net)
|
|
147
|
+
*/
|
|
148
|
+
export function closeDeferredItems(db, ids, closingObsId) {
|
|
149
|
+
if (!Array.isArray(ids) || ids.length === 0) return;
|
|
150
|
+
if (!Number.isInteger(closingObsId) || closingObsId <= 0) {
|
|
151
|
+
throw new Error('closingObsId must be a positive integer');
|
|
152
|
+
}
|
|
153
|
+
// Defense-in-depth: even if caller already validated via resolveDeferredIds,
|
|
154
|
+
// re-check status here (caller may have done resolution earlier in the same
|
|
155
|
+
// transaction without holding a lock).
|
|
156
|
+
const stmt = db.prepare(`
|
|
157
|
+
UPDATE deferred_work
|
|
158
|
+
SET status='done', closed_at_epoch=?, closed_by_obs_id=?
|
|
159
|
+
WHERE id=? AND status='open'
|
|
160
|
+
`);
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const tx = db.transaction((idList) => {
|
|
163
|
+
for (const id of idList) {
|
|
164
|
+
const r = stmt.run(now, closingObsId, id);
|
|
165
|
+
if (r.changes !== 1) {
|
|
166
|
+
throw new Error(`closeDeferredItems: id ${id} was not in 'open' status (changes=${r.changes})`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
tx(ids);
|
|
171
|
+
}
|
package/lib/git-state.mjs
CHANGED
|
@@ -6,10 +6,25 @@ import { execFileSync } from 'child_process';
|
|
|
6
6
|
|
|
7
7
|
const GIT_TIMEOUT_MS = 1500;
|
|
8
8
|
|
|
9
|
+
// Strip inherited GIT_* env so child `git` operates on the requested `cwd`
|
|
10
|
+
// rather than a parent process's repo. Required when readGitState is called
|
|
11
|
+
// from contexts where GIT_DIR/GIT_INDEX_FILE/GIT_WORK_TREE/GIT_PREFIX leak in:
|
|
12
|
+
// pre-commit hooks running tests, hooks invoked under `git commit`, etc.
|
|
13
|
+
// Without this, headSha and `changed` reflect the parent's repo, not cwd's.
|
|
14
|
+
function buildCleanEnv() {
|
|
15
|
+
const env = { ...process.env };
|
|
16
|
+
delete env.GIT_DIR;
|
|
17
|
+
delete env.GIT_INDEX_FILE;
|
|
18
|
+
delete env.GIT_WORK_TREE;
|
|
19
|
+
delete env.GIT_PREFIX;
|
|
20
|
+
return env;
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
function run(cmd, args, { cwd } = {}) {
|
|
10
24
|
try {
|
|
11
25
|
return execFileSync(cmd, args, {
|
|
12
26
|
cwd,
|
|
27
|
+
env: buildCleanEnv(),
|
|
13
28
|
encoding: 'utf8',
|
|
14
29
|
timeout: GIT_TIMEOUT_MS,
|
|
15
30
|
// Suppress git's own stderr noise (e.g. "fatal: not a git repository").
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// One-shot v2.70.0 upgrade banner.
|
|
2
|
+
// Split out of hook.mjs because hook.mjs has module-level side effects
|
|
3
|
+
// (notably `if (!event) process.exit(0)` at top level) that abort vitest
|
|
4
|
+
// workers if imported directly from a test. See test
|
|
5
|
+
// tests/hook-upgrade-banner.test.mjs.
|
|
6
|
+
|
|
7
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One-shot stderr banner on first SessionStart after v2.70.0 upgrade.
|
|
12
|
+
* Notifies users that the `### Deferred Work` block now reads from the
|
|
13
|
+
* deferred_work table (not high-importance observations as in v2.69.x).
|
|
14
|
+
* Idempotent via a marker file in `runtimeDir`; subsequent calls in the
|
|
15
|
+
* same project are silent.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} args
|
|
18
|
+
* @param {string} args.project Project name (used in banner + marker filename).
|
|
19
|
+
* @param {string} args.runtimeDir RUNTIME_DIR (test override; production passes hook-shared.RUNTIME_DIR).
|
|
20
|
+
*/
|
|
21
|
+
export function emitV270UpgradeBanner({ project, runtimeDir }) {
|
|
22
|
+
const marker = join(runtimeDir, `.deferred-block-migrated-${project}`);
|
|
23
|
+
if (existsSync(marker)) return;
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`[mem] v2.70.0 upgrade notice (project "${project}"): Deferred Work block now ` +
|
|
26
|
+
`backed by deferred_work table. To keep an obs visible there, run ` +
|
|
27
|
+
`\`claude-mem-lite defer add "<title>" --priority 3\`. ` +
|
|
28
|
+
`Pin to 2.69.x to revert.\n`
|
|
29
|
+
);
|
|
30
|
+
try { writeFileSync(marker, String(Date.now())); } catch { /* best-effort marker */ }
|
|
31
|
+
}
|
package/mem-cli.mjs
CHANGED
|
@@ -28,6 +28,10 @@ import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
|
28
28
|
// move each cmdXxx into its own cli/<cmd>.mjs; mem-cli.mjs becomes pure dispatch.
|
|
29
29
|
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints } from './cli/common.mjs';
|
|
30
30
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
31
|
+
import {
|
|
32
|
+
insertDeferred, listOpenWithOrdinal, dropDeferred,
|
|
33
|
+
resolveDeferredIds, closeDeferredItems,
|
|
34
|
+
} from './lib/deferred-work.mjs';
|
|
31
35
|
|
|
32
36
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
33
37
|
|
|
@@ -884,7 +888,7 @@ function cmdSave(db, args) {
|
|
|
884
888
|
const { positional, flags } = parseArgs(args);
|
|
885
889
|
const text = positional.join(' ');
|
|
886
890
|
if (!text) {
|
|
887
|
-
fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T]');
|
|
891
|
+
fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T] [--closes-deferred 1,D#42]');
|
|
888
892
|
return;
|
|
889
893
|
}
|
|
890
894
|
|
|
@@ -914,15 +918,60 @@ function cmdSave(db, args) {
|
|
|
914
918
|
return;
|
|
915
919
|
}
|
|
916
920
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
921
|
+
// --closes-deferred parsing: accepts comma-separated mixed tokens
|
|
922
|
+
// ("1,D#42,3") with bare integers treated as ordinals and "D#N" as raw ids.
|
|
923
|
+
// We pre-parse tokens here (cheap, syntax-only) but defer resolveDeferredIds
|
|
924
|
+
// INTO the transaction, AFTER the dedup check. Resolving outside the
|
|
925
|
+
// transaction would throw on the duplicate-replay path: the previously-
|
|
926
|
+
// closed deferred row is no longer 'open', so ordinal/id resolution would
|
|
927
|
+
// crash even though the duplicate short-circuit makes closure a no-op.
|
|
928
|
+
// Resolving inside the dedup-gated branch keeps "save the same content
|
|
929
|
+
// twice" idempotent (mirrors server.mjs:934 dedup-skip-closure intent).
|
|
930
|
+
let closesTokens = null;
|
|
931
|
+
if (flags['closes-deferred'] !== undefined && flags['closes-deferred'] !== false) {
|
|
932
|
+
const raw = String(flags['closes-deferred']);
|
|
933
|
+
closesTokens = raw.split(',').map(t => t.trim()).filter(Boolean).map(t => {
|
|
934
|
+
return /^\d+$/.test(t) ? parseInt(t, 10) : t;
|
|
935
|
+
});
|
|
936
|
+
if (closesTokens.length === 0) {
|
|
937
|
+
fail('[mem] --closes-deferred requires at least one token (integer ordinal or D#N)');
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
let result;
|
|
943
|
+
let closesIds = null;
|
|
944
|
+
try {
|
|
945
|
+
result = db.transaction(() => {
|
|
946
|
+
const r = saveObservation(db, {
|
|
947
|
+
content: text,
|
|
948
|
+
title: flags.title,
|
|
949
|
+
type,
|
|
950
|
+
importance: rawImp,
|
|
951
|
+
project,
|
|
952
|
+
files: saveFiles,
|
|
953
|
+
lesson_learned: rawLesson,
|
|
954
|
+
});
|
|
955
|
+
// Skip closure on dedup short-circuit — the obs row already exists, so
|
|
956
|
+
// the deferred item should NOT be re-closed by a duplicate save call.
|
|
957
|
+
// Resolving deferred ids only on the non-duplicate path keeps repeated
|
|
958
|
+
// save commands (with the same --closes-deferred) idempotent even after
|
|
959
|
+
// the deferred row has transitioned out of 'open'.
|
|
960
|
+
if (r.kind === 'duplicate') return r;
|
|
961
|
+
if (closesTokens) {
|
|
962
|
+
closesIds = resolveDeferredIds(db, project, closesTokens);
|
|
963
|
+
closeDeferredItems(db, closesIds, r.id);
|
|
964
|
+
}
|
|
965
|
+
return r;
|
|
966
|
+
})();
|
|
967
|
+
} catch (e) {
|
|
968
|
+
if (closesTokens) {
|
|
969
|
+
fail(`[mem] save with --closes-deferred failed: ${e.message}`);
|
|
970
|
+
} else {
|
|
971
|
+
fail(`[mem] save failed: ${e.message}`);
|
|
972
|
+
}
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
926
975
|
|
|
927
976
|
if (result.kind === 'duplicate') {
|
|
928
977
|
out(`[mem] Skipped: similar to existing #${result.existingId}. Use "claude-mem-lite get ${result.existingId}" to review.`);
|
|
@@ -930,7 +979,108 @@ function cmdSave(db, args) {
|
|
|
930
979
|
}
|
|
931
980
|
|
|
932
981
|
const lessonNote = result.lessonCaptured ? ' 💡lesson captured' : '';
|
|
933
|
-
|
|
982
|
+
const closedNote = closesIds && closesIds.length > 0
|
|
983
|
+
? ` Closed: ${closesIds.map(i => `D#${i}`).join(', ')}.`
|
|
984
|
+
: '';
|
|
985
|
+
out(`[mem] Saved #${result.id} [${result.type}] "${truncate(result.title, 80)}" (project: ${result.project})${lessonNote}${closedNote}`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ─── cmdDefer (sub-dispatch: add | list | drop) ──────────────────────────────
|
|
989
|
+
|
|
990
|
+
function cmdDefer(db, args) {
|
|
991
|
+
const sub = args[0];
|
|
992
|
+
const rest = args.slice(1);
|
|
993
|
+
switch (sub) {
|
|
994
|
+
case 'add': cmdDeferAdd(db, rest); break;
|
|
995
|
+
case 'list': cmdDeferList(db, rest); break;
|
|
996
|
+
case 'drop': cmdDeferDrop(db, rest); break;
|
|
997
|
+
default:
|
|
998
|
+
fail('[mem] Usage: claude-mem-lite defer <add|list|drop> ...');
|
|
999
|
+
fail('[mem] defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P]');
|
|
1000
|
+
fail('[mem] defer list [--project P] [--limit N]');
|
|
1001
|
+
fail('[mem] defer drop <id-or-D#N> --reason "<reason>" [--project P]');
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function cmdDeferAdd(db, args) {
|
|
1006
|
+
const { positional, flags } = parseArgs(args);
|
|
1007
|
+
const title = positional.join(' ').trim();
|
|
1008
|
+
if (!title) {
|
|
1009
|
+
fail('[mem] Usage: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P]');
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const priority = flags.priority !== undefined ? parseInt(flags.priority, 10) : 2;
|
|
1013
|
+
if (![1, 2, 3].includes(priority)) {
|
|
1014
|
+
fail(`[mem] Invalid --priority "${flags.priority}". Must be 1 (low), 2 (normal), or 3 (urgent).`);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1018
|
+
const detail = typeof flags.detail === 'string' ? flags.detail : null;
|
|
1019
|
+
const files = flags.files
|
|
1020
|
+
? flags.files.split(',').map(f => f.trim()).filter(Boolean)
|
|
1021
|
+
: null;
|
|
1022
|
+
|
|
1023
|
+
let r;
|
|
1024
|
+
try {
|
|
1025
|
+
r = insertDeferred(db, { project, title, priority, detail, files });
|
|
1026
|
+
} catch (e) {
|
|
1027
|
+
fail(`[mem] defer add failed: ${e.message}`);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
// Compute the freshly-inserted row's ordinal for an immediately-actionable
|
|
1031
|
+
// response ("ok, deferred this as item N"). Mirrors server.mjs:980.
|
|
1032
|
+
const open = listOpenWithOrdinal(db, project, 50);
|
|
1033
|
+
const ord = open.find(o => o.id === r.id)?.ordinal ?? '?';
|
|
1034
|
+
out(`[mem] Deferred as D#${r.id} (item ${ord}) in project "${project}".`);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function cmdDeferList(db, args) {
|
|
1038
|
+
const { flags } = parseArgs(args);
|
|
1039
|
+
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1040
|
+
const limit = parseIntFlag(flags.limit, { name: '--limit', defaultValue: 10, max: 100 });
|
|
1041
|
+
const list = listOpenWithOrdinal(db, project, limit);
|
|
1042
|
+
if (list.length === 0) {
|
|
1043
|
+
out(`[mem] No open deferred items in project "${project}".`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
out(`[mem] Open deferred items (project "${project}"):`);
|
|
1047
|
+
for (const r of list) {
|
|
1048
|
+
const pTag = r.priority === 3 ? '🔴' : r.priority === 1 ? '⚪' : '🟡';
|
|
1049
|
+
out(` ${r.ordinal}. ${pTag} [P${r.priority}] ${r.title} (D#${r.id})`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function cmdDeferDrop(db, args) {
|
|
1054
|
+
const { positional, flags } = parseArgs(args);
|
|
1055
|
+
if (positional.length === 0) {
|
|
1056
|
+
fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N> --reason "<reason>" [--project P]');
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const reason = flags.reason;
|
|
1060
|
+
if (!reason || typeof reason !== 'string' || reason.trim().length === 0) {
|
|
1061
|
+
fail('[mem] defer drop requires --reason "<non-empty string>"');
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const rawTok = positional[0];
|
|
1065
|
+
// Accept both bare integer (ordinal) and "D#N" string. Mirrors the MCP
|
|
1066
|
+
// mem_defer_drop input contract (server.mjs:1025) by using the same
|
|
1067
|
+
// single-element resolveDeferredIds call.
|
|
1068
|
+
const token = /^\d+$/.test(rawTok) ? parseInt(rawTok, 10) : rawTok;
|
|
1069
|
+
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1070
|
+
|
|
1071
|
+
let realId;
|
|
1072
|
+
try {
|
|
1073
|
+
[realId] = resolveDeferredIds(db, project, [token]);
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
fail(`[mem] defer drop: ${e.message}`);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const r = dropDeferred(db, realId, reason);
|
|
1079
|
+
if (r.changed === 0) {
|
|
1080
|
+
out(`[mem] D#${realId} was not in 'open' status — drop is a no-op.`);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
out(`[mem] Dropped D#${realId} in project "${project}". Reason: ${reason.trim()}`);
|
|
934
1084
|
}
|
|
935
1085
|
|
|
936
1086
|
// N-1: Quality-focused stats for R-2 A/B baseline.
|
|
@@ -2178,6 +2328,19 @@ Commands:
|
|
|
2178
2328
|
--project P Project name
|
|
2179
2329
|
--files f1,f2 Comma-separated file paths
|
|
2180
2330
|
--lesson T Lesson learned (≤500 chars; alias: --lesson-learned)
|
|
2331
|
+
--closes-deferred 1,D#42 Close deferred items in same transaction
|
|
2332
|
+
|
|
2333
|
+
defer <action> First-class deferred work (v2.70+)
|
|
2334
|
+
add "<title>" Mark deferred work for next session
|
|
2335
|
+
--priority N 1-3 (default 2)
|
|
2336
|
+
--detail T Constraint + why deferred
|
|
2337
|
+
--files f1,f2 Comma-separated file paths
|
|
2338
|
+
--project P Project name
|
|
2339
|
+
list List open deferred items
|
|
2340
|
+
--limit N Max results (default 10)
|
|
2341
|
+
--project P Filter by project
|
|
2342
|
+
drop <D#N|ordinal> Drop a deferred item (no fix needed)
|
|
2343
|
+
--reason "..." Required audit trail
|
|
2181
2344
|
|
|
2182
2345
|
delete <id1,id2,...> Delete observations by ID
|
|
2183
2346
|
--confirm Execute deletion (preview by default)
|
|
@@ -2506,6 +2669,7 @@ export async function run(argv) {
|
|
|
2506
2669
|
case 'get': cmdGet(db, cmdArgs); break;
|
|
2507
2670
|
case 'timeline': cmdTimeline(db, cmdArgs); break;
|
|
2508
2671
|
case 'save': cmdSave(db, cmdArgs); break;
|
|
2672
|
+
case 'defer': cmdDefer(db, cmdArgs); break;
|
|
2509
2673
|
case 'delete': cmdDelete(db, cmdArgs); break;
|
|
2510
2674
|
case 'update': cmdUpdate(db, cmdArgs); break;
|
|
2511
2675
|
case 'export': cmdExport(db, cmdArgs); break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.70.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
"lib/metrics.mjs",
|
|
63
63
|
"lib/mem-override.mjs",
|
|
64
64
|
"lib/save-observation.mjs",
|
|
65
|
+
"lib/deferred-work.mjs",
|
|
66
|
+
"lib/upgrade-banner.mjs",
|
|
65
67
|
"cli/common.mjs",
|
|
66
68
|
"cli/fts-check.mjs",
|
|
67
69
|
"cli/doctor.mjs",
|
package/schema.mjs
CHANGED
|
@@ -40,7 +40,12 @@ export const REGISTRY_DB_PATH = join(DB_DIR, 'resource-registry.db');
|
|
|
40
40
|
// DROP+CREATE so DBs that picked up the strict v29 trigger get the UUID-
|
|
41
41
|
// gated body. Required because `CREATE TRIGGER IF NOT EXISTS` is a no-op
|
|
42
42
|
// when the trigger already exists, even with a different body.
|
|
43
|
-
|
|
43
|
+
// v31 (v2.70.0): deferred_work table — first-class carry-forward surface.
|
|
44
|
+
// Decoupled from observations: different decay semantics (no time decay; older
|
|
45
|
+
// items rank HIGHER as tech debt accumulates), different lifecycle (mutable
|
|
46
|
+
// status open→done|dropped vs immutable obs). Closure tied to obs via
|
|
47
|
+
// closed_by_obs_id FK with ON DELETE SET NULL (audit trail preserved).
|
|
48
|
+
export const CURRENT_SCHEMA_VERSION = 31;
|
|
44
49
|
|
|
45
50
|
const CORE_SCHEMA = `
|
|
46
51
|
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
|
@@ -541,6 +546,33 @@ export function initSchema(db) {
|
|
|
541
546
|
)
|
|
542
547
|
`);
|
|
543
548
|
|
|
549
|
+
// ─── v31 (v2.70.0): deferred_work — carry-forward TODOs ─────────────────────
|
|
550
|
+
// Independent table because decay semantics are inverted (older = higher
|
|
551
|
+
// priority signal) and lifecycle is mutable (status flips). Project-scoped
|
|
552
|
+
// queries; no FTS5 (per-project N expected ≪ 100). Idempotent migration.
|
|
553
|
+
db.exec(`
|
|
554
|
+
CREATE TABLE IF NOT EXISTS deferred_work (
|
|
555
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
556
|
+
project TEXT NOT NULL,
|
|
557
|
+
title TEXT NOT NULL,
|
|
558
|
+
detail TEXT,
|
|
559
|
+
priority INTEGER NOT NULL DEFAULT 2,
|
|
560
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','done','dropped')),
|
|
561
|
+
created_at_epoch INTEGER NOT NULL,
|
|
562
|
+
closed_at_epoch INTEGER,
|
|
563
|
+
closed_by_obs_id INTEGER REFERENCES observations(id) ON DELETE SET NULL,
|
|
564
|
+
drop_reason TEXT,
|
|
565
|
+
source_session_id TEXT,
|
|
566
|
+
source_prompt_id INTEGER REFERENCES user_prompts(id) ON DELETE SET NULL,
|
|
567
|
+
files TEXT
|
|
568
|
+
);
|
|
569
|
+
CREATE INDEX IF NOT EXISTS idx_deferred_open
|
|
570
|
+
ON deferred_work(project, priority DESC, created_at_epoch ASC)
|
|
571
|
+
WHERE status = 'open';
|
|
572
|
+
CREATE INDEX IF NOT EXISTS idx_deferred_closed_by
|
|
573
|
+
ON deferred_work(closed_by_obs_id) WHERE closed_by_obs_id IS NOT NULL;
|
|
574
|
+
`);
|
|
575
|
+
|
|
544
576
|
// Record schema version for fast-path on subsequent calls
|
|
545
577
|
db.exec('CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)');
|
|
546
578
|
db.transaction(() => {
|
package/server.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { reRankWithContext, markSuperseded, autoBoostIfNeeded, runIdleCleanup, b
|
|
|
13
13
|
import { searchObservationsHybrid, findFtsAnchor } from './search-engine.mjs';
|
|
14
14
|
import { effectiveQuiet } from './hook-shared.mjs';
|
|
15
15
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
16
|
-
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
|
|
16
|
+
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema, memDeferSchema, memDeferListSchema, memDeferDropSchema, tools as TOOL_DEFS } from './tool-schemas.mjs';
|
|
17
17
|
|
|
18
18
|
// Lookup helper: all user-facing tool descriptions live in tool-schemas.mjs
|
|
19
19
|
// (discouragement-style, Task 5). This keeps server.mjs from drifting.
|
|
@@ -30,6 +30,10 @@ import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
|
30
30
|
import { searchResources } from './registry-retriever.mjs';
|
|
31
31
|
import { probeOtherSources as probeIdSources, parseIdToken, bucketIdTokens } from './lib/id-routing.mjs';
|
|
32
32
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
33
|
+
import {
|
|
34
|
+
insertDeferred, listOpenWithOrdinal, dropDeferred,
|
|
35
|
+
resolveDeferredIds, closeDeferredItems,
|
|
36
|
+
} from './lib/deferred-work.mjs';
|
|
33
37
|
import { getVocabulary, rebuildVocabulary, _resetVocabCache, computeVector } from './tfidf.mjs';
|
|
34
38
|
import { createRequire } from 'module';
|
|
35
39
|
|
|
@@ -907,22 +911,121 @@ server.registerTool(
|
|
|
907
911
|
safeHandler(async (args) => {
|
|
908
912
|
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
909
913
|
const project = args.project || inferProject();
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
914
|
+
|
|
915
|
+
let closesIds = null;
|
|
916
|
+
let result;
|
|
917
|
+
try {
|
|
918
|
+
result = db.transaction(() => {
|
|
919
|
+
const r = saveObservation(db, {
|
|
920
|
+
content: args.content,
|
|
921
|
+
title: args.title,
|
|
922
|
+
type: args.type || 'discovery',
|
|
923
|
+
importance: args.importance,
|
|
924
|
+
project,
|
|
925
|
+
files: args.files || [],
|
|
926
|
+
lesson_learned: args.lesson_learned,
|
|
927
|
+
});
|
|
928
|
+
if (r.kind === 'duplicate') return r; // dedup short-circuits BEFORE resolver — replay is idempotent
|
|
929
|
+
// Resolve INSIDE tx + after dedup check so duplicate replays don't throw on
|
|
930
|
+
// already-closed items. Mirrors mem-cli.mjs cmdSave shape.
|
|
931
|
+
if (args.closes_deferred && args.closes_deferred.length > 0) {
|
|
932
|
+
closesIds = resolveDeferredIds(db, project, args.closes_deferred);
|
|
933
|
+
closeDeferredItems(db, closesIds, r.id);
|
|
934
|
+
}
|
|
935
|
+
return r;
|
|
936
|
+
})();
|
|
937
|
+
} catch (e) {
|
|
938
|
+
if (args.closes_deferred && args.closes_deferred.length > 0) {
|
|
939
|
+
// Re-throw with a clearer prefix so MCP error response names the
|
|
940
|
+
// contract failure — gate on caller intent (args.closes_deferred) since
|
|
941
|
+
// closesIds is closure-scoped and may not have been assigned before throw.
|
|
942
|
+
throw new Error(`mem_save with closes_deferred failed: ${e.message}`, { cause: e });
|
|
943
|
+
}
|
|
944
|
+
throw e; // unwrapped — preserves original message + stack
|
|
945
|
+
}
|
|
919
946
|
|
|
920
947
|
if (result.kind === 'duplicate') {
|
|
921
948
|
return { content: [{ type: 'text', text: `Skipped: similar to existing #${result.existingId} in project "${project}". Use mem_get(ids=[${result.existingId}]) to review.` }] };
|
|
922
949
|
}
|
|
923
950
|
|
|
924
951
|
const lessonNote = result.lessonCaptured ? ` 💡lesson captured` : '';
|
|
925
|
-
|
|
952
|
+
const closedNote = closesIds && closesIds.length > 0
|
|
953
|
+
? ` Closed deferred: ${closesIds.map(i => `D#${i}`).join(', ')}.`
|
|
954
|
+
: '';
|
|
955
|
+
return { content: [{ type: 'text', text: `Saved as observation #${result.id} [${result.type}] in project "${project}".${lessonNote}${closedNote}` }] };
|
|
956
|
+
})
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
// ─── Tool: mem_defer ────────────────────────────────────────────────────────
|
|
960
|
+
|
|
961
|
+
server.registerTool(
|
|
962
|
+
'mem_defer',
|
|
963
|
+
{
|
|
964
|
+
description: descriptionOf('mem_defer'),
|
|
965
|
+
inputSchema: memDeferSchema,
|
|
966
|
+
},
|
|
967
|
+
safeHandler(async (args) => {
|
|
968
|
+
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
969
|
+
const project = args.project || inferProject();
|
|
970
|
+
const r = insertDeferred(db, {
|
|
971
|
+
project,
|
|
972
|
+
title: args.title,
|
|
973
|
+
priority: args.priority ?? 2,
|
|
974
|
+
detail: args.detail ?? null,
|
|
975
|
+
files: args.files ?? null,
|
|
976
|
+
});
|
|
977
|
+
// Compute the ordinal for the freshly-inserted row so the response is
|
|
978
|
+
// immediately actionable ("ok, I deferred this as item 1").
|
|
979
|
+
const open = listOpenWithOrdinal(db, project, 50);
|
|
980
|
+
const ord = open.find(o => o.id === r.id)?.ordinal ?? null;
|
|
981
|
+
return { content: [{ type: 'text', text:
|
|
982
|
+
`Deferred as D#${r.id} (item ${ord ?? '?'}) in project "${project}" — surfaces in next SessionStart banner.` }] };
|
|
983
|
+
})
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
// ─── Tool: mem_defer_list ───────────────────────────────────────────────────
|
|
987
|
+
|
|
988
|
+
server.registerTool(
|
|
989
|
+
'mem_defer_list',
|
|
990
|
+
{
|
|
991
|
+
description: descriptionOf('mem_defer_list'),
|
|
992
|
+
inputSchema: memDeferListSchema,
|
|
993
|
+
},
|
|
994
|
+
safeHandler(async (args) => {
|
|
995
|
+
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
996
|
+
const project = args.project || inferProject();
|
|
997
|
+
const list = listOpenWithOrdinal(db, project, args.limit ?? 10);
|
|
998
|
+
if (list.length === 0) {
|
|
999
|
+
return { content: [{ type: 'text', text: `No open deferred items in project "${project}".` }] };
|
|
1000
|
+
}
|
|
1001
|
+
const lines = [`Open deferred items (project "${project}"):`];
|
|
1002
|
+
for (const r of list) {
|
|
1003
|
+
const pTag = r.priority === 3 ? '🔴' : r.priority === 1 ? '⚪' : '🟡';
|
|
1004
|
+
lines.push(`${r.ordinal}. ${pTag} [P${r.priority}] ${r.title} (D#${r.id})`);
|
|
1005
|
+
}
|
|
1006
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1007
|
+
})
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
// ─── Tool: mem_defer_drop ───────────────────────────────────────────────────
|
|
1011
|
+
|
|
1012
|
+
server.registerTool(
|
|
1013
|
+
'mem_defer_drop',
|
|
1014
|
+
{
|
|
1015
|
+
description: descriptionOf('mem_defer_drop'),
|
|
1016
|
+
inputSchema: memDeferDropSchema,
|
|
1017
|
+
},
|
|
1018
|
+
safeHandler(async (args) => {
|
|
1019
|
+
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
1020
|
+
const project = args.project || inferProject();
|
|
1021
|
+
// Resolve id (accept D#N or ordinal int) via resolveDeferredIds with a
|
|
1022
|
+
// single-element array — reuses the same project + status validation.
|
|
1023
|
+
const [realId] = resolveDeferredIds(db, project, [args.id]);
|
|
1024
|
+
const r = dropDeferred(db, realId, args.reason);
|
|
1025
|
+
if (r.changed === 0) {
|
|
1026
|
+
return { content: [{ type: 'text', text: `D#${realId} was not in 'open' status — drop is a no-op.` }] };
|
|
1027
|
+
}
|
|
1028
|
+
return { content: [{ type: 'text', text: `Dropped D#${realId} in project "${project}". Reason: ${args.reason}` }] };
|
|
926
1029
|
})
|
|
927
1030
|
);
|
|
928
1031
|
|
|
@@ -2038,11 +2141,15 @@ server.registerTool(
|
|
|
2038
2141
|
);
|
|
2039
2142
|
|
|
2040
2143
|
// ─── Hidden tool filter ─────────────────────────────────────────────────────
|
|
2041
|
-
// All
|
|
2042
|
-
//
|
|
2043
|
-
//
|
|
2044
|
-
//
|
|
2045
|
-
//
|
|
2144
|
+
// All tools are registered (so `tools/call <name>` still resolves for scripts
|
|
2145
|
+
// and direct MCP clients), but only the core tools appear in the `tools/list`
|
|
2146
|
+
// response. Hiding the maintenance/admin tools keeps Claude Code's startup
|
|
2147
|
+
// context small while preserving the contract that the plugin dogfoods (see
|
|
2148
|
+
// CLAUDE.md §Mem usage contract and adopt-content.mjs).
|
|
2149
|
+
// Surface counts as of v2.70.0: 9 core (mem_search/recent/timeline/get/save/
|
|
2150
|
+
// recall + mem_defer/mem_defer_list/mem_defer_drop) + 11 hidden (maintenance/
|
|
2151
|
+
// admin/specialized) = 20 registered; tests/tool-schemas.test.mjs is the
|
|
2152
|
+
// authoritative count.
|
|
2046
2153
|
//
|
|
2047
2154
|
// Safe because:
|
|
2048
2155
|
// - Protocol-layer override: we replace the mcp.js default ListTools
|
|
@@ -2055,9 +2162,9 @@ const HIDDEN_TOOL_NAMES = new Set(
|
|
|
2055
2162
|
);
|
|
2056
2163
|
|
|
2057
2164
|
// Opt-out: setting CLAUDE_MEM_ALL_TOOLS=1 restores pre-v2.34.0 behavior where
|
|
2058
|
-
//
|
|
2059
|
-
// autonomously invoking the now-hidden maintenance tools can use this as
|
|
2060
|
-
// immediate escape hatch while adopting the CLI entry points documented in
|
|
2165
|
+
// every registered tool is visible in `tools/list`. Users who relied on Claude
|
|
2166
|
+
// Code autonomously invoking the now-hidden maintenance tools can use this as
|
|
2167
|
+
// an immediate escape hatch while adopting the CLI entry points documented in
|
|
2061
2168
|
// adopt-content.mjs / README.
|
|
2062
2169
|
const EXPOSE_ALL_TOOLS = process.env.CLAUDE_MEM_ALL_TOOLS === '1';
|
|
2063
2170
|
|
|
@@ -2080,7 +2187,7 @@ if (!EXPOSE_ALL_TOOLS) {
|
|
|
2080
2187
|
// harnesses stay silent.
|
|
2081
2188
|
if (!effectiveQuiet()) {
|
|
2082
2189
|
const status = EXPOSE_ALL_TOOLS
|
|
2083
|
-
?
|
|
2190
|
+
? `all ${TOOL_DEFS.length} tools exposed via CLAUDE_MEM_ALL_TOOLS=1`
|
|
2084
2191
|
: `tools/list narrowed to ${TOOL_DEFS.length - HIDDEN_TOOL_NAMES.size} core tools (${HIDDEN_TOOL_NAMES.size} hidden but callable by exact name; unset CLAUDE_MEM_ALL_TOOLS to keep, set =1 to restore all)`;
|
|
2085
2192
|
process.stderr.write(`[claude-mem-lite v${PKG_VERSION}] ${status}\n`);
|
|
2086
2193
|
}
|
package/source-files.mjs
CHANGED
|
@@ -63,6 +63,13 @@ export const SOURCE_FILES = [
|
|
|
63
63
|
// mem-cli.mjs::cmdSave and server.mjs::mem_save. Statically imported from both
|
|
64
64
|
// entry points; missing it from the manifest broke MCP saves on auto-update.
|
|
65
65
|
'lib/save-observation.mjs',
|
|
66
|
+
// v2.70 deferred-work: carry-forward TODO primitives. Statically imported by
|
|
67
|
+
// server.mjs (mem_defer family) and mem-cli.mjs (defer subcommand).
|
|
68
|
+
'lib/deferred-work.mjs',
|
|
69
|
+
// v2.70 one-shot upgrade banner. Split out of hook.mjs because hook.mjs has
|
|
70
|
+
// module-level `process.exit(0)` side effects that abort vitest workers on
|
|
71
|
+
// direct import. Statically imported by hook.mjs SessionStart handler.
|
|
72
|
+
'lib/upgrade-banner.mjs',
|
|
66
73
|
];
|
|
67
74
|
|
|
68
75
|
/**
|
package/tool-schemas.mjs
CHANGED
|
@@ -140,6 +140,31 @@ export const memDeleteSchema = {
|
|
|
140
140
|
confirm: coerceBool.describe('false=preview what will be deleted, true=execute deletion'),
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
+
// Coerce closes_deferred input — accepts mixed array of integers (ordinals) and
|
|
144
|
+
// "D#<n>" strings (raw ids). Numeric strings are coerced to numbers (so MCP
|
|
145
|
+
// bridges that JSON-stringify ints don't drop them); D#-prefixed strings stay
|
|
146
|
+
// strings for the resolver. Other string shapes reject early at schema layer.
|
|
147
|
+
const coerceDeferredTokens = z.preprocess(
|
|
148
|
+
(v) => {
|
|
149
|
+
if (Array.isArray(v)) {
|
|
150
|
+
return v.map(x => {
|
|
151
|
+
if (typeof x === 'number') return x;
|
|
152
|
+
if (typeof x === 'string') {
|
|
153
|
+
const s = x.trim();
|
|
154
|
+
if (/^-?\d+$/.test(s)) return parseInt(s, 10);
|
|
155
|
+
return s;
|
|
156
|
+
}
|
|
157
|
+
return x;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return v;
|
|
161
|
+
},
|
|
162
|
+
z.array(z.union([
|
|
163
|
+
z.number().int().positive(),
|
|
164
|
+
z.string().regex(/^D#\d+$/, 'expected D#N (raw id) or positive integer (ordinal)'),
|
|
165
|
+
])).min(1).max(20)
|
|
166
|
+
);
|
|
167
|
+
|
|
143
168
|
export const memSaveSchema = {
|
|
144
169
|
content: z.string().min(1).max(50000).describe('Memory content to save'),
|
|
145
170
|
title: z.string().optional().describe('Short title'),
|
|
@@ -148,6 +173,7 @@ export const memSaveSchema = {
|
|
|
148
173
|
importance: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('Importance level: 1=routine, 2=notable, 3=critical (default: 2 for explicit saves)'),
|
|
149
174
|
files: coerceStringArray.optional().describe('File paths associated with this observation'),
|
|
150
175
|
lesson_learned: z.string().max(500).optional().describe('Key lesson or takeaway (for bugfix: root cause & fix; for decision: rationale)'),
|
|
176
|
+
closes_deferred: coerceDeferredTokens.optional().describe('Close one or more deferred_work items in the same project. Mixed array: bare integer = ordinal-within-project, "D#<n>" string = raw id. Transactional with the obs insert — a single invalid id rolls back the whole save.'),
|
|
151
177
|
};
|
|
152
178
|
|
|
153
179
|
export const memStatsSchema = {
|
|
@@ -252,6 +278,28 @@ export const memBrowseSchema = {
|
|
|
252
278
|
limit: coerceInt.pipe(z.number().int().min(1).max(100)).optional().describe('Max entries per tier (default 5, or 20 when filtering by tier)'),
|
|
253
279
|
};
|
|
254
280
|
|
|
281
|
+
export const memDeferSchema = {
|
|
282
|
+
title: z.string().min(1).max(200).describe('One-line subject of the deferred item'),
|
|
283
|
+
priority: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('1=low, 2=normal, 3=urgent (default: 2)'),
|
|
284
|
+
detail: z.string().max(2000).optional().describe('Optional longer description / constraint / why deferred'),
|
|
285
|
+
files: coerceStringArray.optional().describe('Optional file paths this deferred item concerns'),
|
|
286
|
+
project: z.string().optional().describe('Project name (default: inferred from CWD)'),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export const memDeferListSchema = {
|
|
290
|
+
project: z.string().optional().describe('Project name (default: inferred from CWD)'),
|
|
291
|
+
limit: coerceInt.pipe(z.number().int().min(1).max(50)).optional().describe('Max results (default 10)'),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const memDeferDropSchema = {
|
|
295
|
+
id: z.union([
|
|
296
|
+
coerceInt.pipe(z.number().int().positive()),
|
|
297
|
+
z.string().regex(/^D#\d+$/, 'expected D#N or positive integer'),
|
|
298
|
+
]).describe('Deferred item id — accepts D#N (raw id) or positive integer (ordinal-within-project)'),
|
|
299
|
+
reason: z.string().min(1).max(500).describe('Why this item is being dropped (required for audit trail)'),
|
|
300
|
+
project: z.string().optional().describe('Project name (default: inferred from CWD)'),
|
|
301
|
+
};
|
|
302
|
+
|
|
255
303
|
// ────────────────────────────────────────────────────────────────────────────
|
|
256
304
|
// Tool descriptions — discouragement style (Task 5, v2.31)
|
|
257
305
|
//
|
|
@@ -265,14 +313,16 @@ export const memBrowseSchema = {
|
|
|
265
313
|
// 40-60% vs. encouragement-style ("use this to..."). See tests/tool-schemas.test.mjs
|
|
266
314
|
// for the invariants this list must satisfy.
|
|
267
315
|
//
|
|
268
|
-
// Core vs hidden (v2.34.0): only
|
|
316
|
+
// Core vs hidden (v2.34.0, expanded v2.70.0): only 9 tools are exposed via MCP
|
|
317
|
+
// `tools/list` (the original 6 + mem_defer/mem_defer_list/mem_defer_drop). The
|
|
269
318
|
// remaining 11 stay registered — and are still callable by name at the MCP
|
|
270
319
|
// protocol level (`tools/call` by exact name) — but are omitted from the list
|
|
271
320
|
// response so they don't bloat every agent's startup context. The core set
|
|
272
321
|
// covers the hot paths the invited-memory contract promises (recall before
|
|
273
|
-
// Edit, save after bugfix, search/recent/timeline/get for retrieval
|
|
274
|
-
// tools are either maintenance
|
|
275
|
-
//
|
|
322
|
+
// Edit, save after bugfix, search/recent/timeline/get for retrieval, defer
|
|
323
|
+
// for cross-session carry-forward). Hidden tools are either maintenance
|
|
324
|
+
// (compress/maintain/optimize/fts_check), admin/infra
|
|
325
|
+
// (stats/export/update/delete), or specialized browsers
|
|
276
326
|
// (browse/registry/use) — all of which have CLI equivalents documented in
|
|
277
327
|
// `adopt-content.mjs`.
|
|
278
328
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -599,4 +649,57 @@ export const tools = [
|
|
|
599
649
|
inputSchema: memBrowseSchema,
|
|
600
650
|
hidden: true,
|
|
601
651
|
},
|
|
652
|
+
{
|
|
653
|
+
name: 'mem_defer',
|
|
654
|
+
description:
|
|
655
|
+
'Save a future-session TODO to the project carry-forward list. Surfaces in the next SessionStart `### Deferred Work` banner with an ordinal (e.g. "1") so user can say "处理1" / "handle item 1".\n' +
|
|
656
|
+
'\n' +
|
|
657
|
+
'DO NOT use when:\n' +
|
|
658
|
+
' - Work is in-flight this session (just do it; do not defer mid-task)\n' +
|
|
659
|
+
' - This-PR follow-up cleanup (file in tasks/, not deferred_work)\n' +
|
|
660
|
+
' - Already saved a bugfix obs for it (use mem_save closes_deferred instead)\n' +
|
|
661
|
+
'\n' +
|
|
662
|
+
'USE when:\n' +
|
|
663
|
+
' - User says "下次/next session/留给下个会话/defer to next round"\n' +
|
|
664
|
+
' - Wrap-up phase enumerates follow-up items for the next session\n' +
|
|
665
|
+
' - Bug surfaces but root cause is out of this session\'s scope\n' +
|
|
666
|
+
'\n' +
|
|
667
|
+
'Equivalent CLI: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail "..."] [--files a.mjs,b.mjs]',
|
|
668
|
+
inputSchema: memDeferSchema,
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: 'mem_defer_list',
|
|
672
|
+
description:
|
|
673
|
+
'List open deferred_work items for a project, ordered by priority DESC then age. Each item carries a per-project ordinal (1, 2, 3...) for "处理1"-style reference.\n' +
|
|
674
|
+
'\n' +
|
|
675
|
+
'DO NOT use when:\n' +
|
|
676
|
+
' - The SessionStart `### Deferred Work` block already shows the items (reuse that — same data)\n' +
|
|
677
|
+
' - You only need one item by id (use mem_get on the obs that closed it, or look up the row directly)\n' +
|
|
678
|
+
'\n' +
|
|
679
|
+
'USE when:\n' +
|
|
680
|
+
' - User asks "what was on the deferred list" / "what did I leave for next time"\n' +
|
|
681
|
+
' - About to refer to "item N" and need to confirm what N points to\n' +
|
|
682
|
+
' - Auditing carry-forward state across multiple sessions\n' +
|
|
683
|
+
'\n' +
|
|
684
|
+
'Equivalent CLI: claude-mem-lite defer list [--project X] [--limit 10]',
|
|
685
|
+
inputSchema: memDeferListSchema,
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'mem_defer_drop',
|
|
689
|
+
description:
|
|
690
|
+
'Mark a deferred_work item as dropped (no fix, no longer relevant) with a required reason. For real fixes, prefer mem_save({type:"bugfix", closes_deferred:[N]}) — establishes audit trail.\n' +
|
|
691
|
+
'\n' +
|
|
692
|
+
'DO NOT use when:\n' +
|
|
693
|
+
' - The item was actually fixed (use mem_save closes_deferred — audit linkage)\n' +
|
|
694
|
+
' - You want to delete the row (drops are soft — status flips, row stays)\n' +
|
|
695
|
+
' - The reason is trivial — drop reason is the only audit trail for "why no fix"\n' +
|
|
696
|
+
'\n' +
|
|
697
|
+
'USE when:\n' +
|
|
698
|
+
' - Originally-deferred item turns out to be a flaky test, not a real bug\n' +
|
|
699
|
+
' - Scope changed and the work is no longer needed\n' +
|
|
700
|
+
' - User explicitly says "drop the deferred X, never mind"\n' +
|
|
701
|
+
'\n' +
|
|
702
|
+
'Equivalent CLI: claude-mem-lite defer drop <D#N|ordinal> --reason "..."',
|
|
703
|
+
inputSchema: memDeferDropSchema,
|
|
704
|
+
},
|
|
602
705
|
];
|