hypomnema 1.1.0 → 1.2.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/README.ko.md +74 -26
- package/README.md +57 -9
- package/commands/audit.md +2 -2
- package/commands/crystallize.md +113 -23
- package/commands/feedback.md +40 -26
- package/commands/ingest.md +31 -9
- package/commands/upgrade.md +2 -2
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONTRIBUTING.md +1 -1
- package/hooks/hooks.json +30 -1
- package/hooks/hypo-auto-commit.mjs +10 -4
- package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
- package/hooks/hypo-auto-stage.mjs +4 -3
- package/hooks/hypo-compact-guard.mjs +33 -24
- package/hooks/hypo-cwd-change.mjs +111 -24
- package/hooks/hypo-file-watch.mjs +23 -10
- package/hooks/hypo-first-prompt.mjs +69 -23
- package/hooks/hypo-hot-rebuild.mjs +22 -10
- package/hooks/hypo-lookup.mjs +171 -65
- package/hooks/hypo-personal-check.mjs +209 -112
- package/hooks/hypo-pre-commit.mjs +46 -0
- package/hooks/hypo-session-end.mjs +58 -0
- package/hooks/hypo-session-record.mjs +11 -5
- package/hooks/hypo-session-start.mjs +302 -52
- package/hooks/hypo-shared.mjs +817 -37
- package/hooks/hypo-web-fetch-ingest.mjs +121 -0
- package/hooks/version-check-fetch.mjs +74 -0
- package/hooks/version-check.mjs +184 -0
- package/package.json +17 -3
- package/scripts/crystallize.mjs +623 -18
- package/scripts/doctor.mjs +730 -47
- package/scripts/feedback-sync.mjs +974 -0
- package/scripts/feedback.mjs +253 -44
- package/scripts/graph.mjs +35 -22
- package/scripts/ingest.mjs +89 -16
- package/scripts/init.mjs +398 -113
- package/scripts/lib/design-history-stale.mjs +83 -0
- package/scripts/lib/extensions.mjs +749 -0
- package/scripts/lib/frontmatter.mjs +5 -1
- package/scripts/lib/hypo-ignore.mjs +12 -10
- package/scripts/lib/pkg-json.mjs +23 -5
- package/scripts/lib/project-create.mjs +225 -0
- package/scripts/lib/schema-vocab.mjs +96 -0
- package/scripts/lint.mjs +238 -31
- package/scripts/query.mjs +26 -10
- package/scripts/resume.mjs +16 -6
- package/scripts/session-audit.mjs +37 -27
- package/scripts/smoke-pack.mjs +224 -0
- package/scripts/stats.mjs +24 -10
- package/scripts/uninstall.mjs +363 -49
- package/scripts/upgrade.mjs +706 -202
- package/scripts/verify.mjs +24 -14
- package/scripts/weekly-report.mjs +59 -25
- package/skills/crystallize/SKILL.md +20 -7
- package/skills/ingest/SKILL.md +25 -5
- package/templates/.hypoignore +16 -2
- package/templates/Home.md +2 -0
- package/templates/SCHEMA.md +61 -6
- package/templates/extensions/agents/.gitkeep +0 -0
- package/templates/extensions/commands/.gitkeep +0 -0
- package/templates/extensions/hooks/.gitkeep +0 -0
- package/templates/extensions/skills/.gitkeep +0 -0
- package/templates/gitignore +5 -0
- package/templates/hot.md +2 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +42 -2
- package/templates/hypo-help.md +1 -1
- package/templates/pages/observability/_index.md +77 -0
- package/templates/projects/_template/index.md +2 -2
- package/templates/projects/_template/prd.md +1 -1
|
@@ -6,21 +6,42 @@
|
|
|
6
6
|
* project hot.md. Skips if still within the same project subtree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import { join } from 'path';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
HYPO_DIR,
|
|
14
|
+
buildOutput,
|
|
15
|
+
loadHypoIgnore,
|
|
16
|
+
isIgnored,
|
|
17
|
+
sessionMarkerPath,
|
|
18
|
+
shouldSuggestProjectCreation,
|
|
19
|
+
buildProjectSuggestionLine,
|
|
20
|
+
recordSuggestionCooldown,
|
|
21
|
+
sanitizeProjForPrompt,
|
|
22
|
+
} from './hypo-shared.mjs';
|
|
13
23
|
|
|
14
24
|
const PROJECTS_DIR = join(HYPO_DIR, 'projects');
|
|
15
|
-
const GLOBAL_HOT
|
|
16
|
-
const MAX_CHARS
|
|
25
|
+
const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
|
|
26
|
+
const MAX_CHARS = 3000;
|
|
27
|
+
|
|
28
|
+
// Privacy guard: a .hypoignore-matched hot.md must not be
|
|
29
|
+
// re-emitted into additionalContext on cwd change.
|
|
30
|
+
function readIfNotIgnored(path, patterns) {
|
|
31
|
+
if (!path) return null;
|
|
32
|
+
if (patterns.length > 0 && isIgnored(path, HYPO_DIR, patterns)) return null;
|
|
33
|
+
return readFileSync(path, 'utf-8').slice(0, MAX_CHARS);
|
|
34
|
+
}
|
|
17
35
|
|
|
18
36
|
function parseFrontmatterField(content, key) {
|
|
19
37
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
20
38
|
if (!match) return null;
|
|
21
|
-
const line = match[1].split('\n').find(l => l.startsWith(`${key}:`));
|
|
39
|
+
const line = match[1].split('\n').find((l) => l.startsWith(`${key}:`));
|
|
22
40
|
if (!line) return null;
|
|
23
|
-
return line
|
|
41
|
+
return line
|
|
42
|
+
.slice(key.length + 1)
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/^['"]|['"]$/g, '');
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
function findProjectHot(cwd) {
|
|
@@ -30,7 +51,7 @@ function findProjectHot(cwd) {
|
|
|
30
51
|
if (!statSync(projDir).isDirectory()) continue;
|
|
31
52
|
const indexPath = join(projDir, 'index.md');
|
|
32
53
|
if (!existsSync(indexPath)) continue;
|
|
33
|
-
const content
|
|
54
|
+
const content = readFileSync(indexPath, 'utf-8');
|
|
34
55
|
const workingDir = parseFrontmatterField(content, 'working_dir');
|
|
35
56
|
if (!workingDir) continue;
|
|
36
57
|
const resolved = workingDir.startsWith('~/')
|
|
@@ -38,7 +59,13 @@ function findProjectHot(cwd) {
|
|
|
38
59
|
: workingDir;
|
|
39
60
|
if (cwd === resolved || cwd.startsWith(resolved + '/')) {
|
|
40
61
|
const hotPath = join(projDir, 'hot.md');
|
|
41
|
-
|
|
62
|
+
const statePath = join(projDir, 'session-state.md');
|
|
63
|
+
return {
|
|
64
|
+
proj,
|
|
65
|
+
hotPath: existsSync(hotPath) ? hotPath : null,
|
|
66
|
+
statePath: existsSync(statePath) ? statePath : null,
|
|
67
|
+
resolved,
|
|
68
|
+
};
|
|
42
69
|
}
|
|
43
70
|
}
|
|
44
71
|
return null;
|
|
@@ -46,14 +73,17 @@ function findProjectHot(cwd) {
|
|
|
46
73
|
|
|
47
74
|
let raw = '';
|
|
48
75
|
process.stdin.setEncoding('utf-8');
|
|
49
|
-
process.stdin.on('data', chunk => raw += chunk);
|
|
76
|
+
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
50
77
|
process.stdin.on('end', () => {
|
|
51
78
|
try {
|
|
52
79
|
let data = {};
|
|
53
|
-
try {
|
|
80
|
+
try {
|
|
81
|
+
data = JSON.parse(raw);
|
|
82
|
+
} catch {}
|
|
54
83
|
|
|
55
84
|
const newCwd = data.new_cwd || data.new_directory || data.cwd || process.cwd();
|
|
56
85
|
const oldCwd = data.old_cwd || data.old_directory || data.previous_cwd || '';
|
|
86
|
+
const sessionId = data.session_id || 'default';
|
|
57
87
|
|
|
58
88
|
// Skip re-injection if still in the same project
|
|
59
89
|
const oldHit = oldCwd ? findProjectHot(oldCwd) : null;
|
|
@@ -64,28 +94,85 @@ process.stdin.on('end', () => {
|
|
|
64
94
|
return;
|
|
65
95
|
}
|
|
66
96
|
|
|
97
|
+
const ignorePatterns = loadHypoIgnore(HYPO_DIR);
|
|
98
|
+
|
|
67
99
|
if (newHit) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
100
|
+
const fromFile = readIfNotIgnored(newHit.hotPath, ignorePatterns);
|
|
101
|
+
const content = fromFile ?? '(no hot.md yet — will be created at session close)';
|
|
102
|
+
// fix #13: arm the first-prompt marker so the NEXT user prompt re-triggers
|
|
103
|
+
// hypo-first-prompt, which forces a "Resuming <project>" summary line.
|
|
104
|
+
// Only arm when real hot content was actually injected — if hot.md is
|
|
105
|
+
// missing or .hypoignore'd (fromFile null), there is nothing for the LLM
|
|
106
|
+
// to summarize, so forcing "Resuming" would be empty noise (codex review).
|
|
107
|
+
if (fromFile) {
|
|
108
|
+
try {
|
|
109
|
+
writeFileSync(
|
|
110
|
+
sessionMarkerPath(sessionId),
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
proj: newHit.proj,
|
|
113
|
+
hotPath: newHit.hotPath,
|
|
114
|
+
statePath: newHit.statePath,
|
|
115
|
+
hasSnapshot: true,
|
|
116
|
+
source: 'cwd-change',
|
|
117
|
+
ts: Date.now(),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
`[hypo-cwd-change] marker write failed: ${err?.message ?? String(err)}\n`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log(
|
|
127
|
+
JSON.stringify(
|
|
128
|
+
buildOutput(
|
|
129
|
+
`[WIKI: cwd changed → project=${sanitizeProjForPrompt(newHit.proj)}]\n\n${content}`,
|
|
130
|
+
{
|
|
131
|
+
continue: true,
|
|
132
|
+
suppressOutput: true,
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
74
137
|
return;
|
|
75
138
|
}
|
|
76
139
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
140
|
+
// MISS: cwd matches no project. fix #23 / ADR 0023 — offer to create one
|
|
141
|
+
// when the trigger conditions hold. Same nudge-only model as session-start.
|
|
142
|
+
let suggestPrefix = '';
|
|
143
|
+
if (shouldSuggestProjectCreation(newCwd, HYPO_DIR)) {
|
|
144
|
+
const suggestLine = buildProjectSuggestionLine(newCwd);
|
|
145
|
+
suggestPrefix = `${suggestLine}\n\n`;
|
|
146
|
+
recordSuggestionCooldown(HYPO_DIR, newCwd);
|
|
147
|
+
process.stderr.write(`\n\x1b[33m${suggestLine}\x1b[0m\n`);
|
|
80
148
|
}
|
|
81
149
|
|
|
82
|
-
const globalContent =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
));
|
|
150
|
+
const globalContent = existsSync(GLOBAL_HOT)
|
|
151
|
+
? readIfNotIgnored(GLOBAL_HOT, ignorePatterns)
|
|
152
|
+
: null;
|
|
86
153
|
|
|
154
|
+
if (!globalContent) {
|
|
155
|
+
if (suggestPrefix) {
|
|
156
|
+
console.log(
|
|
157
|
+
JSON.stringify(
|
|
158
|
+
buildOutput(suggestPrefix.trimEnd(), { continue: true, suppressOutput: true }),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(
|
|
167
|
+
JSON.stringify(
|
|
168
|
+
buildOutput(
|
|
169
|
+
`${suggestPrefix}[WIKI: cwd changed → no project match, injecting global hot]\n\n${globalContent}`,
|
|
170
|
+
{ continue: true, suppressOutput: true },
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
87
174
|
} catch (err) {
|
|
88
|
-
process.stderr.write(`[
|
|
175
|
+
process.stderr.write(`[hypo-cwd-change] error: ${err?.message ?? String(err)}\n`);
|
|
89
176
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
90
177
|
}
|
|
91
178
|
});
|
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFileSync, existsSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
|
-
import { HYPO_DIR } from './hypo-shared.mjs';
|
|
11
|
+
import { HYPO_DIR, loadHypoIgnore, isIgnored } from './hypo-shared.mjs';
|
|
12
12
|
|
|
13
13
|
const MAX_CHARS = 2000;
|
|
14
14
|
|
|
15
15
|
let raw = '';
|
|
16
16
|
process.stdin.setEncoding('utf-8');
|
|
17
|
-
process.stdin.on('data', chunk => raw += chunk);
|
|
17
|
+
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
18
18
|
process.stdin.on('end', () => {
|
|
19
19
|
try {
|
|
20
20
|
let data = {};
|
|
21
|
-
try {
|
|
21
|
+
try {
|
|
22
|
+
data = JSON.parse(raw);
|
|
23
|
+
} catch {}
|
|
22
24
|
|
|
23
25
|
const filePath = data.file_path || data.path || '';
|
|
24
26
|
|
|
@@ -27,6 +29,15 @@ process.stdin.on('end', () => {
|
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
// Privacy guard: refuse to inject
|
|
33
|
+
// .hypoignore-matched paths. Without this, `.env*` or other secrets under
|
|
34
|
+
// HYPO_DIR are re-emitted as additionalContext to the Claude provider.
|
|
35
|
+
const patterns = loadHypoIgnore(HYPO_DIR);
|
|
36
|
+
if (patterns.length > 0 && isIgnored(filePath, HYPO_DIR, patterns)) {
|
|
37
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
if (!existsSync(filePath)) {
|
|
31
42
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
32
43
|
return;
|
|
@@ -35,13 +46,15 @@ process.stdin.on('end', () => {
|
|
|
35
46
|
const content = readFileSync(filePath, 'utf-8').slice(0, MAX_CHARS);
|
|
36
47
|
const relPath = filePath.replace(HYPO_DIR + '/', '');
|
|
37
48
|
|
|
38
|
-
console.log(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
console.log(
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
continue: true,
|
|
52
|
+
suppressOutput: true,
|
|
53
|
+
additionalContext: `[WIKI FILE UPDATED: ${relPath}]\n\n${content}`,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
process.stderr.write(`[hypo-file-watch] error: ${err?.message ?? String(err)}\n`);
|
|
45
58
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
46
59
|
}
|
|
47
60
|
});
|
|
@@ -2,31 +2,33 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* hypo-first-prompt.mjs — UserPromptSubmit hook
|
|
4
4
|
*
|
|
5
|
-
* Consumes the marker written by
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Consumes the marker written by hypo-session-start.mjs (source omitted /
|
|
6
|
+
* 'session-start') or hypo-cwd-change.mjs (source 'cwd-change', fix #13).
|
|
7
|
+
* On the FIRST user prompt after the marker is written, FORCES a one-line
|
|
8
|
+
* resume summary into the reply (fix #3 — the old "answer only if related"
|
|
9
|
+
* conditional is removed; the line is injected unconditionally).
|
|
8
10
|
*
|
|
9
|
-
* hot.md content is NOT re-injected here —
|
|
10
|
-
*
|
|
11
|
+
* hot.md / session-state.md content is NOT re-injected here — the upstream
|
|
12
|
+
* hook already placed it in additionalContext. This hook only forces the LLM
|
|
13
|
+
* to lead with the summary line drawn from that context.
|
|
11
14
|
* Marker expires after 10 minutes.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import { readFileSync, unlinkSync, existsSync } from 'fs';
|
|
15
|
-
import {
|
|
16
|
-
import { join } from 'path';
|
|
17
|
-
import { buildOutput } from './hypo-shared.mjs';
|
|
18
|
+
import { buildOutput, sessionMarkerPath, sanitizeProjForPrompt } from './hypo-shared.mjs';
|
|
18
19
|
|
|
19
20
|
const MARKER_TTL = 10 * 60 * 1000; // 10 min
|
|
20
21
|
|
|
21
22
|
let raw = '';
|
|
22
23
|
process.stdin.setEncoding('utf-8');
|
|
23
|
-
process.stdin.on('data', chunk => raw += chunk);
|
|
24
|
+
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
24
25
|
process.stdin.on('end', () => {
|
|
25
26
|
try {
|
|
26
27
|
let data = {};
|
|
27
|
-
try {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
try {
|
|
29
|
+
data = JSON.parse(raw);
|
|
30
|
+
} catch {}
|
|
31
|
+
const MARKER_FILE = sessionMarkerPath(data.session_id);
|
|
30
32
|
|
|
31
33
|
if (!existsSync(MARKER_FILE)) {
|
|
32
34
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
@@ -34,26 +36,70 @@ process.stdin.on('end', () => {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const marker = JSON.parse(readFileSync(MARKER_FILE, 'utf-8'));
|
|
37
|
-
const age
|
|
39
|
+
const age = Date.now() - (marker.ts || 0);
|
|
38
40
|
|
|
39
|
-
try {
|
|
41
|
+
try {
|
|
42
|
+
unlinkSync(MARKER_FILE);
|
|
43
|
+
} catch {}
|
|
40
44
|
|
|
41
45
|
if (age > MARKER_TTL) {
|
|
42
46
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
const hasSnapshot
|
|
50
|
+
const hasSnapshot = marker.hasSnapshot ?? (marker.hotPath && existsSync(marker.hotPath));
|
|
47
51
|
const snapshotNote = hasSnapshot ? '' : ' (no snapshot yet — first session)';
|
|
52
|
+
// fix #13: a cwd-change re-trigger says "Resuming"; a fresh session start
|
|
53
|
+
// (default source) says "Previously working on".
|
|
54
|
+
const verb = marker.source === 'cwd-change' ? 'Resuming' : 'Previously working on';
|
|
55
|
+
// marker.proj originates from a wiki directory name read by findProjectFiles;
|
|
56
|
+
// sanitize via the shared helper so a hand-crafted project name cannot close
|
|
57
|
+
// the wrapper tag, smuggle newlines/control chars, or inject conflicting
|
|
58
|
+
// directives into the resume contract (codex v2 review 2026-05-26).
|
|
59
|
+
const projSafe = sanitizeProjForPrompt(marker.proj);
|
|
60
|
+
// When there is no snapshot, the [HOT] / [SESSION STATE] context has nothing
|
|
61
|
+
// for the model to fill the placeholders with. Provide a concrete fallback
|
|
62
|
+
// line so the model doesn't leak literal `[one-line summary]` text on a
|
|
63
|
+
// first-ever session (codex v2 review 2026-05-26).
|
|
64
|
+
const exampleLine = hasSnapshot
|
|
65
|
+
? `${verb} ${projSafe}: [one-line summary]. Continue with [next task]?`
|
|
66
|
+
: `${verb} ${projSafe}: no prior snapshot yet — first session. What would you like to start with?`;
|
|
67
|
+
const fillNote = hasSnapshot
|
|
68
|
+
? `Replace the bracketed placeholders using the [HOT] / [SESSION STATE] ` +
|
|
69
|
+
`context already injected this session — do NOT emit the literal brackets.`
|
|
70
|
+
: `Use the line above verbatim — there is no prior snapshot to summarize.`;
|
|
48
71
|
|
|
49
|
-
console.log(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
console.log(
|
|
73
|
+
JSON.stringify(
|
|
74
|
+
buildOutput(
|
|
75
|
+
`<hypomnema-session-resume>\n` +
|
|
76
|
+
`[WIKI SESSION START: project=${projSafe}${snapshotNote}]\n` +
|
|
77
|
+
`\n` +
|
|
78
|
+
`Lead your FIRST reply this session with exactly one line in this shape:\n` +
|
|
79
|
+
`\n` +
|
|
80
|
+
`${exampleLine}\n` +
|
|
81
|
+
`\n` +
|
|
82
|
+
`${fillNote}\n` +
|
|
83
|
+
`\n` +
|
|
84
|
+
`Emit this line unconditionally on the first prompt, including when the ` +
|
|
85
|
+
`user's message is:\n` +
|
|
86
|
+
` • a simple greeting ("안녕", "hi", "hello")\n` +
|
|
87
|
+
` • a trivial question or unrelated topic\n` +
|
|
88
|
+
` • a one-word reply\n` +
|
|
89
|
+
`\n` +
|
|
90
|
+
`Do not skip it, do not decide it is "not relevant", do not shorten the ` +
|
|
91
|
+
`reply to omit it. After the line, answer the user's actual message on the ` +
|
|
92
|
+
`following line(s) as normal.\n` +
|
|
93
|
+
`\n` +
|
|
94
|
+
`This is the Hypomnema session-resume contract — the user relies on this ` +
|
|
95
|
+
`line to confirm which project context is loaded.\n` +
|
|
96
|
+
`</hypomnema-session-resume>`,
|
|
97
|
+
{ continue: true, suppressOutput: true },
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
process.stderr.write(`[hypo-first-prompt] error: ${err?.message ?? String(err)}\n`);
|
|
57
103
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
58
104
|
}
|
|
59
105
|
});
|
|
@@ -14,8 +14,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { HYPO_DIR, computeSessionGrowth, formatGrowthMetrics } from './hypo-shared.mjs';
|
|
16
16
|
|
|
17
|
-
const HOT_PATH
|
|
18
|
-
const GROWTH_CACHE
|
|
17
|
+
const HOT_PATH = join(HYPO_DIR, 'hot.md');
|
|
18
|
+
const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
|
|
19
19
|
|
|
20
20
|
function parseFrontmatter(content) {
|
|
21
21
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -56,10 +56,12 @@ function rebuild() {
|
|
|
56
56
|
|
|
57
57
|
const today = new Date().toISOString().slice(0, 10);
|
|
58
58
|
|
|
59
|
-
const tableRows = rows
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const tableRows = rows
|
|
60
|
+
.map(({ name, slug }) => {
|
|
61
|
+
const date = getProjectDate(slug) || today;
|
|
62
|
+
return `| ${name} | ${date} | [[projects/${slug}/hot]] |`;
|
|
63
|
+
})
|
|
64
|
+
.join('\n');
|
|
63
65
|
|
|
64
66
|
const canonical = `---
|
|
65
67
|
title: Hot Cache — Pointer
|
|
@@ -92,7 +94,7 @@ ${tableRows}
|
|
|
92
94
|
function emitGrowth() {
|
|
93
95
|
if (!existsSync(HYPO_DIR)) return;
|
|
94
96
|
const stats = computeSessionGrowth(HYPO_DIR);
|
|
95
|
-
const line
|
|
97
|
+
const line = formatGrowthMetrics('stop', stats);
|
|
96
98
|
if (line) process.stderr.write(`${line}\n`);
|
|
97
99
|
try {
|
|
98
100
|
mkdirSync(join(HYPO_DIR, '.cache'), { recursive: true });
|
|
@@ -100,7 +102,17 @@ function emitGrowth() {
|
|
|
100
102
|
} catch {}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
try {
|
|
104
|
-
|
|
105
|
+
try {
|
|
106
|
+
rebuild();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
process.stderr.write(`[hypo-hot-rebuild] error: ${err?.message ?? String(err)}\n`);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
emitGrowth();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
process.stderr.write(`[hypo-hot-rebuild] error: ${err?.message ?? String(err)}\n`);
|
|
114
|
+
}
|
|
105
115
|
|
|
106
|
-
try {
|
|
116
|
+
try {
|
|
117
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
118
|
+
} catch {}
|