@tyroneross/bookmark 0.2.0 → 0.3.2
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 +29 -12
- package/.claude-plugin/plugin.json +12 -8
- package/.codex-plugin/plugin.json +31 -0
- package/.mcp.json +8 -0
- package/CLAUDE.md +82 -38
- package/LICENSE +202 -21
- package/README.md +150 -57
- package/agents/snapshot-analyst.md +1 -1
- package/commands/bookmark.md +29 -0
- package/commands/feedback.md +37 -0
- package/commands/restore.md +14 -4
- package/commands/snapshot.md +19 -7
- package/commands/status.md +1 -1
- package/dist/cli/index.js +600 -66
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/snapshot-resolution.d.ts +15 -0
- package/dist/cli/snapshot-resolution.d.ts.map +1 -0
- package/dist/cli/snapshot-resolution.js +21 -0
- package/dist/cli/snapshot-resolution.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +150 -19
- package/dist/config.js.map +1 -1
- package/dist/context/freshness.d.ts +3 -0
- package/dist/context/freshness.d.ts.map +1 -0
- package/dist/context/freshness.js +29 -0
- package/dist/context/freshness.js.map +1 -0
- package/dist/context/handoff-prompt.d.ts +12 -0
- package/dist/context/handoff-prompt.d.ts.map +1 -0
- package/dist/context/handoff-prompt.js +29 -0
- package/dist/context/handoff-prompt.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +85 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +97 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +304 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/registry.d.ts +40 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +134 -0
- package/dist/registry.js.map +1 -0
- package/dist/restore/index.d.ts +13 -1
- package/dist/restore/index.d.ts.map +1 -1
- package/dist/restore/index.js +154 -256
- package/dist/restore/index.js.map +1 -1
- package/dist/setup/auto-setup.d.ts +8 -2
- package/dist/setup/auto-setup.d.ts.map +1 -1
- package/dist/setup/auto-setup.js +281 -66
- package/dist/setup/auto-setup.js.map +1 -1
- package/dist/setup/configure-hooks.d.ts +14 -3
- package/dist/setup/configure-hooks.d.ts.map +1 -1
- package/dist/setup/configure-hooks.js +156 -24
- package/dist/setup/configure-hooks.js.map +1 -1
- package/dist/snapshot/capture.d.ts +12 -10
- package/dist/snapshot/capture.d.ts.map +1 -1
- package/dist/snapshot/capture.js +36 -48
- package/dist/snapshot/capture.js.map +1 -1
- package/dist/snapshot/compress.d.ts +3 -3
- package/dist/snapshot/compress.d.ts.map +1 -1
- package/dist/snapshot/compress.js +16 -51
- package/dist/snapshot/compress.js.map +1 -1
- package/dist/snapshot/storage.d.ts +1 -0
- package/dist/snapshot/storage.d.ts.map +1 -1
- package/dist/snapshot/storage.js +7 -6
- package/dist/snapshot/storage.js.map +1 -1
- package/dist/threshold/state.d.ts +5 -0
- package/dist/threshold/state.d.ts.map +1 -1
- package/dist/threshold/state.js +28 -2
- package/dist/threshold/state.js.map +1 -1
- package/dist/threshold/token-usage.d.ts +29 -0
- package/dist/threshold/token-usage.d.ts.map +1 -0
- package/dist/threshold/token-usage.js +128 -0
- package/dist/threshold/token-usage.js.map +1 -0
- package/dist/trails/identity.d.ts +76 -0
- package/dist/trails/identity.d.ts.map +1 -0
- package/dist/trails/identity.js +117 -0
- package/dist/trails/identity.js.map +1 -0
- package/dist/trails/reader.d.ts +1 -1
- package/dist/trails/reader.js +2 -2
- package/dist/trails/reader.js.map +1 -1
- package/dist/trails/writer.d.ts +2 -8
- package/dist/trails/writer.d.ts.map +1 -1
- package/dist/trails/writer.js +33 -191
- package/dist/trails/writer.js.map +1 -1
- package/dist/transcript/extractor.d.ts +8 -9
- package/dist/transcript/extractor.d.ts.map +1 -1
- package/dist/transcript/extractor.js +97 -360
- package/dist/transcript/extractor.js.map +1 -1
- package/dist/types.d.ts +34 -41
- package/dist/types.d.ts.map +1 -1
- package/hooks/hooks.json +12 -14
- package/package.json +13 -7
- package/scripts/install-plugin.sh +48 -0
- package/skills/context-continuity/SKILL.md +43 -24
- package/commands/activate.md +0 -20
- package/dist/transcript/estimator.d.ts +0 -20
- package/dist/transcript/estimator.d.ts.map +0 -1
- package/dist/transcript/estimator.js +0 -95
- package/dist/transcript/estimator.js.map +0 -1
package/dist/registry.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
const REGISTRY_VERSION = '1.0.0';
|
|
5
|
+
const MAX_ENTRIES = 500;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a human-readable project name from a directory path.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
* 1. package.json "name" field in cwd
|
|
11
|
+
* 2. basename of git repo root (walk up until .git is found)
|
|
12
|
+
* 3. basename of cwd — unless it's the home directory
|
|
13
|
+
* 4. Fallback: basename(cwd) regardless
|
|
14
|
+
*/
|
|
15
|
+
export function resolveProjectName(cwd) {
|
|
16
|
+
const home = homedir();
|
|
17
|
+
// 1. package.json name
|
|
18
|
+
const pkgPath = join(cwd, 'package.json');
|
|
19
|
+
if (existsSync(pkgPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
22
|
+
if (pkg.name && pkg.name.trim()) {
|
|
23
|
+
// Strip scoped package prefix (@scope/name → name)
|
|
24
|
+
return pkg.name.replace(/^@[^/]+\//, '').trim();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fall through
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 2. Walk up looking for .git directory
|
|
32
|
+
let dir = cwd;
|
|
33
|
+
const root = dirname(home); // Stop before going above home's parent
|
|
34
|
+
while (dir && dir !== root && dir !== dirname(dir)) {
|
|
35
|
+
if (existsSync(join(dir, '.git'))) {
|
|
36
|
+
return basename(dir);
|
|
37
|
+
}
|
|
38
|
+
dir = dirname(dir);
|
|
39
|
+
}
|
|
40
|
+
// 3. Avoid returning home dir basename as the name
|
|
41
|
+
const cwdBase = basename(cwd);
|
|
42
|
+
if (cwd === home || cwdBase === basename(home)) {
|
|
43
|
+
return 'home';
|
|
44
|
+
}
|
|
45
|
+
return cwdBase;
|
|
46
|
+
}
|
|
47
|
+
export function getRegistryPath() {
|
|
48
|
+
return join(homedir(), '.bookmark', 'registry.json');
|
|
49
|
+
}
|
|
50
|
+
function ensureRegistryDir() {
|
|
51
|
+
const dir = join(homedir(), '.bookmark');
|
|
52
|
+
if (!existsSync(dir))
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
export function loadRegistry() {
|
|
56
|
+
const path = getRegistryPath();
|
|
57
|
+
if (!existsSync(path)) {
|
|
58
|
+
return { version: REGISTRY_VERSION, last_updated: 0, snapshots: [] };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
62
|
+
if (!parsed.snapshots)
|
|
63
|
+
parsed.snapshots = [];
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { version: REGISTRY_VERSION, last_updated: 0, snapshots: [] };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function saveRegistry(registry) {
|
|
71
|
+
ensureRegistryDir();
|
|
72
|
+
registry.last_updated = Date.now();
|
|
73
|
+
writeFileSync(getRegistryPath(), JSON.stringify(registry, null, 2), 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
export function appendToRegistry(snapshot, canonicalFile) {
|
|
76
|
+
try {
|
|
77
|
+
const registry = loadRegistry();
|
|
78
|
+
const entry = {
|
|
79
|
+
id: snapshot.snapshot_id,
|
|
80
|
+
timestamp: snapshot.timestamp,
|
|
81
|
+
trigger: snapshot.trigger,
|
|
82
|
+
project_path: snapshot.project_path,
|
|
83
|
+
project_name: resolveProjectName(snapshot.project_path),
|
|
84
|
+
canonical_file: canonicalFile,
|
|
85
|
+
files_changed_count: snapshot.files_changed.length,
|
|
86
|
+
};
|
|
87
|
+
// Dedupe by id, newest wins
|
|
88
|
+
registry.snapshots = [entry, ...registry.snapshots.filter(e => e.id !== entry.id)];
|
|
89
|
+
if (registry.snapshots.length > MAX_ENTRIES) {
|
|
90
|
+
registry.snapshots.length = MAX_ENTRIES;
|
|
91
|
+
}
|
|
92
|
+
registry.last_project = {
|
|
93
|
+
path: snapshot.project_path,
|
|
94
|
+
name: entry.project_name,
|
|
95
|
+
last_activity_at: snapshot.timestamp,
|
|
96
|
+
};
|
|
97
|
+
saveRegistry(registry);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Never break capture for registry bookkeeping
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function touchLastProject(projectPath) {
|
|
104
|
+
try {
|
|
105
|
+
const registry = loadRegistry();
|
|
106
|
+
registry.last_project = {
|
|
107
|
+
path: projectPath,
|
|
108
|
+
name: resolveProjectName(projectPath),
|
|
109
|
+
last_activity_at: Date.now(),
|
|
110
|
+
};
|
|
111
|
+
saveRegistry(registry);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Silent — never break restore for bookkeeping
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function findById(id) {
|
|
118
|
+
const registry = loadRegistry();
|
|
119
|
+
return registry.snapshots.find(e => e.id === id) ?? null;
|
|
120
|
+
}
|
|
121
|
+
export function getLastProject() {
|
|
122
|
+
return loadRegistry().last_project ?? null;
|
|
123
|
+
}
|
|
124
|
+
/** Prune entries whose canonical_file is missing. Lazy cleanup on read paths. */
|
|
125
|
+
export function pruneStale() {
|
|
126
|
+
const registry = loadRegistry();
|
|
127
|
+
const live = registry.snapshots.filter(e => existsSync(e.canonical_file));
|
|
128
|
+
if (live.length !== registry.snapshots.length) {
|
|
129
|
+
registry.snapshots = live;
|
|
130
|
+
saveRegistry(registry);
|
|
131
|
+
}
|
|
132
|
+
return registry;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/restore/index.d.ts
CHANGED
|
@@ -7,7 +7,19 @@ export interface RestoreOptions {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Generate restoration context for a SessionStart hook.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* v0.4 adds:
|
|
12
|
+
* 1. BOOKMARK_IDENTITY parsing + repo_path validation
|
|
13
|
+
* 2. Home-scope pointer delegation to a canonical repo bookmark
|
|
14
|
+
* 3. Hard staleness block at 72h (instead of soft warning only)
|
|
15
|
+
*
|
|
16
|
+
* Cascade:
|
|
17
|
+
* 1. bookmark.context.md — if present and useful
|
|
18
|
+
* 1a. If identity scope=home + points_to_canonical, follow the pointer
|
|
19
|
+
* 1b. If identity scope=repo and path mismatch, prefix a warning
|
|
20
|
+
* 1c. If age >= STALENESS_HARD_BLOCK_HOURS, return staleness block
|
|
21
|
+
* 2. LATEST.md — file tracking fallback
|
|
22
|
+
* 3. Empty
|
|
11
23
|
*/
|
|
12
24
|
export declare function restoreContext(options: RestoreOptions): HookOutput;
|
|
13
25
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/restore/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/restore/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAiB,MAAM,aAAa,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAAG,UAAU,CAAC;CACjD;AAWD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,CAqFlE"}
|
package/dist/restore/index.js
CHANGED
|
@@ -1,279 +1,180 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, statSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readLatestMd, getSnapshotCount } from '../snapshot/storage.js';
|
|
3
4
|
import { readContextMd } from '../trails/reader.js';
|
|
5
|
+
import { parseIdentity, validateRepoIdentity, followPointer, } from '../trails/identity.js';
|
|
4
6
|
import { loadState, saveState, resetForNewSession, incrementCompaction } from '../threshold/state.js';
|
|
5
7
|
import { loadConfig, getStoragePath } from '../config.js';
|
|
8
|
+
import { touchLastProject } from '../registry.js';
|
|
9
|
+
/**
|
|
10
|
+
* Hours after which a bookmark is considered too stale to auto-restore.
|
|
11
|
+
* Below this threshold: restore with a soft warning.
|
|
12
|
+
* At or above: hard-block — return an empty context with guidance instead
|
|
13
|
+
* of risking a confident wrong start from stale content.
|
|
14
|
+
*/
|
|
15
|
+
const STALENESS_HARD_BLOCK_HOURS = 72;
|
|
16
|
+
const STALENESS_SOFT_WARN_HOURS = 24;
|
|
6
17
|
/**
|
|
7
18
|
* Generate restoration context for a SessionStart hook.
|
|
8
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* v0.4 adds:
|
|
21
|
+
* 1. BOOKMARK_IDENTITY parsing + repo_path validation
|
|
22
|
+
* 2. Home-scope pointer delegation to a canonical repo bookmark
|
|
23
|
+
* 3. Hard staleness block at 72h (instead of soft warning only)
|
|
24
|
+
*
|
|
25
|
+
* Cascade:
|
|
26
|
+
* 1. bookmark.context.md — if present and useful
|
|
27
|
+
* 1a. If identity scope=home + points_to_canonical, follow the pointer
|
|
28
|
+
* 1b. If identity scope=repo and path mismatch, prefix a warning
|
|
29
|
+
* 1c. If age >= STALENESS_HARD_BLOCK_HOURS, return staleness block
|
|
30
|
+
* 2. LATEST.md — file tracking fallback
|
|
31
|
+
* 3. Empty
|
|
9
32
|
*/
|
|
10
33
|
export function restoreContext(options) {
|
|
11
34
|
const config = loadConfig(options.cwd);
|
|
12
35
|
const storagePath = getStoragePath(options.cwd, config);
|
|
13
36
|
const state = loadState(storagePath);
|
|
14
|
-
// Handle session state transitions
|
|
15
37
|
handleSessionTransition(storagePath, state, options, config.thresholds);
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
38
|
+
const stopRequestedPath = join(storagePath, '.stop-requested');
|
|
39
|
+
if (existsSync(stopRequestedPath)) {
|
|
40
|
+
try {
|
|
41
|
+
unlinkSync(stopRequestedPath);
|
|
42
|
+
}
|
|
43
|
+
catch { /* ignore */ }
|
|
19
44
|
}
|
|
20
|
-
|
|
21
|
-
if (options.source === 'resume') {
|
|
45
|
+
if (!config.restoreOnSessionStart)
|
|
22
46
|
return {};
|
|
23
|
-
|
|
24
|
-
// Check for existing snapshots
|
|
25
|
-
const snapshotCount = getSnapshotCount(storagePath);
|
|
26
|
-
if (snapshotCount === 0) {
|
|
27
|
-
// First run — tell Claude that bookmark is active
|
|
28
|
-
return {
|
|
29
|
-
systemMessage: '[Bookmark: Active — no snapshots yet. Snapshots will be captured automatically ' +
|
|
30
|
-
'before compaction, on 20-minute intervals, and at session end.]\n\n' +
|
|
31
|
-
'Briefly let the user know that Bookmark is active and will start capturing context snapshots automatically.',
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
// Read LATEST.md
|
|
35
|
-
const latestMd = readLatestMd(storagePath);
|
|
36
|
-
if (!latestMd) {
|
|
47
|
+
if (options.source === 'resume')
|
|
37
48
|
return {};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const snapshot = loadLatestSnapshot(storagePath);
|
|
63
|
-
const systemMessage = snapshot
|
|
64
|
-
? buildSmartRestoration(snapshot, snapshotCount)
|
|
65
|
-
: buildFallbackRestoration(latestMd, snapshotCount);
|
|
66
|
-
return { systemMessage };
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Merge a chain of snapshots into one unified context.
|
|
70
|
-
* Chain is in chronological order (oldest first).
|
|
71
|
-
*
|
|
72
|
-
* Strategy:
|
|
73
|
-
* - Intent/progress: Latest non-Unknown wins (most current)
|
|
74
|
-
* - Files: Union by path, merge operations, sum line counts
|
|
75
|
-
* - Decisions: Accumulate unique across chain, cap at 6
|
|
76
|
-
* - Open items: Latest snapshot only (prior items are done or superseded)
|
|
77
|
-
* - Sentiment: Latest non-neutral wins
|
|
78
|
-
*/
|
|
79
|
-
function mergeSnapshotChain(chain) {
|
|
80
|
-
const latest = chain[chain.length - 1];
|
|
81
|
-
const oldest = chain[0];
|
|
82
|
-
// Intent/progress: walk backward, take first non-Unknown
|
|
83
|
-
let intent = 'Unknown';
|
|
84
|
-
let progress = 'Unknown';
|
|
85
|
-
let sentiment = 'neutral';
|
|
86
|
-
for (let i = chain.length - 1; i >= 0; i--) {
|
|
87
|
-
if (intent === 'Unknown' && chain[i].intent && chain[i].intent !== 'Unknown') {
|
|
88
|
-
intent = chain[i].intent;
|
|
89
|
-
}
|
|
90
|
-
if (progress === 'Unknown' && chain[i].progress && chain[i].progress !== 'Unknown') {
|
|
91
|
-
progress = chain[i].progress;
|
|
92
|
-
}
|
|
93
|
-
if (sentiment === 'neutral' && chain[i].user_sentiment && chain[i].user_sentiment !== 'neutral') {
|
|
94
|
-
sentiment = chain[i].user_sentiment;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// Files: merge by path across all snapshots
|
|
98
|
-
const fileMap = new Map();
|
|
99
|
-
for (const snap of chain) {
|
|
100
|
-
for (const f of snap.files_changed) {
|
|
101
|
-
const existing = fileMap.get(f.path);
|
|
102
|
-
if (existing) {
|
|
103
|
-
// Merge operations (union)
|
|
104
|
-
const ops = new Set([...existing.operations, ...f.operations]);
|
|
105
|
-
existing.operations = [...ops];
|
|
106
|
-
// Sum line counts
|
|
107
|
-
existing.lines_changed = (existing.lines_changed ?? 0) + (f.lines_changed ?? 0);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
fileMap.set(f.path, { ...f });
|
|
49
|
+
// Primary: bookmark.context.md with identity-aware handling
|
|
50
|
+
const rawContextMd = readContextMd(storagePath);
|
|
51
|
+
if (rawContextMd && isContextMdUseful(rawContextMd)) {
|
|
52
|
+
const contextPath = join(storagePath, 'bookmark.context.md');
|
|
53
|
+
const { identity, bodyWithoutIdentity } = parseIdentity(rawContextMd);
|
|
54
|
+
// 1a. Home-scope pointer? Follow it to the canonical repo bookmark.
|
|
55
|
+
if (identity?.scope === 'home') {
|
|
56
|
+
const target = followPointer(identity);
|
|
57
|
+
if (target) {
|
|
58
|
+
const targetAge = target.staleness_hours ?? 0;
|
|
59
|
+
if (targetAge >= STALENESS_HARD_BLOCK_HOURS) {
|
|
60
|
+
trackRestore(storagePath, 0);
|
|
61
|
+
return { systemMessage: buildHardStalenessMessage(target.canonical_path, targetAge) };
|
|
62
|
+
}
|
|
63
|
+
// Strip identity block from target content for cleaner display
|
|
64
|
+
const { bodyWithoutIdentity: targetBody } = parseIdentity(target.content);
|
|
65
|
+
const header = buildPointerFollowHeader(identity, target.canonical_path, targetAge);
|
|
66
|
+
const message = `${header}\n\n${targetBody}`;
|
|
67
|
+
trackRestore(storagePath, message.length);
|
|
68
|
+
if (identity.points_to_canonical) {
|
|
69
|
+
touchLastProject(derivProjectFromCanonical(identity.points_to_canonical));
|
|
70
|
+
}
|
|
71
|
+
return { systemMessage: message };
|
|
111
72
|
}
|
|
73
|
+
// Pointer target missing — fall through to present the pointer body itself
|
|
112
74
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const d of snap.decisions) {
|
|
119
|
-
// Deduplicate by normalized prefix (first 60 chars, lowered)
|
|
120
|
-
const key = d.description.slice(0, 60).toLowerCase().trim();
|
|
121
|
-
if (!decisionSet.has(key)) {
|
|
122
|
-
decisionSet.add(key);
|
|
123
|
-
allDecisions.push(d);
|
|
124
|
-
}
|
|
75
|
+
// 1c. Hard staleness block
|
|
76
|
+
const ageHours = getAgeHours(contextPath);
|
|
77
|
+
if (ageHours !== null && ageHours >= STALENESS_HARD_BLOCK_HOURS) {
|
|
78
|
+
trackRestore(storagePath, 0);
|
|
79
|
+
return { systemMessage: buildHardStalenessMessage(contextPath, ageHours) };
|
|
125
80
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
81
|
+
// 1b. Path-mismatch warning for repo-scoped identities
|
|
82
|
+
const mismatchWarning = identity ? validateRepoIdentity(identity, options.cwd) : null;
|
|
83
|
+
// Soft staleness warning
|
|
84
|
+
const softWarning = ageHours !== null && ageHours >= STALENESS_SOFT_WARN_HOURS
|
|
85
|
+
? `[Note: This bookmark context is ${ageHours}h old and may be outdated.]`
|
|
86
|
+
: null;
|
|
87
|
+
const prefixes = [mismatchWarning, softWarning].filter(Boolean).join('\n\n');
|
|
88
|
+
const message = prefixes ? `${prefixes}\n\n${bodyWithoutIdentity}` : bodyWithoutIdentity;
|
|
89
|
+
trackRestore(storagePath, message.length);
|
|
90
|
+
touchLastProject(options.cwd);
|
|
91
|
+
return { systemMessage: message };
|
|
92
|
+
}
|
|
93
|
+
if (rawContextMd) {
|
|
94
|
+
trackBoilerplateCaught(storagePath);
|
|
95
|
+
}
|
|
96
|
+
// Fallback: LATEST.md
|
|
97
|
+
const snapshotCount = getSnapshotCount(storagePath);
|
|
98
|
+
const latestMd = readLatestMd(storagePath);
|
|
99
|
+
if (latestMd) {
|
|
100
|
+
const message = buildFallbackRestoration(latestMd, snapshotCount);
|
|
101
|
+
trackRestore(storagePath, message.length);
|
|
102
|
+
touchLastProject(options.cwd);
|
|
103
|
+
return { systemMessage: message };
|
|
104
|
+
}
|
|
105
|
+
// No local bookmark and no LATEST.md — stay silent rather than inject
|
|
106
|
+
// another repo's context. The home-scope pointer at ~/.bookmark/bookmark.context.md
|
|
107
|
+
// is the supported way to redirect from an empty CWD to a canonical project.
|
|
108
|
+
return {};
|
|
143
109
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const spanMinutes = Math.round((merged.chainSpan.newest - merged.chainSpan.oldest) / 60000);
|
|
151
|
-
const spanStr = spanMinutes > 60
|
|
152
|
-
? `${Math.round(spanMinutes / 60)}h ${spanMinutes % 60}m`
|
|
153
|
-
: `${spanMinutes}m`;
|
|
154
|
-
lines.push(`[Bookmark: Context recovered — ${chainLength} snapshots merged across ${spanStr}]`);
|
|
155
|
-
lines.push('');
|
|
156
|
-
// Intent
|
|
157
|
-
if (merged.intent !== 'Unknown') {
|
|
158
|
-
lines.push(`**User intent:** ${merged.intent}`);
|
|
159
|
-
}
|
|
160
|
-
// Progress
|
|
161
|
-
if (merged.progress !== 'Unknown') {
|
|
162
|
-
lines.push(`**Progress:** ${merged.progress}`);
|
|
163
|
-
}
|
|
164
|
-
// Sentiment
|
|
165
|
-
if (merged.sentiment !== 'neutral') {
|
|
166
|
-
lines.push(`**User feedback:** ${merged.sentiment}`);
|
|
167
|
-
}
|
|
168
|
-
lines.push('');
|
|
169
|
-
// Files — show cumulative work
|
|
170
|
-
if (merged.files.length > 0) {
|
|
171
|
-
lines.push(`**Files modified:** ${merged.files.length} files (~${merged.totalLinesChanged} lines total)`);
|
|
172
|
-
// Sort by lines changed descending — most important files first
|
|
173
|
-
const sorted = [...merged.files].sort((a, b) => (b.lines_changed ?? 0) - (a.lines_changed ?? 0));
|
|
174
|
-
for (const f of sorted.slice(0, 10)) {
|
|
175
|
-
const shortPath = f.path.includes(merged.projectPath)
|
|
176
|
-
? f.path.slice(merged.projectPath.length).replace(/^\//, '')
|
|
177
|
-
: f.path;
|
|
178
|
-
const lc = f.lines_changed ? ` ~${f.lines_changed}L` : '';
|
|
179
|
-
lines.push(`- \`${shortPath}\` (${f.operations.join('/')}${lc})`);
|
|
180
|
-
}
|
|
181
|
-
if (merged.files.length > 10) {
|
|
182
|
-
lines.push(`- ...+${merged.files.length - 10} more`);
|
|
183
|
-
}
|
|
184
|
-
lines.push('');
|
|
185
|
-
}
|
|
186
|
-
// Decisions — accumulated across chain
|
|
187
|
-
if (merged.decisions.length > 0) {
|
|
188
|
-
lines.push('**Key decisions (across session):**');
|
|
189
|
-
for (const d of merged.decisions) {
|
|
190
|
-
lines.push(`- ${d.description.slice(0, 120)}`);
|
|
191
|
-
}
|
|
192
|
-
lines.push('');
|
|
193
|
-
}
|
|
194
|
-
// Open items — from latest snapshot only
|
|
195
|
-
if (merged.openItems.length > 0) {
|
|
196
|
-
lines.push('**Remaining:**');
|
|
197
|
-
for (const item of merged.openItems.slice(0, 4)) {
|
|
198
|
-
lines.push(`- ${item.description.slice(0, 120)}`);
|
|
199
|
-
}
|
|
200
|
-
lines.push('');
|
|
201
|
-
}
|
|
202
|
-
// Chain metadata
|
|
203
|
-
if (merged.compactionCycle > 0) {
|
|
204
|
-
lines.push(`> Compaction cycle ${merged.compactionCycle} — context compressed ${merged.compactionCycle} time(s), this merges all recovered state.`);
|
|
205
|
-
}
|
|
206
|
-
if (snapshotCount > chainLength) {
|
|
207
|
-
lines.push(`> ${snapshotCount} total snapshots. \`/bookmark:list\` for full history.`);
|
|
208
|
-
}
|
|
209
|
-
lines.push('');
|
|
210
|
-
lines.push('Resume working on the task above. Do not re-read files you already modified unless the user asks.');
|
|
211
|
-
return lines.join('\n');
|
|
110
|
+
/** Derive the project path from a canonical bookmark.context.md path. */
|
|
111
|
+
function derivProjectFromCanonical(canonicalPath) {
|
|
112
|
+
// .../<project>/.bookmark/bookmark.context.md → <project>
|
|
113
|
+
const marker = `/.bookmark/`;
|
|
114
|
+
const idx = canonicalPath.lastIndexOf(marker);
|
|
115
|
+
return idx > 0 ? canonicalPath.slice(0, idx) : canonicalPath;
|
|
212
116
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (snapshot.progress && snapshot.progress !== 'Unknown') {
|
|
229
|
-
lines.push(`**Progress:** ${snapshot.progress}`);
|
|
230
|
-
}
|
|
231
|
-
// Sentiment — user's reaction to work so far
|
|
232
|
-
if (snapshot.user_sentiment && snapshot.user_sentiment !== 'neutral') {
|
|
233
|
-
lines.push(`**User feedback:** ${snapshot.user_sentiment}`);
|
|
234
|
-
}
|
|
235
|
-
lines.push('');
|
|
236
|
-
// Files changed — concrete work done
|
|
237
|
-
if (snapshot.files_changed.length > 0) {
|
|
238
|
-
const totalLines = snapshot.files_changed.reduce((sum, f) => sum + (f.lines_changed ?? 0), 0);
|
|
239
|
-
lines.push(`**Files modified:** ${snapshot.files_changed.length} files (~${totalLines} lines)`);
|
|
240
|
-
for (const f of snapshot.files_changed.slice(0, 8)) {
|
|
241
|
-
const shortPath = f.path.includes(snapshot.project_path)
|
|
242
|
-
? f.path.slice(snapshot.project_path.length).replace(/^\//, '')
|
|
243
|
-
: f.path;
|
|
244
|
-
const lc = f.lines_changed ? ` ~${f.lines_changed}L` : '';
|
|
245
|
-
lines.push(`- \`${shortPath}\` (${f.operations.join('/')}${lc})`);
|
|
246
|
-
}
|
|
247
|
-
if (snapshot.files_changed.length > 8) {
|
|
248
|
-
lines.push(`- ...+${snapshot.files_changed.length - 8} more`);
|
|
249
|
-
}
|
|
250
|
-
lines.push('');
|
|
251
|
-
}
|
|
252
|
-
// Key decisions — only if LLM-extracted (high quality)
|
|
253
|
-
if (snapshot.decisions.length > 0) {
|
|
254
|
-
lines.push('**Key decisions:**');
|
|
255
|
-
for (const d of snapshot.decisions.slice(0, 4)) {
|
|
256
|
-
lines.push(`- ${d.description.slice(0, 120)}`);
|
|
257
|
-
}
|
|
258
|
-
lines.push('');
|
|
117
|
+
/** Record a successful restore — chars injected / 4 ≈ tokens */
|
|
118
|
+
function trackRestore(storagePath, charCount) {
|
|
119
|
+
try {
|
|
120
|
+
const state = loadState(storagePath);
|
|
121
|
+
state.restores_performed = (state.restores_performed ?? 0) + 1;
|
|
122
|
+
state.tokens_injected = (state.tokens_injected ?? 0) + Math.round(charCount / 4);
|
|
123
|
+
saveState(storagePath, state);
|
|
124
|
+
}
|
|
125
|
+
catch { /* never break restore for tracking */ }
|
|
126
|
+
}
|
|
127
|
+
function trackBoilerplateCaught(storagePath) {
|
|
128
|
+
try {
|
|
129
|
+
const state = loadState(storagePath);
|
|
130
|
+
state.boilerplate_caught = (state.boilerplate_caught ?? 0) + 1;
|
|
131
|
+
saveState(storagePath, state);
|
|
259
132
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
133
|
+
catch { /* never break restore for tracking */ }
|
|
134
|
+
}
|
|
135
|
+
function isContextMdUseful(content) {
|
|
136
|
+
if (content.length < 200)
|
|
137
|
+
return false;
|
|
138
|
+
if (content.startsWith('[Bookmark Context') && !content.includes('## '))
|
|
139
|
+
return false;
|
|
140
|
+
const markers = ['## ', '**Task', '**Status', '**Progress', 'done', 'remaining', '- '];
|
|
141
|
+
return markers.some(m => content.includes(m));
|
|
142
|
+
}
|
|
143
|
+
function getAgeHours(path) {
|
|
144
|
+
try {
|
|
145
|
+
const mtime = statSync(path).mtimeMs;
|
|
146
|
+
return Math.round((Date.now() - mtime) / (1000 * 60 * 60));
|
|
267
147
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
lines.push(`> Compaction cycle ${snapshot.compaction_cycle} — context was compressed, this is the recovered state.`);
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
271
150
|
}
|
|
272
|
-
|
|
273
|
-
|
|
151
|
+
}
|
|
152
|
+
function buildHardStalenessMessage(path, ageHours) {
|
|
153
|
+
return [
|
|
154
|
+
`[Bookmark: auto-restore BLOCKED — source is ${ageHours}h stale (threshold ${STALENESS_HARD_BLOCK_HOURS}h).]`,
|
|
155
|
+
'',
|
|
156
|
+
`The bookmark file at ${path} is too old to treat as current context.`,
|
|
157
|
+
'Stale auto-restore creates confident wrong starts — worse than no restore at all.',
|
|
158
|
+
'',
|
|
159
|
+
'To proceed, either:',
|
|
160
|
+
'- Run `/bookmark:list` and pick a specific snapshot explicitly',
|
|
161
|
+
'- Read the stale file manually if you still want its content: ' +
|
|
162
|
+
`\`cat "${path}"\``,
|
|
163
|
+
'- Ask the user what they were working on most recently',
|
|
164
|
+
].join('\n');
|
|
165
|
+
}
|
|
166
|
+
function buildPointerFollowHeader(pointer, canonicalPath, ageHours) {
|
|
167
|
+
const lines = [
|
|
168
|
+
`[Bookmark: followed home-scope pointer → repo-scope bookmark]`,
|
|
169
|
+
'',
|
|
170
|
+
`Canonical file: ${canonicalPath}`,
|
|
171
|
+
];
|
|
172
|
+
if (pointer.points_to_project) {
|
|
173
|
+
lines.push(`Project: ${pointer.points_to_project}`);
|
|
174
|
+
}
|
|
175
|
+
if (ageHours >= STALENESS_SOFT_WARN_HOURS) {
|
|
176
|
+
lines.push(`Age: ${ageHours}h (approaching staleness threshold of ${STALENESS_HARD_BLOCK_HOURS}h)`);
|
|
274
177
|
}
|
|
275
|
-
lines.push('');
|
|
276
|
-
lines.push('Resume working on the task above. Do not re-read files you already modified unless the user asks.');
|
|
277
178
|
return lines.join('\n');
|
|
278
179
|
}
|
|
279
180
|
function buildFallbackRestoration(latestMd, snapshotCount) {
|
|
@@ -294,16 +195,13 @@ function handleSessionTransition(storagePath, state, options, thresholds) {
|
|
|
294
195
|
switch (source) {
|
|
295
196
|
case 'startup':
|
|
296
197
|
case 'clear':
|
|
297
|
-
// New session — reset compaction count
|
|
298
198
|
updatedState = resetForNewSession(state, sessionId, thresholds);
|
|
299
199
|
break;
|
|
300
200
|
case 'compact':
|
|
301
|
-
// After compaction — increment compaction count
|
|
302
201
|
updatedState = incrementCompaction(state, thresholds);
|
|
303
202
|
updatedState.session_id = sessionId;
|
|
304
203
|
break;
|
|
305
204
|
case 'resume':
|
|
306
|
-
// Resuming — keep state, just update session ID if different
|
|
307
205
|
updatedState = { ...state, session_id: sessionId, last_event_time: Date.now() };
|
|
308
206
|
break;
|
|
309
207
|
default:
|