claude-mem-lite 6.30.0 → 6.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/cli/common.mjs +6 -5
- package/cli/verify-apply.mjs +1 -1
- package/hook.mjs +2 -2
- package/install.mjs +13 -2
- package/lib/db-unusable.mjs +1 -1
- package/lib/maintain-core.mjs +2 -2
- package/lib/quiet-scope.mjs +11 -6
- package/lib/startup-dashboard.mjs +4 -1
- package/lib/stats-quality.mjs +1 -1
- package/mem-cli.mjs +12 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/setup.sh +11 -4
- package/secret-scrub.mjs +31 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.30.
|
|
12
|
+
"version": "6.30.1",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.30.
|
|
3
|
+
"version": "6.30.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/cli/common.mjs
CHANGED
|
@@ -650,10 +650,11 @@ export function formatProbeHints(probe) {
|
|
|
650
650
|
}
|
|
651
651
|
|
|
652
652
|
/**
|
|
653
|
-
* The line under one side of a scan's Needs-review pair (D#461), from reviewRows: type,
|
|
654
|
-
* date, the lesson or that there is none,
|
|
655
|
-
* with near-identical titles apart without
|
|
656
|
-
*
|
|
653
|
+
* The line under one side of a scan's Needs-review pair (D#461), from reviewRows: type, importance
|
|
654
|
+
* (update.md keeps the more important row, D#497), project, date, the lesson or that there is none,
|
|
655
|
+
* and the start of the narrative — enough to tell two rows with near-identical titles apart without
|
|
656
|
+
* `get`, which counts as an access.
|
|
657
|
+
* @param {{id: number, type?: string, importance?: number, project?: string, created_at_epoch?: number,
|
|
657
658
|
* narrative?: string|null, lesson_learned?: string|null}|undefined} row
|
|
658
659
|
* @returns {string}
|
|
659
660
|
*/
|
|
@@ -671,5 +672,5 @@ export function formatReviewSide(row) {
|
|
|
671
672
|
? `lesson: ${flat(row.lesson_learned, 100)}`
|
|
672
673
|
: 'no lesson';
|
|
673
674
|
const date = row.created_at_epoch ? new Date(row.created_at_epoch).toISOString().slice(0, 10) : '?';
|
|
674
|
-
return `#${row.id} ${row.type || '?'} · ${row.project} · ${date} · ${lesson} · ${flat(row.narrative, 120) || '(no narrative)'}`;
|
|
675
|
+
return `#${row.id} ${row.type || '?'} · importance ${row.importance ?? '?'} · ${row.project} · ${date} · ${lesson} · ${flat(row.narrative, 120) || '(no narrative)'}`;
|
|
675
676
|
}
|
package/cli/verify-apply.mjs
CHANGED
|
@@ -153,7 +153,7 @@ export function cmdVerifyApply(db, args) {
|
|
|
153
153
|
out(`[mem] Plan digest: ${digest}`);
|
|
154
154
|
out('[mem] Dry run — nothing written. After the user approves exactly this plan, run:');
|
|
155
155
|
out(
|
|
156
|
-
` ${SELF} verify-apply ${shellWord(file)} --project
|
|
156
|
+
` ${SELF} verify-apply ${shellWord(file)} --project=${shellWord(project)} --apply --digest ${digest}`,
|
|
157
157
|
);
|
|
158
158
|
return;
|
|
159
159
|
}
|
package/hook.mjs
CHANGED
|
@@ -3004,9 +3004,9 @@ function rekeyProjectOnce(db, project) {
|
|
|
3004
3004
|
? ''
|
|
3005
3005
|
: left === 1
|
|
3006
3006
|
? " 1 other stays under the old id (another directory's, or nothing shows whose); " +
|
|
3007
|
-
`list it with \`${CLI_INVOKE} recent 50 --project
|
|
3007
|
+
`list it with \`${CLI_INVOKE} recent 50 --project=${legacy}\`.`
|
|
3008
3008
|
: ` ${left} others stay under the old id (another directory's, or nothing shows whose); ` +
|
|
3009
|
-
`list them with \`${CLI_INVOKE} recent 50 --project
|
|
3009
|
+
`list them with \`${CLI_INVOKE} recent 50 --project=${legacy}\`.`;
|
|
3010
3010
|
queueHookSystemMessage(
|
|
3011
3011
|
`claude-mem-lite: this project's memory now has its own id, ${project}. Its old id, ${legacy}, ` +
|
|
3012
3012
|
'could be shared with other directories whose names are not plain ASCII. ' +
|
package/install.mjs
CHANGED
|
@@ -564,6 +564,15 @@ export function deployLockfile(projectDir, dataDir) {
|
|
|
564
564
|
return shrinkwrap;
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
function devNodeModules() {
|
|
568
|
+
try {
|
|
569
|
+
const pkg = createRequire(join(PROJECT_DIR, 'package.json')).resolve('better-sqlite3/package.json');
|
|
570
|
+
return dirname(dirname(pkg));
|
|
571
|
+
} catch {
|
|
572
|
+
return join(PROJECT_DIR, 'node_modules');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
567
576
|
export function deployCodeTree(IS_DEV) {
|
|
568
577
|
if (IS_DEV) {
|
|
569
578
|
log('Dev mode — creating symlinks in ~/.claude-mem-lite/...');
|
|
@@ -584,10 +593,12 @@ export function deployCodeTree(IS_DEV) {
|
|
|
584
593
|
const scriptsLink = join(DATA_DIR, 'scripts');
|
|
585
594
|
clearLinkPath(scriptsLink);
|
|
586
595
|
symlinkSync(join(PROJECT_DIR, 'scripts'), scriptsLink);
|
|
587
|
-
// Symlink node_modules
|
|
596
|
+
// Symlink node_modules/: the one this checkout loads its dependencies from. A worktree nested in
|
|
597
|
+
// the main checkout has none of its own and Node finds the one above it; linking
|
|
598
|
+
// PROJECT_DIR/node_modules left the link dangling there (D#489).
|
|
588
599
|
const nmLink = join(DATA_DIR, 'node_modules');
|
|
589
600
|
clearLinkPath(nmLink);
|
|
590
|
-
symlinkSync(
|
|
601
|
+
symlinkSync(devNodeModules(), nmLink);
|
|
591
602
|
// R10 P3-28: the registry/ symlink is gone. The directory left with the skill registry
|
|
592
603
|
// in v5.0.0, so the existsSync guard was permanently false and the branch was dead —
|
|
593
604
|
// clearLinkPath still ran, silently removing a stale ~/.claude-mem-lite/registry link
|
package/lib/db-unusable.mjs
CHANGED
|
@@ -224,7 +224,7 @@ export function dbUnusableRemedy(dbPath, err) {
|
|
|
224
224
|
*/
|
|
225
225
|
export function formatDbUnusableNotice({ dbPath, remedy }) {
|
|
226
226
|
const lines = [
|
|
227
|
-
'⚠️ [claude-mem-lite] Memory is OFF: the database file cannot be
|
|
227
|
+
'⚠️ [claude-mem-lite] Memory is OFF: the database file cannot be used.',
|
|
228
228
|
remedy.kind === 'schema'
|
|
229
229
|
? ` ${dbPath} exists but SQLite cannot read its schema.`
|
|
230
230
|
: ` ${dbPath} exists but SQLite does not recognise it as a database.`,
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -809,7 +809,7 @@ export function purgeStale(db, { projectFilter, baseParams, opCap = OP_CAP }, re
|
|
|
809
809
|
* lets autoBoost lift importance 1 -> 2. A plain SELECT by id touches no counter.
|
|
810
810
|
* @param {import('better-sqlite3').Database} db
|
|
811
811
|
* @param {number[]} ids
|
|
812
|
-
* @returns {Map<number, {id: number, type: string, project: string, created_at_epoch: number,
|
|
812
|
+
* @returns {Map<number, {id: number, type: string, importance: number, project: string, created_at_epoch: number,
|
|
813
813
|
* narrative: string|null, lesson_learned: string|null}>}
|
|
814
814
|
*/
|
|
815
815
|
export function reviewRows(db, ids) {
|
|
@@ -817,7 +817,7 @@ export function reviewRows(db, ids) {
|
|
|
817
817
|
if (uniq.length === 0) return new Map();
|
|
818
818
|
const rows = db
|
|
819
819
|
.prepare(
|
|
820
|
-
`SELECT id, type, project, created_at_epoch, narrative, lesson_learned FROM observations
|
|
820
|
+
`SELECT id, type, importance, project, created_at_epoch, narrative, lesson_learned FROM observations
|
|
821
821
|
WHERE id IN (${uniq.map(() => '?').join(',')})`,
|
|
822
822
|
)
|
|
823
823
|
.all(...uniq);
|
package/lib/quiet-scope.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { isAdopted as isAdoptedClaudeMd, readBlockAt } from '../claudemd.mjs';
|
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
13
|
import { dirname, join, resolve } from 'path';
|
|
14
14
|
import { PLUGIN_SLUG } from '../adopt-content.mjs';
|
|
15
|
-
import { isSharedAncestor, RULES_MD } from './local-steering.mjs';
|
|
15
|
+
import { isSharedAncestor, RULES_MD, userInstructionFiles } from './local-steering.mjs';
|
|
16
16
|
|
|
17
17
|
// Phase A (v2.31.3+): MEM_QUIET_HOOKS=1 drops descriptive hook/MCP-instruction
|
|
18
18
|
// bodies (File Lessons / Key Context headers, MCP WHEN-TO-USE & decision rules,
|
|
@@ -34,8 +34,12 @@ export function isQuietHooks() {
|
|
|
34
34
|
export function isAdoptedHere(cwd) {
|
|
35
35
|
try {
|
|
36
36
|
const resolved = cwd || process.env.CLAUDE_PROJECT_DIR || process.env.PWD || process.cwd();
|
|
37
|
-
if (
|
|
38
|
-
|
|
37
|
+
if (isAdoptedMemdir(memdirPath(resolved), PLUGIN_SLUG)) return true;
|
|
38
|
+
// Project instructions = managed-only leaves the files below out at launch, so only steering
|
|
39
|
+
// that SessionStart injects counts, as silentAutoAdopt decides there (D#501(2)).
|
|
40
|
+
const managedOnly = userInstructionFiles() === 'managed-only';
|
|
41
|
+
if (managedOnly) return isSteeringInjectedHere(resolved, { managedOnly });
|
|
42
|
+
if (isAdoptedClaudeMd(resolved, PLUGIN_SLUG)) return true;
|
|
39
43
|
// r3: the block auto-adopt wrote into CLAUDE.local.md loads whatever the switches say
|
|
40
44
|
// now (MEM_NO_AUTO_ADOPT only stops new writes). A file read, not a git call: this runs
|
|
41
45
|
// on every quiet check, and the project dir is the top-level in the normal case.
|
|
@@ -61,7 +65,7 @@ export function isAdoptedHere(cwd) {
|
|
|
61
65
|
* @param {string} cwd resolved project root
|
|
62
66
|
* @returns {boolean}
|
|
63
67
|
*/
|
|
64
|
-
function isSteeringInjectedHere(cwd) {
|
|
68
|
+
function isSteeringInjectedHere(cwd, { managedOnly = false } = {}) {
|
|
65
69
|
if (process.env.MEM_NO_AUTO_ADOPT === '1') return false;
|
|
66
70
|
if (isAutoAdoptDisabledFor(cwd)) return false;
|
|
67
71
|
// silentAutoAdopt holds a subdirectory session to an opt-out recorded for the repository root,
|
|
@@ -69,8 +73,9 @@ function isSteeringInjectedHere(cwd) {
|
|
|
69
73
|
// gets neither the steering nor the verbose sections.
|
|
70
74
|
const root = workTreeTop(cwd);
|
|
71
75
|
if (!root || root === resolve(cwd) || isSharedAncestor(root)) return true;
|
|
72
|
-
// Any directory up to the root, as silentAutoAdopt checks (D#177(2))
|
|
73
|
-
|
|
76
|
+
// Any directory up to the root, as silentAutoAdopt checks (D#177(2)); under managed-only it
|
|
77
|
+
// skips them, as none is loaded at launch.
|
|
78
|
+
for (let d = dirname(resolve(cwd)); !managedOnly; d = dirname(d)) {
|
|
74
79
|
if (readBlockAt(join(d, 'CLAUDE.md'), PLUGIN_SLUG).body !== null) return true;
|
|
75
80
|
if (d === root || dirname(d) === d) break;
|
|
76
81
|
}
|
|
@@ -10,6 +10,7 @@ import { readGitState } from './git-state.mjs';
|
|
|
10
10
|
import { readProjectTasks } from './task-reader.mjs';
|
|
11
11
|
import { recentPlans } from './plan-reader.mjs';
|
|
12
12
|
import { isAdoptedHere } from './quiet-scope.mjs';
|
|
13
|
+
import { userInstructionFiles } from './local-steering.mjs';
|
|
13
14
|
import {
|
|
14
15
|
HANDOFF_EXPIRY_EXIT,
|
|
15
16
|
UNCONSUMED_HANDOFF_SQL,
|
|
@@ -160,8 +161,10 @@ export function buildDashboard({
|
|
|
160
161
|
function buildAdoptHint({ projectPath, stubs }) {
|
|
161
162
|
if (process.env.MEM_NO_ADOPT_HINT === '1') return null;
|
|
162
163
|
if (process.env.MEM_QUIET_HOOKS === '1') return null;
|
|
164
|
+
// `adopt` writes CLAUDE.md, which Project instructions = managed-only leaves out at launch (D#501(1)).
|
|
165
|
+
if (userInstructionFiles() === 'managed-only') return null;
|
|
163
166
|
// stubs.adopted === true/false lets tests assert both branches without touching FS.
|
|
164
167
|
const adopted = stubs && 'adopted' in stubs ? stubs.adopted : isAdoptedHere(projectPath);
|
|
165
168
|
if (adopted) return null;
|
|
166
|
-
return `🧷 Invited-memory 未启用:\`${CLI_INVOKE} adopt\` 写入 CLAUDE.md
|
|
169
|
+
return `🧷 Invited-memory 未启用:\`${CLI_INVOKE} adopt\` 写入 CLAUDE.md 托管块;静音 \`MEM_NO_ADOPT_HINT=1\``;
|
|
167
170
|
}
|
package/lib/stats-quality.mjs
CHANGED
|
@@ -224,7 +224,7 @@ export function formatQualityReport(data) {
|
|
|
224
224
|
const purgeGap = (purgeNum - 10).toFixed(1);
|
|
225
225
|
const purgeStatus = purgeNum <= 10 ? '✅' : purgeNum <= 30 ? '🟡' : '🔴';
|
|
226
226
|
lines.push(
|
|
227
|
-
` ${purgeStatus} Pending purge ≤ 10% → currently ${purgePct}% (${purgeRow.pending_purge}/${purgeRow.compressed}) (gap ${purgeGap >= 0 ? '+' : ''}${purgeGap}pp)${purgeNum > 10 ? ` — preview: ${CLI_INVOKE} maintain execute --ops purge_stale${project ? ` --project
|
|
227
|
+
` ${purgeStatus} Pending purge ≤ 10% → currently ${purgePct}% (${purgeRow.pending_purge}/${purgeRow.compressed}) (gap ${purgeGap >= 0 ? '+' : ''}${purgeGap}pp)${purgeNum > 10 ? ` — preview: ${CLI_INVOKE} maintain execute --ops purge_stale${project ? ` --project=${shellWord(project)}` : ''} (add --confirm to delete the rows it counts)` : ''}`,
|
|
228
228
|
);
|
|
229
229
|
}
|
|
230
230
|
|
package/mem-cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { ensureDbWithWalRecovery, refreshPlannerStats, DB_PATH, DB_DIR, CODE_DIR } from './schema.mjs';
|
|
6
6
|
import { dirAccessError, walOpenBlocked, dataDirRemedy, WAL_BLOCKED_WHY } from './lib/wal-open-blocked.mjs';
|
|
7
7
|
import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
|
|
8
|
-
import { isFtsCorruptionError, FTS_CORRUPTION_REMEDY } from './lib/db-unusable.mjs';
|
|
8
|
+
import { isFtsCorruptionError, isDbUnusableError, FTS_CORRUPTION_REMEDY } from './lib/db-unusable.mjs';
|
|
9
9
|
import {
|
|
10
10
|
truncate,
|
|
11
11
|
queryLabel,
|
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
schemaSkewRemedy,
|
|
101
101
|
formatSchemaSkewNotice,
|
|
102
102
|
} from './lib/schema-skew.mjs';
|
|
103
|
-
import { CLI_PATH, CLI_INVOKE } from './cli-path.mjs';
|
|
103
|
+
import { CLI_PATH, CLI_INVOKE, shellWord } from './cli-path.mjs';
|
|
104
104
|
import {
|
|
105
105
|
parseArgs,
|
|
106
106
|
out,
|
|
@@ -3640,7 +3640,7 @@ async function cmdImportJsonl(db, argv) {
|
|
|
3640
3640
|
// — an orphan-only first import must not fall through to the "already imported"
|
|
3641
3641
|
// no-op branch below.
|
|
3642
3642
|
if (projectCheck.note) out(`[mem] Imported${projectCheck.note}.`);
|
|
3643
|
-
out(`[mem] Try: ${CLI_INVOKE} recent 5 --project
|
|
3643
|
+
out(`[mem] Try: ${CLI_INVOKE} recent 5 --project=${shellWord(project)}`);
|
|
3644
3644
|
} else if (totalRecognized > 0) {
|
|
3645
3645
|
// Lines WERE Claude Code transcript events but produced no new rows — the file
|
|
3646
3646
|
// was already imported (idempotent re-run) or carried no extractable content.
|
|
@@ -3932,12 +3932,14 @@ const MUTATING_FILTER_READS = {
|
|
|
3932
3932
|
// A command not in MUTATING_FILTER_READS names here when its run writes (round-2 pre-tag review
|
|
3933
3933
|
// P3-2: defer drop and fts-check rebuild ignored --dry-run silently; verify-apply --apply is left
|
|
3934
3934
|
// to its digest, since --apply is verify-apply's own flag and the dispatcher may not read it).
|
|
3935
|
+
// A preview is the same command without the flag that makes it write (the user's other flags stay),
|
|
3936
|
+
// or another command (`preview`).
|
|
3935
3937
|
const DRY_RUN_UNREAD = {
|
|
3936
|
-
compress: {
|
|
3938
|
+
compress: { without: '--execute' },
|
|
3937
3939
|
maintain: { preview: 'maintain scan' },
|
|
3938
|
-
optimize: {
|
|
3940
|
+
optimize: { without: '--run or --run-all' },
|
|
3939
3941
|
delete: { preview: null },
|
|
3940
|
-
'activity promote': {
|
|
3942
|
+
'activity promote': { without: '--execute' },
|
|
3941
3943
|
'activity delete': { preview: null },
|
|
3942
3944
|
'import-jsonl': { preview: null, writes: () => true },
|
|
3943
3945
|
'defer drop': { preview: null, writes: () => true },
|
|
@@ -4062,11 +4064,11 @@ async function runDispatch(argv) {
|
|
|
4062
4064
|
|
|
4063
4065
|
if (Object.hasOwn(DRY_RUN_UNREAD, mutatingKey)) {
|
|
4064
4066
|
const parsed = parseArgs(cmdArgs, { track: false });
|
|
4065
|
-
const { preview, writes } = DRY_RUN_UNREAD[mutatingKey];
|
|
4067
|
+
const { preview, without, writes } = DRY_RUN_UNREAD[mutatingKey];
|
|
4066
4068
|
if ((mutating ? mutating.writes(parsed) : writes(parsed)) && asksDryRun(parsed.flags['dry-run'])) {
|
|
4067
4069
|
fail(
|
|
4068
4070
|
`[mem] ${mutatingKey} does not read --dry-run — nothing was changed, since the run would have ` +
|
|
4069
|
-
`written. ${preview ? `Preview with:
|
|
4071
|
+
`written. ${without ? `Run it again without ${without} to preview.` : preview ? `Preview with: \`${CLI_INVOKE} ${preview}\`.` : 'It has no preview.'}`,
|
|
4070
4072
|
);
|
|
4071
4073
|
return;
|
|
4072
4074
|
}
|
|
@@ -4279,6 +4281,8 @@ async function runDispatch(argv) {
|
|
|
4279
4281
|
}
|
|
4280
4282
|
out(`[mem] Error: Cannot open database: ${e.message}`);
|
|
4281
4283
|
out(`[mem] DB path: ${DB_PATH}`);
|
|
4284
|
+
// The remedy overwrites the database, so it is doctor's to print, not this line's (D#488).
|
|
4285
|
+
if (isDbUnusableError(e)) out(`[mem] Next: run \`${CLI_INVOKE} doctor\`.`);
|
|
4282
4286
|
process.exitCode = 1;
|
|
4283
4287
|
return;
|
|
4284
4288
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.30.
|
|
3
|
+
"version": "6.30.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.30.
|
|
9
|
+
"version": "6.30.1",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.30.
|
|
3
|
+
"version": "6.30.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. FTS5 BM25 keyword search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/scripts/setup.sh
CHANGED
|
@@ -425,15 +425,16 @@ fi
|
|
|
425
425
|
# hooks remain in the config home's settings.json. A user who installed via global
|
|
426
426
|
# `claude-mem-lite install` and later switched to the marketplace plugin
|
|
427
427
|
# will run every hook twice (direct settings.json hooks AND plugin hooks)
|
|
428
|
-
# until they run
|
|
429
|
-
#
|
|
428
|
+
# until they run cleanup-hooks to clear the settings.json entries (not
|
|
429
|
+
# uninstall, which removes the plugin too). /plugin uninstall does not touch
|
|
430
|
+
# settings.json.
|
|
430
431
|
# CODE_DIR/runtime, same reason: the residue it warns about is stale hook entries in the config
|
|
431
432
|
# home's settings.json — one warning per config home, regardless of where the data lives.
|
|
432
433
|
RESIDUE_MARKER="$CODE_DIR/runtime/.residue-warned-v2.55$(marker_suffix "$CC_CONFIG_DIR/settings.json" "$CC_DEFAULT_CONFIG_DIR/settings.json")"
|
|
433
434
|
if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && ! -f "$RESIDUE_MARKER" ]]; then
|
|
434
435
|
SETTINGS="$CC_CONFIG_DIR/settings.json"
|
|
435
436
|
if [[ -f "$SETTINGS" ]]; then
|
|
436
|
-
SETTINGS_PATH="$SETTINGS" node -e '
|
|
437
|
+
SETTINGS_PATH="$SETTINGS" CFG_DIR="$CC_CONFIG_DIR" node -e '
|
|
437
438
|
const fs = require("fs");
|
|
438
439
|
try {
|
|
439
440
|
const raw = fs.readFileSync(process.env.SETTINGS_PATH, "utf8");
|
|
@@ -459,7 +460,13 @@ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && ! -f "$RESIDUE_MARKER" ]]; then
|
|
|
459
460
|
process.stderr.write("\x1b[33m⚠\x1b[0m Legacy direct-install hooks detected in " + process.env.SETTINGS_PATH + "\n");
|
|
460
461
|
process.stderr.write(" Events with stale entries: " + [...new Set(found)].join(", ") + "\n");
|
|
461
462
|
process.stderr.write(" These will fire alongside plugin hooks (each tool call runs twice).\n");
|
|
462
|
-
|
|
463
|
+
// A shell word, single-quoted unless plain (cli-path.mjs shellWord); \x27 is a quote here.
|
|
464
|
+
const q = (w) => (/^[\w@%+=:,.\/-]+$/.test(w) ? w : "\x27" + w.split("\x27").join("\x27\\\x27\x27") + "\x27");
|
|
465
|
+
// The config home this warning is about, named even when it is the default one: the shell
|
|
466
|
+
// the user types into may carry a CLAUDE_CONFIG_DIR that names another profile.
|
|
467
|
+
const env = "CLAUDE_CONFIG_DIR=" + q(process.env.CFG_DIR) + " ";
|
|
468
|
+
const fix = env + "node " + q(process.env.CLAUDE_PLUGIN_ROOT + "/cli.mjs") + " cleanup-hooks";
|
|
469
|
+
process.stderr.write(" Fix: run \x1b[1m" + fix + "\x1b[0m to clear settings.json,\n");
|
|
463
470
|
process.stderr.write(" then keep using the plugin install. (One-time warning.)\n\n");
|
|
464
471
|
process.exit(2);
|
|
465
472
|
}
|
package/secret-scrub.mjs
CHANGED
|
@@ -385,6 +385,35 @@ const PGP_CRC_RE = /^=[A-Za-z0-9+/]{4}$/;
|
|
|
385
385
|
// under a PGP header, a secret-key packet in the old or new format (`lQ…`, `xc…`/`xV…`).
|
|
386
386
|
const KEY_MAGIC_RE = /^(?:MII|MIG|MC4C|MHcC|b3BlbnNzaC1rZXktdjE)/;
|
|
387
387
|
const PGP_MAGIC_RE = /^(?:lQ|x[cV])/;
|
|
388
|
+
const B64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Whether base64 `s` starts the way a DER private key starts: a SEQUENCE holding an INTEGER 0 or 1
|
|
392
|
+
* (PKCS#1, PKCS#8, SEC1, DSA) or a SEQUENCE and an OID (encrypted PKCS#8; a public key's
|
|
393
|
+
* SubjectPublicKeyInfo starts that way too, so a public-key line under a PRIVATE KEY marker is
|
|
394
|
+
* erased as well). The prefix list above missed keys whose DER header begins otherwise: secp256k1
|
|
395
|
+
* `MHQC`, P-521 `MIHc`/`MIHu`, Ed448 `MEcC`, X448 `MEYC`, the 192/224-bit curves and others (D#504 F1).
|
|
396
|
+
*/
|
|
397
|
+
function isDerKeyStart(s) {
|
|
398
|
+
if (s.length < 12) return false;
|
|
399
|
+
const b = [];
|
|
400
|
+
for (let i = 0; i < 12; i += 4) {
|
|
401
|
+
let n = 0;
|
|
402
|
+
for (let k = 0; k < 4; k++) {
|
|
403
|
+
const v = B64_DIGITS.indexOf(s[i + k]);
|
|
404
|
+
if (v < 0) return false;
|
|
405
|
+
n = n * 64 + v;
|
|
406
|
+
}
|
|
407
|
+
b.push(n >> 16, (n >> 8) & 255, n & 255);
|
|
408
|
+
}
|
|
409
|
+
if (b[0] !== 0x30) return false;
|
|
410
|
+
const o = b[1] < 0x80 ? 2 : b[1] <= 0x83 ? b[1] - 0x7e : -1;
|
|
411
|
+
if (o < 0) return false;
|
|
412
|
+
if (b[o] === 0x02 && b[o + 1] === 0x01 && b[o + 2] <= 1) return true;
|
|
413
|
+
return b[o] === 0x30 && b[o + (b[o + 1] === 0x81 ? 3 : 2)] === 0x06;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const startsLikeKey = (s) => KEY_MAGIC_RE.test(s) || isDerKeyStart(s);
|
|
388
417
|
// RFC 1421 / RFC 4880 armor headers, before the body. Named, not any `Word:`: a `Note:` line is
|
|
389
418
|
// prose, and taking it for a header made the lone base64 line under it a key.
|
|
390
419
|
// RFC 1421's full set and tool-written `X-` headers count too (v6.19.2 pre-tag delta review F4:
|
|
@@ -684,7 +713,7 @@ function cutOffKeyEnd(text, b, be) {
|
|
|
684
713
|
// Under an opener whose prefix does not match the diff lines below it (`'+`, `rb'+`,
|
|
685
714
|
// `+KEY = '''`) the line still starts with its `+`; the magic is tested without it (D#167).
|
|
686
715
|
const body = first.replace(/^[-+]/, '');
|
|
687
|
-
const magic =
|
|
716
|
+
const magic = startsLikeKey(body) || (text.slice(b, be).includes('PGP') && PGP_MAGIC_RE.test(body));
|
|
688
717
|
return longs >= 2 || armors > 0 || longest >= 40 || magic ? end : -1;
|
|
689
718
|
}
|
|
690
719
|
|
|
@@ -797,7 +826,7 @@ function keyTailSpan(text, e, ee, floor) {
|
|
|
797
826
|
// 16-39 characters over an END named in prose is not (F7).
|
|
798
827
|
if (longs === 0) return null;
|
|
799
828
|
const b64ish = /[0-9+]|=$/.test(topCore);
|
|
800
|
-
if (!(longs >= 2 || longest >= 40 || crc || (clean && b64ish) ||
|
|
829
|
+
if (!(longs >= 2 || longest >= 40 || crc || (clean && b64ish) || startsLikeKey(topCore))) return null;
|
|
801
830
|
return [top, sentence ? bottom : ee];
|
|
802
831
|
}
|
|
803
832
|
|