@onenomad/engram-mcp 1.0.0-beta.13 → 1.1.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/README.md +685 -691
- package/dist/cli.js +41 -41
- package/dist/governance.js +6 -6
- package/dist/handoff.d.ts +53 -48
- package/dist/handoff.js +156 -134
- package/dist/handoff.js.map +1 -1
- package/dist/migrate.js +5 -5
- package/dist/server.js +946 -927
- package/dist/server.js.map +1 -1
- package/dist/storage-postgres.js +61 -61
- package/migrations/postgres/001_init.sql +70 -70
- package/migrations/postgres/002_indexes.sql +45 -45
- package/package.json +69 -69
package/dist/cli.js
CHANGED
|
@@ -18,47 +18,47 @@ import { parseArgs } from 'node:util';
|
|
|
18
18
|
import { loadConfig } from './config.js';
|
|
19
19
|
import { Storage } from './storage.js';
|
|
20
20
|
import { search, formatRecalledMemories } from './search.js';
|
|
21
|
-
const HELP = `engram-mcp — memory CLI
|
|
22
|
-
|
|
23
|
-
Usage:
|
|
24
|
-
engram-mcp run MCP stdio server
|
|
25
|
-
engram-mcp search --query <q> [opts] hybrid search
|
|
26
|
-
engram-mcp query [opts] filter listing
|
|
27
|
-
engram-mcp login <server-url> | --server <url> pair with Pyre Cloud
|
|
28
|
-
engram-mcp logout remove cached credentials
|
|
29
|
-
engram-mcp help this message
|
|
30
|
-
|
|
31
|
-
search options:
|
|
32
|
-
--query <q> (required) natural-language query
|
|
33
|
-
--project <p> filter by domain (project namespace)
|
|
34
|
-
--topic <t> filter by topic
|
|
35
|
-
--tag <t> filter by exact tag
|
|
36
|
-
--limit <n> max results (default 10)
|
|
37
|
-
--min-relevance <f> drop results with score < f (0..1)
|
|
38
|
-
--format json|text output mode (default json)
|
|
39
|
-
--no-embed skip embedding model load (keyword/IDF only,
|
|
40
|
-
~1.5s faster cold-start, lower recall)
|
|
41
|
-
|
|
42
|
-
query options:
|
|
43
|
-
--project <p> filter by domain
|
|
44
|
-
--topic <t> filter by topic
|
|
45
|
-
--tag <t> filter by exact tag
|
|
46
|
-
--tier <t> daily | short-term | long-term | archive
|
|
47
|
-
--layer <l> episodic | semantic | procedural
|
|
48
|
-
--min-importance <f> drop chunks with importance < f (0..1)
|
|
49
|
-
--limit <n> max results (default 25)
|
|
50
|
-
--format json|text output mode (default json)
|
|
51
|
-
|
|
52
|
-
login options:
|
|
53
|
-
<server-url> (required) pyre-web server URL, e.g. https://getpyre.ai
|
|
54
|
-
--server <url> alternative to the positional arg
|
|
55
|
-
(PYRE_API_URL env var also works)
|
|
56
|
-
|
|
57
|
-
Environment:
|
|
58
|
-
ENGRAM_DATA_DIR data directory (default ~/.claude/engram)
|
|
59
|
-
PYRE_API_URL pyre-web server URL (login subcommand; alternative
|
|
60
|
-
to positional arg / --server flag)
|
|
61
|
-
PYRE_CREDENTIALS_FILE override ~/.pyre/credentials.json location
|
|
21
|
+
const HELP = `engram-mcp — memory CLI
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
engram-mcp run MCP stdio server
|
|
25
|
+
engram-mcp search --query <q> [opts] hybrid search
|
|
26
|
+
engram-mcp query [opts] filter listing
|
|
27
|
+
engram-mcp login <server-url> | --server <url> pair with Pyre Cloud
|
|
28
|
+
engram-mcp logout remove cached credentials
|
|
29
|
+
engram-mcp help this message
|
|
30
|
+
|
|
31
|
+
search options:
|
|
32
|
+
--query <q> (required) natural-language query
|
|
33
|
+
--project <p> filter by domain (project namespace)
|
|
34
|
+
--topic <t> filter by topic
|
|
35
|
+
--tag <t> filter by exact tag
|
|
36
|
+
--limit <n> max results (default 10)
|
|
37
|
+
--min-relevance <f> drop results with score < f (0..1)
|
|
38
|
+
--format json|text output mode (default json)
|
|
39
|
+
--no-embed skip embedding model load (keyword/IDF only,
|
|
40
|
+
~1.5s faster cold-start, lower recall)
|
|
41
|
+
|
|
42
|
+
query options:
|
|
43
|
+
--project <p> filter by domain
|
|
44
|
+
--topic <t> filter by topic
|
|
45
|
+
--tag <t> filter by exact tag
|
|
46
|
+
--tier <t> daily | short-term | long-term | archive
|
|
47
|
+
--layer <l> episodic | semantic | procedural
|
|
48
|
+
--min-importance <f> drop chunks with importance < f (0..1)
|
|
49
|
+
--limit <n> max results (default 25)
|
|
50
|
+
--format json|text output mode (default json)
|
|
51
|
+
|
|
52
|
+
login options:
|
|
53
|
+
<server-url> (required) pyre-web server URL, e.g. https://getpyre.ai
|
|
54
|
+
--server <url> alternative to the positional arg
|
|
55
|
+
(PYRE_API_URL env var also works)
|
|
56
|
+
|
|
57
|
+
Environment:
|
|
58
|
+
ENGRAM_DATA_DIR data directory (default ~/.claude/engram)
|
|
59
|
+
PYRE_API_URL pyre-web server URL (login subcommand; alternative
|
|
60
|
+
to positional arg / --server flag)
|
|
61
|
+
PYRE_CREDENTIALS_FILE override ~/.pyre/credentials.json location
|
|
62
62
|
`;
|
|
63
63
|
const SEARCH_OPTS = {
|
|
64
64
|
query: { type: 'string' },
|
package/dist/governance.js
CHANGED
|
@@ -160,12 +160,12 @@ async function llmContradictionCheck(config, newContent, candidates) {
|
|
|
160
160
|
const existingList = candidates
|
|
161
161
|
.map((c, i) => `${i}. [${c.id}] ${c.content.slice(0, 200)}`)
|
|
162
162
|
.join('\n');
|
|
163
|
-
const response = await llmComplete(config, `You detect contradictions between a new memory and existing memories.
|
|
164
|
-
A contradiction exists when two statements cannot both be true.
|
|
165
|
-
NOT a contradiction: additional details, updates, or elaborations.
|
|
166
|
-
IS a contradiction: opposite claims, negated facts, mutually exclusive preferences.
|
|
167
|
-
|
|
168
|
-
Return JSON: [{"index": number, "type": "direct"|"semantic"|"temporal", "confidence": 0.0-1.0}]
|
|
163
|
+
const response = await llmComplete(config, `You detect contradictions between a new memory and existing memories.
|
|
164
|
+
A contradiction exists when two statements cannot both be true.
|
|
165
|
+
NOT a contradiction: additional details, updates, or elaborations.
|
|
166
|
+
IS a contradiction: opposite claims, negated facts, mutually exclusive preferences.
|
|
167
|
+
|
|
168
|
+
Return JSON: [{"index": number, "type": "direct"|"semantic"|"temporal", "confidence": 0.0-1.0}]
|
|
169
169
|
Return [] if no contradictions found. Return ONLY valid JSON.`, `NEW MEMORY:\n${newContent}\n\nEXISTING MEMORIES:\n${existingList}`, { maxTokens: 300, temperature: 0 });
|
|
170
170
|
const result = { found: false, contradictions: [] };
|
|
171
171
|
try {
|
package/dist/handoff.d.ts
CHANGED
|
@@ -1,48 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HANDOFF NOTES — "where we left off" lifeline for cross-session continuity.
|
|
3
|
-
*
|
|
4
|
-
* Unlike diary entries (free-form journal) or session-state (ephemeral scratchpad),
|
|
5
|
-
* handoffs are *structured* resume-from-here snapshots written immediately before
|
|
6
|
-
* context compaction or session end. If the context window fills before compaction
|
|
7
|
-
* runs, the user abandons the chat — the handoff is the ONLY way to continue in
|
|
8
|
-
* a fresh session without re-explaining everything.
|
|
9
|
-
*
|
|
10
|
-
* Schema is opinionated on purpose: a fresh agent can pick up from any field
|
|
11
|
-
* without hunting through prose.
|
|
12
|
-
*/
|
|
13
|
-
export interface HandoffNote {
|
|
14
|
-
/** ISO timestamp of when this handoff was written */
|
|
15
|
-
timestamp: string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* HANDOFF NOTES — "where we left off" lifeline for cross-session continuity.
|
|
3
|
+
*
|
|
4
|
+
* Unlike diary entries (free-form journal) or session-state (ephemeral scratchpad),
|
|
5
|
+
* handoffs are *structured* resume-from-here snapshots written immediately before
|
|
6
|
+
* context compaction or session end. If the context window fills before compaction
|
|
7
|
+
* runs, the user abandons the chat — the handoff is the ONLY way to continue in
|
|
8
|
+
* a fresh session without re-explaining everything.
|
|
9
|
+
*
|
|
10
|
+
* Schema is opinionated on purpose: a fresh agent can pick up from any field
|
|
11
|
+
* without hunting through prose.
|
|
12
|
+
*/
|
|
13
|
+
export interface HandoffNote {
|
|
14
|
+
/** ISO timestamp of when this handoff was written */
|
|
15
|
+
timestamp: string;
|
|
16
|
+
/** Optional human-friendly checkpoint name (e.g. "engram-named-checkpoints"). Allows list-and-pick resume across many saved sessions. */
|
|
17
|
+
name?: string;
|
|
18
|
+
/** Session or conversation identifier */
|
|
19
|
+
sessionId: string | null;
|
|
20
|
+
/** Why the handoff was written: compact, session-end, manual, context-pressure */
|
|
21
|
+
reason: 'compact' | 'session-end' | 'manual' | 'context-pressure';
|
|
22
|
+
/** One-sentence description of the active task */
|
|
23
|
+
currentTask: string;
|
|
24
|
+
/** What's already been completed in this session */
|
|
25
|
+
completed: string[];
|
|
26
|
+
/** The very next concrete action(s) to take on resume */
|
|
27
|
+
nextSteps: string[];
|
|
28
|
+
/** Unresolved questions, blockers, or decisions awaiting user input */
|
|
29
|
+
openQuestions: string[];
|
|
30
|
+
/** File paths (ideally path:line) the next agent needs to look at */
|
|
31
|
+
fileRefs: string[];
|
|
32
|
+
/** Key decisions made this session that shape future work */
|
|
33
|
+
decisions: string[];
|
|
34
|
+
/** Anything else the next agent MUST know — hidden constraints, quirks, gotchas */
|
|
35
|
+
notes: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Write a handoff note. Persists BOTH JSON (machine-readable) and markdown (human-readable).
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeHandoff(dataDir: string, note: Omit<HandoffNote, 'timestamp'>): HandoffNote;
|
|
41
|
+
export declare function readHandoff(dataDir: string, identifier?: string): HandoffNote | null;
|
|
42
|
+
export interface HandoffListEntry {
|
|
43
|
+
stamp: string;
|
|
44
|
+
timestamp: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
currentTask: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* List handoff checkpoints, newest first. Includes the optional `name` so a
|
|
51
|
+
* caller can present a list-and-pick UI keyed on either stamp or name.
|
|
52
|
+
*/
|
|
53
|
+
export declare function listHandoffs(dataDir: string, limit?: number): HandoffListEntry[];
|
package/dist/handoff.js
CHANGED
|
@@ -1,135 +1,157 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
function handoffDir(dataDir) {
|
|
4
|
-
return join(dataDir, 'handoffs');
|
|
5
|
-
}
|
|
6
|
-
function stampFilename() {
|
|
7
|
-
// YYYY-MM-DD_HH-MM-SS — safe for filenames, chronologically sortable
|
|
8
|
-
return new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').split('-').slice(0, 6).join('-');
|
|
9
|
-
}
|
|
10
|
-
function handoffJsonPath(dataDir, stamp) {
|
|
11
|
-
return join(handoffDir(dataDir), `${stamp}.json`);
|
|
12
|
-
}
|
|
13
|
-
function handoffMdPath(dataDir, stamp) {
|
|
14
|
-
return join(handoffDir(dataDir), `${stamp}.md`);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Write a handoff note. Persists BOTH JSON (machine-readable) and markdown (human-readable).
|
|
18
|
-
*/
|
|
19
|
-
export function writeHandoff(dataDir, note) {
|
|
20
|
-
const dir = handoffDir(dataDir);
|
|
21
|
-
if (!existsSync(dir))
|
|
22
|
-
mkdirSync(dir, { recursive: true });
|
|
23
|
-
const timestamp = new Date().toISOString();
|
|
24
|
-
const full = { ...note, timestamp };
|
|
25
|
-
const stamp = stampFilename();
|
|
26
|
-
// Atomic write for the JSON+MD pair. Two non-atomic writeFileSync
|
|
27
|
-
// calls in a row could leave a JSON file with no markdown sibling
|
|
28
|
-
// (or vice versa) on crash, breaking the pairing readHandoff
|
|
29
|
-
// relies on. Stage both as .tmp first, then rename both -- minimizes
|
|
30
|
-
// the crash window to the gap between two consecutive renameSync
|
|
31
|
-
// calls (sub-millisecond). True cross-file atomicity isn't
|
|
32
|
-
// expressible in POSIX; this is the best practical approximation.
|
|
33
|
-
const jsonPath = handoffJsonPath(dataDir, stamp);
|
|
34
|
-
const mdPath = handoffMdPath(dataDir, stamp);
|
|
35
|
-
writeFileSync(`${jsonPath}.tmp`, JSON.stringify(full, null, 2), 'utf-8');
|
|
36
|
-
writeFileSync(`${mdPath}.tmp`, formatHandoffMarkdown(full), 'utf-8');
|
|
37
|
-
renameSync(`${jsonPath}.tmp`, jsonPath);
|
|
38
|
-
renameSync(`${mdPath}.tmp`, mdPath);
|
|
39
|
-
return full;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Read the most recent handoff, or a specific one by stamp.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
function handoffDir(dataDir) {
|
|
4
|
+
return join(dataDir, 'handoffs');
|
|
5
|
+
}
|
|
6
|
+
function stampFilename() {
|
|
7
|
+
// YYYY-MM-DD_HH-MM-SS — safe for filenames, chronologically sortable
|
|
8
|
+
return new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').split('-').slice(0, 6).join('-');
|
|
9
|
+
}
|
|
10
|
+
function handoffJsonPath(dataDir, stamp) {
|
|
11
|
+
return join(handoffDir(dataDir), `${stamp}.json`);
|
|
12
|
+
}
|
|
13
|
+
function handoffMdPath(dataDir, stamp) {
|
|
14
|
+
return join(handoffDir(dataDir), `${stamp}.md`);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Write a handoff note. Persists BOTH JSON (machine-readable) and markdown (human-readable).
|
|
18
|
+
*/
|
|
19
|
+
export function writeHandoff(dataDir, note) {
|
|
20
|
+
const dir = handoffDir(dataDir);
|
|
21
|
+
if (!existsSync(dir))
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
const timestamp = new Date().toISOString();
|
|
24
|
+
const full = { ...note, timestamp };
|
|
25
|
+
const stamp = stampFilename();
|
|
26
|
+
// Atomic write for the JSON+MD pair. Two non-atomic writeFileSync
|
|
27
|
+
// calls in a row could leave a JSON file with no markdown sibling
|
|
28
|
+
// (or vice versa) on crash, breaking the pairing readHandoff
|
|
29
|
+
// relies on. Stage both as .tmp first, then rename both -- minimizes
|
|
30
|
+
// the crash window to the gap between two consecutive renameSync
|
|
31
|
+
// calls (sub-millisecond). True cross-file atomicity isn't
|
|
32
|
+
// expressible in POSIX; this is the best practical approximation.
|
|
33
|
+
const jsonPath = handoffJsonPath(dataDir, stamp);
|
|
34
|
+
const mdPath = handoffMdPath(dataDir, stamp);
|
|
35
|
+
writeFileSync(`${jsonPath}.tmp`, JSON.stringify(full, null, 2), 'utf-8');
|
|
36
|
+
writeFileSync(`${mdPath}.tmp`, formatHandoffMarkdown(full), 'utf-8');
|
|
37
|
+
renameSync(`${jsonPath}.tmp`, jsonPath);
|
|
38
|
+
renameSync(`${mdPath}.tmp`, mdPath);
|
|
39
|
+
return full;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the most recent handoff, or a specific one by stamp or name.
|
|
43
|
+
*
|
|
44
|
+
* Identifier resolution order:
|
|
45
|
+
* 1. No identifier → latest timestamped handoff
|
|
46
|
+
* 2. Identifier matches stamp regex → load by stamp
|
|
47
|
+
* 3. Otherwise → scan handoff JSONs for `name` field match (newest match wins)
|
|
48
|
+
*/
|
|
49
|
+
// Timestamped handoff filenames look like "2026-04-22_14-32-05-123Z" (what
|
|
50
|
+
// stampFilename() produces). The rolling `session-checkpoint.json` written
|
|
51
|
+
// by engram_stop_hook.sh does NOT match this shape, so it won't shadow real
|
|
52
|
+
// handoffs when readHandoff() picks the latest.
|
|
53
|
+
const STAMP_RE = /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
|
54
|
+
export function readHandoff(dataDir, identifier) {
|
|
55
|
+
const dir = handoffDir(dataDir);
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
return null;
|
|
58
|
+
if (!identifier) {
|
|
59
|
+
const allJson = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
60
|
+
const timestamped = allJson.filter(f => STAMP_RE.test(f)).sort().reverse();
|
|
61
|
+
const pick = timestamped[0] ?? allJson.sort().reverse()[0];
|
|
62
|
+
if (!pick)
|
|
63
|
+
return null;
|
|
64
|
+
return loadHandoffFile(handoffJsonPath(dataDir, pick.replace(/\.json$/, '')));
|
|
65
|
+
}
|
|
66
|
+
if (STAMP_RE.test(identifier)) {
|
|
67
|
+
return loadHandoffFile(handoffJsonPath(dataDir, identifier));
|
|
68
|
+
}
|
|
69
|
+
// Name lookup — scan newest first, return first hit.
|
|
70
|
+
const stamps = readdirSync(dir)
|
|
71
|
+
.filter(f => f.endsWith('.json'))
|
|
72
|
+
.map(f => f.replace(/\.json$/, ''))
|
|
73
|
+
.sort()
|
|
74
|
+
.reverse();
|
|
75
|
+
for (const stamp of stamps) {
|
|
76
|
+
const note = loadHandoffFile(handoffJsonPath(dataDir, stamp));
|
|
77
|
+
if (note?.name === identifier)
|
|
78
|
+
return note;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function loadHandoffFile(path) {
|
|
83
|
+
if (!existsSync(path))
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* List handoff checkpoints, newest first. Includes the optional `name` so a
|
|
94
|
+
* caller can present a list-and-pick UI keyed on either stamp or name.
|
|
95
|
+
*/
|
|
96
|
+
export function listHandoffs(dataDir, limit = 10) {
|
|
97
|
+
const dir = handoffDir(dataDir);
|
|
98
|
+
if (!existsSync(dir))
|
|
99
|
+
return [];
|
|
100
|
+
const stamps = readdirSync(dir)
|
|
101
|
+
.filter(f => f.endsWith('.json'))
|
|
102
|
+
.map(f => f.replace(/\.json$/, ''))
|
|
103
|
+
.sort()
|
|
104
|
+
.reverse()
|
|
105
|
+
.slice(0, limit);
|
|
106
|
+
const results = [];
|
|
107
|
+
for (const stamp of stamps) {
|
|
108
|
+
try {
|
|
109
|
+
const note = JSON.parse(readFileSync(handoffJsonPath(dataDir, stamp), 'utf-8'));
|
|
110
|
+
results.push({
|
|
111
|
+
stamp,
|
|
112
|
+
timestamp: note.timestamp,
|
|
113
|
+
reason: note.reason,
|
|
114
|
+
currentTask: note.currentTask,
|
|
115
|
+
...(note.name ? { name: note.name } : {}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Skip malformed
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
function formatHandoffMarkdown(note) {
|
|
125
|
+
const lines = [
|
|
126
|
+
`# Handoff — ${note.name ?? note.timestamp}`,
|
|
127
|
+
'',
|
|
128
|
+
note.name ? `**Name:** ${note.name}` : '',
|
|
129
|
+
`**Reason:** ${note.reason}`,
|
|
130
|
+
`**Timestamp:** ${note.timestamp}`,
|
|
131
|
+
note.sessionId ? `**Session:** ${note.sessionId}` : '',
|
|
132
|
+
'',
|
|
133
|
+
'## Current Task',
|
|
134
|
+
note.currentTask || '_unspecified_',
|
|
135
|
+
'',
|
|
136
|
+
];
|
|
137
|
+
if (note.completed.length) {
|
|
138
|
+
lines.push('## Completed', ...note.completed.map(c => `- ${c}`), '');
|
|
139
|
+
}
|
|
140
|
+
if (note.nextSteps.length) {
|
|
141
|
+
lines.push('## Next Steps', ...note.nextSteps.map(s => `- ${s}`), '');
|
|
142
|
+
}
|
|
143
|
+
if (note.openQuestions.length) {
|
|
144
|
+
lines.push('## Open Questions', ...note.openQuestions.map(q => `- ${q}`), '');
|
|
145
|
+
}
|
|
146
|
+
if (note.fileRefs.length) {
|
|
147
|
+
lines.push('## File Refs', ...note.fileRefs.map(f => `- ${f}`), '');
|
|
148
|
+
}
|
|
149
|
+
if (note.decisions.length) {
|
|
150
|
+
lines.push('## Decisions', ...note.decisions.map(d => `- ${d}`), '');
|
|
151
|
+
}
|
|
152
|
+
if (note.notes.trim()) {
|
|
153
|
+
lines.push('## Notes', note.notes.trim(), '');
|
|
154
|
+
}
|
|
155
|
+
return lines.filter((l, i, a) => !(l === '' && a[i - 1] === '')).join('\n');
|
|
156
|
+
}
|
|
135
157
|
//# sourceMappingURL=handoff.js.map
|
package/dist/handoff.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handoff.js","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtG,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"handoff.js","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtG,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAwCjC,SAAS,UAAU,CAAC,OAAe;IACjC,OAAO,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,aAAa;IACpB,qEAAqE;IACrE,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3G,CAAC;AAED,SAAS,eAAe,CAAC,OAAe,EAAE,KAAa;IACrD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC;AACpD,CAAC;AAED,SAAS,aAAa,CAAC,OAAe,EAAE,KAAa;IACnD,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,KAAK,CAAC,CAAC;AAClD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,IAAoC;IAChF,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1D,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,MAAM,IAAI,GAAgB,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;IAE9B,kEAAkE;IAClE,kEAAkE;IAClE,6DAA6D;IAC7D,qEAAqE;IACrE,iEAAiE;IACjE,2DAA2D;IAC3D,kEAAkE;IAClE,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC7C,aAAa,CAAC,GAAG,QAAQ,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACzE,aAAa,CAAC,GAAG,MAAM,MAAM,EAAE,qBAAqB,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;IACrE,UAAU,CAAC,GAAG,QAAQ,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxC,UAAU,CAAC,GAAG,MAAM,MAAM,EAAE,MAAM,CAAC,CAAC;IAEpC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,gDAAgD;AAChD,MAAM,QAAQ,GAAG,sCAAsC,CAAC;AAExD,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,UAAmB;IAC9D,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAClE,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;QAC3E,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,OAAO,eAAe,CAAC,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAChF,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,OAAO,eAAe,CAAC,eAAe,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED,qDAAqD;IACrD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SAChC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;SAClC,IAAI,EAAE;SACN,OAAO,EAAE,CAAC;IACb,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,eAAe,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QAC9D,IAAI,IAAI,EAAE,IAAI,KAAK,UAAU;YAAE,OAAO,IAAI,CAAC;IAC7C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAgB,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAUD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,KAAK,GAAG,EAAE;IACtD,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAEhC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC;SAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;SAChC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;SAClC,IAAI,EAAE;SACN,OAAO,EAAE;SACT,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAEnB,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAgB,CAAC;YAC/F,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK;gBACL,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,qBAAqB,CAAC,IAAiB;IAC9C,MAAM,KAAK,GAAa;QACtB,eAAe,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE;QAC5C,EAAE;QACF,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;QACzC,eAAe,IAAI,CAAC,MAAM,EAAE;QAC5B,kBAAkB,IAAI,CAAC,SAAS,EAAE;QAClC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE;QACtD,EAAE;QACF,iBAAiB;QACjB,IAAI,CAAC,WAAW,IAAI,eAAe;QACnC,EAAE;KACH,CAAC;IAEF,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,mBAAmB,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IAChF,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC9E,CAAC"}
|
package/dist/migrate.js
CHANGED
|
@@ -51,11 +51,11 @@ async function main() {
|
|
|
51
51
|
await client.connect();
|
|
52
52
|
try {
|
|
53
53
|
// Bootstrap the bookkeeping table itself.
|
|
54
|
-
await client.query(`
|
|
55
|
-
CREATE TABLE IF NOT EXISTS _migrations (
|
|
56
|
-
filename text PRIMARY KEY,
|
|
57
|
-
applied_at timestamptz NOT NULL DEFAULT NOW()
|
|
58
|
-
)
|
|
54
|
+
await client.query(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
56
|
+
filename text PRIMARY KEY,
|
|
57
|
+
applied_at timestamptz NOT NULL DEFAULT NOW()
|
|
58
|
+
)
|
|
59
59
|
`);
|
|
60
60
|
const { rows: appliedRows } = await client.query(`SELECT filename FROM _migrations`);
|
|
61
61
|
const applied = new Set(appliedRows.map((r) => r.filename));
|