packmind 0.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/LICENSE +202 -0
- package/README.md +136 -0
- package/dist/adapters/claude-code.js +67 -0
- package/dist/bin/packmind.js +13 -0
- package/dist/cli/backup-cmd.js +41 -0
- package/dist/cli/ctx.js +14 -0
- package/dist/cli/dashboard-cmd.js +34 -0
- package/dist/cli/doctor.js +45 -0
- package/dist/cli/index-cmd.js +15 -0
- package/dist/cli/index.js +68 -0
- package/dist/cli/init.js +83 -0
- package/dist/cli/insights-cmd.js +28 -0
- package/dist/cli/locate.js +15 -0
- package/dist/cli/maintain-cmd.js +39 -0
- package/dist/cli/mcp-cmd.js +5 -0
- package/dist/cli/policy-cmd.js +40 -0
- package/dist/cli/recall-cmd.js +18 -0
- package/dist/cli/registry.js +32 -0
- package/dist/cli/scan.js +18 -0
- package/dist/cli/solutions-cmd.js +24 -0
- package/dist/cli/status.js +28 -0
- package/dist/cli/update.js +73 -0
- package/dist/cost/estimator.js +21 -0
- package/dist/cost/exact.js +35 -0
- package/dist/cost/insights.js +80 -0
- package/dist/cost/ledger.js +47 -0
- package/dist/cost/pricing.js +27 -0
- package/dist/dashboard/server.js +128 -0
- package/dist/guard/path-guard.js +26 -0
- package/dist/guard/policy.js +0 -0
- package/dist/guard/secrets.js +29 -0
- package/dist/hooks/post-read.js +80 -0
- package/dist/hooks/post-write.js +107 -0
- package/dist/hooks/pre-read.js +94 -0
- package/dist/hooks/pre-write.js +101 -0
- package/dist/hooks/prompt-submit.js +37 -0
- package/dist/hooks/runtime.js +471 -0
- package/dist/hooks/session-start.js +72 -0
- package/dist/hooks/stop.js +69 -0
- package/dist/mcp/server.js +112 -0
- package/dist/mcp/tools.js +130 -0
- package/dist/recall/chunker.js +24 -0
- package/dist/recall/embedder.js +51 -0
- package/dist/recall/indexer.js +94 -0
- package/dist/recall/queue.js +24 -0
- package/dist/recall/store.js +48 -0
- package/dist/state/describe.js +63 -0
- package/dist/state/files.js +45 -0
- package/dist/state/formats.js +80 -0
- package/dist/state/maintain.js +25 -0
- package/dist/state/mapper.js +56 -0
- package/dist/state/project.js +33 -0
- package/dist/state/schema.js +47 -0
- package/dist/state/snapshot.js +69 -0
- package/dist/state/walk.js +75 -0
- package/dist/util/fs-atomic.js +106 -0
- package/dist/util/logger.js +17 -0
- package/dist/util/paths.js +20 -0
- package/dist/util/platform.js +10 -0
- package/package.json +72 -0
- package/src/templates/PACKMIND.md +42 -0
- package/src/templates/claude-md-snippet.md +9 -0
- package/src/templates/config.json +48 -0
- package/src/templates/dashboard.html +359 -0
- package/src/templates/gitattributes +2 -0
- package/src/templates/handoff.md +3 -0
- package/src/templates/hooks-package.json +4 -0
- package/src/templates/identity.md +5 -0
- package/src/templates/journal.md +3 -0
- package/src/templates/knowledge.md +12 -0
- package/src/templates/logo-dark.svg +23 -0
- package/src/templates/logo.svg +23 -0
- package/src/templates/map.md +3 -0
- package/src/templates/policy.json +11 -0
- package/src/templates/solutions.json +1 -0
- package/src/templates/usage.json +17 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.projectRoot = projectRoot;
|
|
37
|
+
exports.stateDir = stateDir;
|
|
38
|
+
exports.brainPath = brainPath;
|
|
39
|
+
exports.requireState = requireState;
|
|
40
|
+
exports.withLock = withLock;
|
|
41
|
+
exports.readText = readText;
|
|
42
|
+
exports.writeText = writeText;
|
|
43
|
+
exports.appendLine = appendLine;
|
|
44
|
+
exports.readJson = readJson;
|
|
45
|
+
exports.writeJson = writeJson;
|
|
46
|
+
exports.lines = lines;
|
|
47
|
+
exports.parseMap = parseMap;
|
|
48
|
+
exports.serializeMap = serializeMap;
|
|
49
|
+
exports.parseNeverDo = parseNeverDo;
|
|
50
|
+
exports.estimateTokens = estimateTokens;
|
|
51
|
+
exports.looksSecret = looksSecret;
|
|
52
|
+
exports.confineToRoot = confineToRoot;
|
|
53
|
+
exports.samePath = samePath;
|
|
54
|
+
exports.inputCost = inputCost;
|
|
55
|
+
exports.outputCost = outputCost;
|
|
56
|
+
exports.evaluateWrite = evaluateWrite;
|
|
57
|
+
exports.describeLite = describeLite;
|
|
58
|
+
exports.hookConfig = hookConfig;
|
|
59
|
+
exports.readStdin = readStdin;
|
|
60
|
+
exports.parseInput = parseInput;
|
|
61
|
+
exports.emitContext = emitContext;
|
|
62
|
+
exports.emitDeny = emitDeny;
|
|
63
|
+
exports.newSession = newSession;
|
|
64
|
+
exports.readSession = readSession;
|
|
65
|
+
exports.writeSession = writeSession;
|
|
66
|
+
exports.enqueueRecall = enqueueRecall;
|
|
67
|
+
/**
|
|
68
|
+
* Zero-dependency runtime for PackMind's standalone hook scripts.
|
|
69
|
+
*
|
|
70
|
+
* Hooks are copied into a user's `.packmind/hooks/` and executed by Claude Code
|
|
71
|
+
* as plain `node` scripts, so this module imports ONLY Node builtins. The
|
|
72
|
+
* format functions here mirror src/state/formats.ts, the secret matcher mirrors
|
|
73
|
+
* src/guard/secrets.ts, and the estimator mirrors src/cost/estimator.ts; a
|
|
74
|
+
* parity test pins each mirror to its canonical twin.
|
|
75
|
+
*/
|
|
76
|
+
const fs = __importStar(require("node:fs"));
|
|
77
|
+
const path = __importStar(require("node:path"));
|
|
78
|
+
const crypto = __importStar(require("node:crypto"));
|
|
79
|
+
// --- project + brain paths --------------------------------------------------
|
|
80
|
+
function projectRoot() {
|
|
81
|
+
return process.env.PACKMIND_ROOT || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
82
|
+
}
|
|
83
|
+
function stateDir() {
|
|
84
|
+
return path.join(projectRoot(), ".packmind");
|
|
85
|
+
}
|
|
86
|
+
function brainPath(...parts) {
|
|
87
|
+
return path.join(stateDir(), ...parts);
|
|
88
|
+
}
|
|
89
|
+
/** Exit silently when the project doesn't use PackMind. */
|
|
90
|
+
function requireState() {
|
|
91
|
+
if (!fs.existsSync(stateDir()))
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
// --- atomic + locked IO -----------------------------------------------------
|
|
95
|
+
const FALLBACK = new Set(["EBUSY", "EACCES", "EPERM", "EXDEV"]);
|
|
96
|
+
function spin(ms) {
|
|
97
|
+
const until = Date.now() + ms;
|
|
98
|
+
while (Date.now() < until) {
|
|
99
|
+
/* brief spin */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function withLock(target, body) {
|
|
103
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
104
|
+
const lock = `${target}.lock`;
|
|
105
|
+
let held = false;
|
|
106
|
+
for (let i = 0; i < 60; i++) {
|
|
107
|
+
try {
|
|
108
|
+
fs.mkdirSync(lock);
|
|
109
|
+
held = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err.code !== "EEXIST")
|
|
114
|
+
throw err;
|
|
115
|
+
try {
|
|
116
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 10_000) {
|
|
117
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* retry */
|
|
123
|
+
}
|
|
124
|
+
spin(20);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return body();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (held) {
|
|
132
|
+
try {
|
|
133
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* ignore */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function atomic(target, data) {
|
|
142
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
143
|
+
const tmp = `${target}.${crypto.randomBytes(5).toString("hex")}.tmp`;
|
|
144
|
+
try {
|
|
145
|
+
fs.writeFileSync(tmp, data, "utf8");
|
|
146
|
+
fs.renameSync(tmp, target);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(tmp);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* ignore */
|
|
154
|
+
}
|
|
155
|
+
if (!FALLBACK.has(err.code ?? ""))
|
|
156
|
+
throw err;
|
|
157
|
+
fs.writeFileSync(target, data, "utf8");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function readText(target, fallback = "") {
|
|
161
|
+
try {
|
|
162
|
+
return fs.readFileSync(target, "utf8");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return fallback;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function writeText(target, data) {
|
|
169
|
+
withLock(target, () => atomic(target, data));
|
|
170
|
+
}
|
|
171
|
+
function appendLine(target, line) {
|
|
172
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
173
|
+
withLock(target, () => fs.appendFileSync(target, line, "utf8"));
|
|
174
|
+
}
|
|
175
|
+
function readJson(target, fallback) {
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(fs.readFileSync(target, "utf8"));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return fallback;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function writeJson(target, value) {
|
|
184
|
+
withLock(target, () => atomic(target, JSON.stringify(value, null, 2) + "\n"));
|
|
185
|
+
}
|
|
186
|
+
const SECTION = /^##\s+(.+?)\s*$/;
|
|
187
|
+
const ENTRY = /^-\s+`([^`]+)`\s+·\s+~(\d+)\s+tok(?:\s+·\s+\$([\d.]+))?(?:\s+—\s+(.*\S))?\s*$/;
|
|
188
|
+
function lines(text) {
|
|
189
|
+
return text.split(/\r?\n/);
|
|
190
|
+
}
|
|
191
|
+
function parseMap(text) {
|
|
192
|
+
const out = new Map();
|
|
193
|
+
let section = "";
|
|
194
|
+
for (const line of lines(text)) {
|
|
195
|
+
const s = line.match(SECTION);
|
|
196
|
+
if (s) {
|
|
197
|
+
section = s[1].trim();
|
|
198
|
+
if (!out.has(section))
|
|
199
|
+
out.set(section, []);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!section)
|
|
203
|
+
continue;
|
|
204
|
+
const e = line.match(ENTRY);
|
|
205
|
+
if (e) {
|
|
206
|
+
out.get(section).push({
|
|
207
|
+
file: e[1],
|
|
208
|
+
tokens: parseInt(e[2], 10),
|
|
209
|
+
cost: e[3] ? Number(e[3]) : undefined,
|
|
210
|
+
description: e[4] ? e[4].trim() : "",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
function serializeMap(sections, meta) {
|
|
217
|
+
const out = [
|
|
218
|
+
"# Project Map",
|
|
219
|
+
"",
|
|
220
|
+
`_Maintained by PackMind · ${meta.fileCount} files · updated ${meta.updated}_`,
|
|
221
|
+
"",
|
|
222
|
+
];
|
|
223
|
+
for (const key of [...sections.keys()].sort()) {
|
|
224
|
+
const entries = sections.get(key);
|
|
225
|
+
if (entries.length === 0)
|
|
226
|
+
continue;
|
|
227
|
+
out.push(`## ${key}`, "");
|
|
228
|
+
for (const e of [...entries].sort((a, b) => a.file.localeCompare(b.file))) {
|
|
229
|
+
const cost = e.cost && e.cost > 0 ? ` · $${e.cost.toFixed(4)}` : "";
|
|
230
|
+
const desc = e.description ? ` — ${e.description}` : "";
|
|
231
|
+
out.push(`- \`${e.file}\` · ~${e.tokens} tok${cost}${desc}`);
|
|
232
|
+
}
|
|
233
|
+
out.push("");
|
|
234
|
+
}
|
|
235
|
+
return out.join("\n");
|
|
236
|
+
}
|
|
237
|
+
function parseNeverDo(knowledge) {
|
|
238
|
+
const result = [];
|
|
239
|
+
let active = false;
|
|
240
|
+
for (const line of lines(knowledge)) {
|
|
241
|
+
if (/^##\s+/.test(line)) {
|
|
242
|
+
active = /^##\s+Never\s*Do\b/i.test(line);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!active)
|
|
246
|
+
continue;
|
|
247
|
+
const m = line.match(/^[-*]\s+(?:\[[\d-]+\]\s*)?(.+?)\s*$/);
|
|
248
|
+
if (m)
|
|
249
|
+
result.push(m[1]);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
// --- tokens (mirror of cost/estimator.ts) -----------------------------------
|
|
254
|
+
const CODE_EXT = new Set([
|
|
255
|
+
".ts", ".tsx", ".js", ".jsx", ".py", ".rs", ".go", ".java", ".kt", ".c",
|
|
256
|
+
".cpp", ".h", ".hpp", ".cs", ".rb", ".php", ".swift", ".scala", ".sh",
|
|
257
|
+
".css", ".scss", ".sql", ".json", ".yaml", ".yml", ".toml", ".xml", ".html",
|
|
258
|
+
]);
|
|
259
|
+
function estimateTokens(text, hint) {
|
|
260
|
+
if (!text)
|
|
261
|
+
return 0;
|
|
262
|
+
const ext = hint ? path.extname(hint).toLowerCase() : "";
|
|
263
|
+
const charsPerToken = CODE_EXT.has(ext) ? 3.5 : 4.0;
|
|
264
|
+
const byChars = text.length / charsPerToken;
|
|
265
|
+
const words = text.trim().length ? text.trim().split(/\s+/).length : 0;
|
|
266
|
+
const byWords = words / 0.75;
|
|
267
|
+
return Math.max(1, Math.round((byChars + byWords) / 2));
|
|
268
|
+
}
|
|
269
|
+
// --- secrets (mirror of guard/secrets.ts) -----------------------------------
|
|
270
|
+
const SECRET_GLOBS = [
|
|
271
|
+
".env", ".env.*", "*.env", "*.pem", "*.key", "*.p8", "*.p12", "*.pfx", "*.ppk",
|
|
272
|
+
"*.keystore", "*.jks", "*.cer", "*.crt", "*.der", "id_rsa*", "id_dsa*",
|
|
273
|
+
"id_ecdsa*", "id_ed25519*", "credentials", "credentials.*", ".netrc", ".npmrc",
|
|
274
|
+
".pypirc", "*.secret", "*.secrets", "secrets.*", "service-account*.json",
|
|
275
|
+
"*.kdbx", "*.gpg", "*.asc",
|
|
276
|
+
];
|
|
277
|
+
function toRe(glob) {
|
|
278
|
+
const body = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
279
|
+
return new RegExp(`^${body}$`, "i");
|
|
280
|
+
}
|
|
281
|
+
const SECRET_RES = SECRET_GLOBS.map(toRe);
|
|
282
|
+
function looksSecret(file, extra = []) {
|
|
283
|
+
const base = path.basename(file);
|
|
284
|
+
if (SECRET_RES.some((re) => re.test(base)))
|
|
285
|
+
return true;
|
|
286
|
+
return extra.map(toRe).some((re) => re.test(base));
|
|
287
|
+
}
|
|
288
|
+
// --- path guard (mirror of guard/path-guard.ts) -----------------------------
|
|
289
|
+
function confineToRoot(root, candidate) {
|
|
290
|
+
const base = path.resolve(root);
|
|
291
|
+
const resolved = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(base, candidate);
|
|
292
|
+
const rel = path.relative(base, resolved);
|
|
293
|
+
if (rel === "")
|
|
294
|
+
return resolved;
|
|
295
|
+
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
296
|
+
return null;
|
|
297
|
+
return resolved;
|
|
298
|
+
}
|
|
299
|
+
function samePath(root, a, b) {
|
|
300
|
+
const ra = confineToRoot(root, a);
|
|
301
|
+
const rb = confineToRoot(root, b);
|
|
302
|
+
return ra !== null && rb !== null && ra === rb;
|
|
303
|
+
}
|
|
304
|
+
// --- pricing (mirror of cost/pricing.ts) ------------------------------------
|
|
305
|
+
const PRICES = {
|
|
306
|
+
"claude-opus-4-8": { i: 15, o: 75 },
|
|
307
|
+
"claude-sonnet-4-6": { i: 3, o: 15 },
|
|
308
|
+
"claude-haiku-4-5": { i: 1, o: 5 },
|
|
309
|
+
"claude-fable-5": { i: 5, o: 25 },
|
|
310
|
+
};
|
|
311
|
+
function normModel(m) {
|
|
312
|
+
const s = m.toLowerCase();
|
|
313
|
+
if (s.includes("opus"))
|
|
314
|
+
return "claude-opus-4-8";
|
|
315
|
+
if (s.includes("sonnet"))
|
|
316
|
+
return "claude-sonnet-4-6";
|
|
317
|
+
if (s.includes("haiku"))
|
|
318
|
+
return "claude-haiku-4-5";
|
|
319
|
+
if (s.includes("fable"))
|
|
320
|
+
return "claude-fable-5";
|
|
321
|
+
return "claude-opus-4-8";
|
|
322
|
+
}
|
|
323
|
+
function inputCost(model, tokens) {
|
|
324
|
+
return (tokens / 1_000_000) * (PRICES[normModel(model)] ?? PRICES["claude-opus-4-8"]).i;
|
|
325
|
+
}
|
|
326
|
+
function outputCost(model, tokens) {
|
|
327
|
+
return (tokens / 1_000_000) * (PRICES[normModel(model)] ?? PRICES["claude-opus-4-8"]).o;
|
|
328
|
+
}
|
|
329
|
+
function pathGlobRe(glob) {
|
|
330
|
+
const body = glob
|
|
331
|
+
.replace(/[.+^${}()|[\]]/g, "\\$&")
|
|
332
|
+
.replace(/\*\*/g, " ")
|
|
333
|
+
.replace(/\*/g, "[^/]*")
|
|
334
|
+
.replace(/ /g, ".*")
|
|
335
|
+
.replace(/\?/g, ".");
|
|
336
|
+
return new RegExp(`^${body}$`, "i");
|
|
337
|
+
}
|
|
338
|
+
function evaluateWrite(rules, input) {
|
|
339
|
+
const findings = [];
|
|
340
|
+
const isSecret = looksSecret(path.basename(input.relPath), input.extraSecretGlobs);
|
|
341
|
+
for (const rule of rules ?? []) {
|
|
342
|
+
const conditions = [];
|
|
343
|
+
if (rule.secretFile)
|
|
344
|
+
conditions.push(isSecret);
|
|
345
|
+
if (rule.pathGlob)
|
|
346
|
+
conditions.push(pathGlobRe(rule.pathGlob).test(input.relPath));
|
|
347
|
+
if (rule.content) {
|
|
348
|
+
try {
|
|
349
|
+
conditions.push(new RegExp(rule.content).test(input.content));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
conditions.push(false);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (conditions.length > 0 && conditions.every(Boolean)) {
|
|
356
|
+
findings.push({ ruleId: rule.id, message: rule.message, severity: rule.severity });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (input.blockSecrets && isSecret) {
|
|
360
|
+
findings.push({
|
|
361
|
+
ruleId: "block-secrets",
|
|
362
|
+
message: "Writing to a secrets file is blocked by policy (guard.blockSecrets).",
|
|
363
|
+
severity: "block",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return { findings, block: findings.some((f) => f.severity === "block") };
|
|
367
|
+
}
|
|
368
|
+
// --- light description for map upkeep ---------------------------------------
|
|
369
|
+
function describeLite(file, content) {
|
|
370
|
+
const ext = path.extname(file).toLowerCase();
|
|
371
|
+
const head = content.slice(0, 4096);
|
|
372
|
+
if (ext === ".md" || ext === ".mdx") {
|
|
373
|
+
const h = head.match(/^#{1,3}\s+(.+)$/m);
|
|
374
|
+
if (h)
|
|
375
|
+
return clip(h[1]);
|
|
376
|
+
}
|
|
377
|
+
for (const raw of head.split(/\r?\n/)) {
|
|
378
|
+
const line = raw.trim();
|
|
379
|
+
if (!line)
|
|
380
|
+
continue;
|
|
381
|
+
const c = line.match(/^\s*(?:\/\/+|#+|--|;|\*)\s?(.+)$/);
|
|
382
|
+
if (c && c[1].trim().length > 3 && !/^[=*-]+$/.test(c[1].trim()))
|
|
383
|
+
return clip(c[1]);
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
function clip(s, max = 90) {
|
|
389
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
390
|
+
return t.length > max ? t.slice(0, max - 1) + "…" : t;
|
|
391
|
+
}
|
|
392
|
+
function hookConfig() {
|
|
393
|
+
const raw = readJson(brainPath("config.json"), {});
|
|
394
|
+
return {
|
|
395
|
+
model: typeof raw?.model === "string" ? raw.model : "claude-opus-4-8",
|
|
396
|
+
extraSecretGlobs: Array.isArray(raw?.map?.extraSecretGlobs) ? raw.map.extraSecretGlobs : [],
|
|
397
|
+
blockSecrets: raw?.guard?.blockSecrets === true,
|
|
398
|
+
recallEnabled: raw?.recall?.enabled !== false,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
// --- Claude Code hook IO ----------------------------------------------------
|
|
402
|
+
function readStdin() {
|
|
403
|
+
return new Promise((resolve) => {
|
|
404
|
+
const chunks = [];
|
|
405
|
+
let done = false;
|
|
406
|
+
const finish = () => {
|
|
407
|
+
if (done)
|
|
408
|
+
return;
|
|
409
|
+
done = true;
|
|
410
|
+
resolve(chunks.length ? Buffer.concat(chunks).toString("utf8") : "{}");
|
|
411
|
+
};
|
|
412
|
+
try {
|
|
413
|
+
process.stdin.on("data", (c) => chunks.push(Buffer.from(c)));
|
|
414
|
+
process.stdin.on("end", finish);
|
|
415
|
+
process.stdin.on("error", finish);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
finish();
|
|
419
|
+
}
|
|
420
|
+
setTimeout(finish, 4000);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function parseInput(raw) {
|
|
424
|
+
try {
|
|
425
|
+
return JSON.parse(raw);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return {};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function emitContext(event, text) {
|
|
432
|
+
if (!text)
|
|
433
|
+
return;
|
|
434
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: text } }));
|
|
435
|
+
}
|
|
436
|
+
/** PreToolUse deny (hard block) with a reason Claude sees. */
|
|
437
|
+
function emitDeny(reason) {
|
|
438
|
+
process.stdout.write(JSON.stringify({
|
|
439
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
function newSession(id) {
|
|
443
|
+
return {
|
|
444
|
+
id,
|
|
445
|
+
started: new Date().toISOString(),
|
|
446
|
+
reads: {},
|
|
447
|
+
writes: [],
|
|
448
|
+
editCounts: {},
|
|
449
|
+
inputTokens: 0,
|
|
450
|
+
outputTokens: 0,
|
|
451
|
+
inputCost: 0,
|
|
452
|
+
outputCost: 0,
|
|
453
|
+
mapHits: 0,
|
|
454
|
+
mapMisses: 0,
|
|
455
|
+
dedupedReads: 0,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function readSession() {
|
|
459
|
+
return readJson(brainPath("state", "session.json"), null);
|
|
460
|
+
}
|
|
461
|
+
function writeSession(s) {
|
|
462
|
+
writeJson(brainPath("state", "session.json"), s);
|
|
463
|
+
}
|
|
464
|
+
// --- recall queue (zero-dep enqueue) ----------------------------------------
|
|
465
|
+
function enqueueRecall(relPath) {
|
|
466
|
+
const q = readJson(brainPath("recall", "queue.json"), []);
|
|
467
|
+
if (!q.includes(relPath)) {
|
|
468
|
+
q.push(relPath);
|
|
469
|
+
writeJson(brainPath("recall", "queue.json"), q);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("node:fs"));
|
|
37
|
+
const runtime_js_1 = require("./runtime.js");
|
|
38
|
+
function clearStale(dir) {
|
|
39
|
+
try {
|
|
40
|
+
for (const name of fs.readdirSync(dir)) {
|
|
41
|
+
if (name.endsWith(".tmp") || name.endsWith(".lock")) {
|
|
42
|
+
try {
|
|
43
|
+
fs.rmSync(`${dir}/${name}`, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* ignore */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* dir may not exist */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function main() {
|
|
56
|
+
(0, runtime_js_1.requireState)();
|
|
57
|
+
clearStale((0, runtime_js_1.brainPath)("hooks"));
|
|
58
|
+
clearStale((0, runtime_js_1.brainPath)("state"));
|
|
59
|
+
const now = new Date();
|
|
60
|
+
const id = `s-${now.toISOString().slice(0, 19).replace(/[:T]/g, "")}`;
|
|
61
|
+
(0, runtime_js_1.writeSession)((0, runtime_js_1.newSession)(id));
|
|
62
|
+
(0, runtime_js_1.appendLine)((0, runtime_js_1.brainPath)("journal.md"), `\n## ${id} — ${now.toISOString()}\n\n| Time | Action | File | Tokens |\n|------|--------|------|--------|\n`);
|
|
63
|
+
const parts = [];
|
|
64
|
+
const handoff = (0, runtime_js_1.readText)((0, runtime_js_1.brainPath)("handoff.md")).trim();
|
|
65
|
+
// Only surface a handoff that has real content (skip the seed placeholder).
|
|
66
|
+
if (handoff && !/no session recorded yet/i.test(handoff)) {
|
|
67
|
+
parts.push("Session handoff (where we left off):\n" + handoff);
|
|
68
|
+
}
|
|
69
|
+
parts.push("PackMind is active. Use the `recall` MCP tool to search project memory, and `record_solution`/`remember` to capture fixes and decisions. Check `.packmind/map.md` before reading files.");
|
|
70
|
+
(0, runtime_js_1.emitContext)("SessionStart", parts.join("\n\n"));
|
|
71
|
+
}
|
|
72
|
+
main();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const runtime_js_1 = require("./runtime.js");
|
|
4
|
+
async function main() {
|
|
5
|
+
(0, runtime_js_1.requireState)();
|
|
6
|
+
const session = (0, runtime_js_1.readSession)();
|
|
7
|
+
if (!session)
|
|
8
|
+
process.exit(0);
|
|
9
|
+
const reads = Object.keys(session.reads).length;
|
|
10
|
+
if (reads === 0 && session.writes.length === 0)
|
|
11
|
+
process.exit(0);
|
|
12
|
+
const cfg = (0, runtime_js_1.hookConfig)();
|
|
13
|
+
// Commit the session into the lifetime usage ledger (inline; hooks are dep-free).
|
|
14
|
+
const usagePath = (0, runtime_js_1.brainPath)("usage.json");
|
|
15
|
+
const ledger = (0, runtime_js_1.readJson)(usagePath, null);
|
|
16
|
+
if (ledger?.totals) {
|
|
17
|
+
ledger.sessions = ledger.sessions ?? [];
|
|
18
|
+
ledger.sessions.push({
|
|
19
|
+
id: session.id,
|
|
20
|
+
started: session.started,
|
|
21
|
+
ended: new Date().toISOString(),
|
|
22
|
+
inputTokens: session.inputTokens,
|
|
23
|
+
outputTokens: session.outputTokens,
|
|
24
|
+
inputCost: session.inputCost,
|
|
25
|
+
outputCost: session.outputCost,
|
|
26
|
+
reads,
|
|
27
|
+
writes: session.writes.length,
|
|
28
|
+
});
|
|
29
|
+
const t = ledger.totals;
|
|
30
|
+
t.inputTokens += session.inputTokens;
|
|
31
|
+
t.outputTokens += session.outputTokens;
|
|
32
|
+
t.inputCost += session.inputCost;
|
|
33
|
+
t.outputCost += session.outputCost;
|
|
34
|
+
t.reads += reads;
|
|
35
|
+
t.writes += session.writes.length;
|
|
36
|
+
t.sessions += 1;
|
|
37
|
+
t.dedupedReads += session.dedupedReads;
|
|
38
|
+
t.mapHits += session.mapHits;
|
|
39
|
+
(0, runtime_js_1.writeJson)(usagePath, ledger);
|
|
40
|
+
}
|
|
41
|
+
const turnCost = session.inputCost + session.outputCost;
|
|
42
|
+
(0, runtime_js_1.appendLine)((0, runtime_js_1.brainPath)("journal.md"), `\n> ${session.id}: ${reads} reads, ${session.writes.length} writes, ~$${turnCost.toFixed(4)} this turn.\n`);
|
|
43
|
+
// Regenerate the session handoff doc for cheap resume next time.
|
|
44
|
+
if (session.writes.length > 0) {
|
|
45
|
+
const recent = session.writes.slice(-12).map((w) => `- \`${w.file}\` (${w.action})`);
|
|
46
|
+
(0, runtime_js_1.writeText)((0, runtime_js_1.brainPath)("handoff.md"), [
|
|
47
|
+
"# Session Handoff",
|
|
48
|
+
"",
|
|
49
|
+
`_Updated ${new Date().toISOString()} · ${session.id}_`,
|
|
50
|
+
"",
|
|
51
|
+
"## Recently changed",
|
|
52
|
+
...recent,
|
|
53
|
+
"",
|
|
54
|
+
"## Next steps",
|
|
55
|
+
"- (Capture what's left to do here, or via the `remember` MCP tool.)",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n"));
|
|
58
|
+
}
|
|
59
|
+
const reminders = [];
|
|
60
|
+
if (session.writes.length >= 3) {
|
|
61
|
+
reminders.push("Several files changed — record durable lessons/decisions with the `remember` tool and any fixes with `record_solution`.");
|
|
62
|
+
}
|
|
63
|
+
const heavy = Object.entries(session.editCounts).filter(([, n]) => n >= 4);
|
|
64
|
+
if (heavy.length) {
|
|
65
|
+
reminders.push(`Repeatedly edited ${heavy.map(([f]) => `\`${f}\``).join(", ")} — capture the root cause so it isn't rediscovered.`);
|
|
66
|
+
}
|
|
67
|
+
(0, runtime_js_1.emitContext)("Stop", reminders.join(" "));
|
|
68
|
+
}
|
|
69
|
+
main().catch(() => process.exit(0));
|