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,25 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { brain } from "./files.js";
|
|
3
|
+
import { readTextOr, writeText, appendLine } from "../util/fs-atomic.js";
|
|
4
|
+
const MAX_JOURNAL_LINES = 1500;
|
|
5
|
+
const KEEP_JOURNAL_LINES = 600;
|
|
6
|
+
/**
|
|
7
|
+
* Keep journal.md from growing without bound: once it exceeds a threshold, move
|
|
8
|
+
* the oldest entries into journal.archive.md and keep the recent tail. Returns
|
|
9
|
+
* the number of lines archived (0 if no action needed). Non-destructive — the
|
|
10
|
+
* archived lines are appended, never dropped.
|
|
11
|
+
*/
|
|
12
|
+
export function consolidateJournal(projectRoot) {
|
|
13
|
+
const b = brain(projectRoot);
|
|
14
|
+
const lines = readTextOr(b.journal).split(/\r?\n/);
|
|
15
|
+
if (lines.length <= MAX_JOURNAL_LINES)
|
|
16
|
+
return 0;
|
|
17
|
+
const header = lines.slice(0, 3);
|
|
18
|
+
const body = lines.slice(3);
|
|
19
|
+
const cut = body.length - KEEP_JOURNAL_LINES;
|
|
20
|
+
const archived = body.slice(0, cut);
|
|
21
|
+
const kept = body.slice(cut);
|
|
22
|
+
appendLine(path.join(b.dir, "journal.archive.md"), archived.join("\n") + "\n");
|
|
23
|
+
writeText(b.journal, [...header, "", `> (${cut} older lines archived to journal.archive.md)`, "", ...kept].join("\n"));
|
|
24
|
+
return cut;
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { walkProject } from "./walk.js";
|
|
4
|
+
import { describeFile } from "./describe.js";
|
|
5
|
+
import { parseMap, serializeMap } from "./formats.js";
|
|
6
|
+
import { brain } from "./files.js";
|
|
7
|
+
import { writeText, readTextOr } from "../util/fs-atomic.js";
|
|
8
|
+
import { estimateTokens } from "../cost/estimator.js";
|
|
9
|
+
import { inputCost } from "../cost/pricing.js";
|
|
10
|
+
function sectionFor(rel) {
|
|
11
|
+
const dir = path.posix.dirname(rel);
|
|
12
|
+
return dir === "." ? "./" : dir + "/";
|
|
13
|
+
}
|
|
14
|
+
export function buildMap(projectRoot, config) {
|
|
15
|
+
const sections = new Map();
|
|
16
|
+
for (const { abs, rel } of walkProject(projectRoot, config)) {
|
|
17
|
+
let content;
|
|
18
|
+
try {
|
|
19
|
+
content = fs.readFileSync(abs, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const tokens = estimateTokens(content, rel);
|
|
25
|
+
const key = sectionFor(rel);
|
|
26
|
+
if (!sections.has(key))
|
|
27
|
+
sections.set(key, []);
|
|
28
|
+
sections.get(key).push({
|
|
29
|
+
file: path.posix.basename(rel),
|
|
30
|
+
description: describeFile(rel, content),
|
|
31
|
+
tokens,
|
|
32
|
+
cost: inputCost(config.model, tokens),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
let fileCount = 0;
|
|
36
|
+
for (const [, list] of sections)
|
|
37
|
+
fileCount += list.length;
|
|
38
|
+
return {
|
|
39
|
+
content: serializeMap(sections, { fileCount, updated: new Date().toISOString() }),
|
|
40
|
+
fileCount,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function scanProject(projectRoot, config) {
|
|
44
|
+
const { content, fileCount } = buildMap(projectRoot, config);
|
|
45
|
+
writeText(brain(projectRoot).map, content);
|
|
46
|
+
return fileCount;
|
|
47
|
+
}
|
|
48
|
+
export function countMapEntries(content) {
|
|
49
|
+
let n = 0;
|
|
50
|
+
for (const [, list] of parseMap(content))
|
|
51
|
+
n += list.length;
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
export function currentMapEntries(projectRoot) {
|
|
55
|
+
return countMapEntries(readTextOr(brain(projectRoot).map));
|
|
56
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { homeDirectory } from "../util/platform.js";
|
|
4
|
+
import { STATE_DIR } from "../util/paths.js";
|
|
5
|
+
const ROOT_MARKERS = [
|
|
6
|
+
".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "deno.json",
|
|
7
|
+
"pom.xml", "build.gradle", "composer.json", "Gemfile", "Makefile",
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the project root. Honors an explicit env override
|
|
11
|
+
* (PACKMIND_ROOT / CLAUDE_PROJECT_DIR), then walks up to a marker or an existing
|
|
12
|
+
* `.packmind/`. Never ascends above the user's home directory.
|
|
13
|
+
*/
|
|
14
|
+
export function findRoot(start) {
|
|
15
|
+
const override = process.env.PACKMIND_ROOT || process.env.CLAUDE_PROJECT_DIR;
|
|
16
|
+
let dir = path.resolve(override || start || process.cwd());
|
|
17
|
+
const fsRoot = path.parse(dir).root;
|
|
18
|
+
const home = homeDirectory();
|
|
19
|
+
for (let i = 0; i < 30; i++) {
|
|
20
|
+
if (fs.existsSync(path.join(dir, STATE_DIR)))
|
|
21
|
+
return dir;
|
|
22
|
+
if (ROOT_MARKERS.some((m) => fs.existsSync(path.join(dir, m))))
|
|
23
|
+
return dir;
|
|
24
|
+
const up = path.dirname(dir);
|
|
25
|
+
if (up === dir || up === fsRoot || dir === home)
|
|
26
|
+
break;
|
|
27
|
+
dir = up;
|
|
28
|
+
}
|
|
29
|
+
return path.resolve(override || start || process.cwd());
|
|
30
|
+
}
|
|
31
|
+
export function isHome(dir) {
|
|
32
|
+
return path.resolve(dir) === path.resolve(homeDirectory());
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readJsonOr } from "../util/fs-atomic.js";
|
|
2
|
+
export const DEFAULT_CONFIG = {
|
|
3
|
+
version: 1,
|
|
4
|
+
model: "claude-opus-4-8",
|
|
5
|
+
map: {
|
|
6
|
+
autoScanOnInit: true,
|
|
7
|
+
maxFiles: 600,
|
|
8
|
+
respectGitignore: true,
|
|
9
|
+
excludeDirs: [
|
|
10
|
+
"node_modules", ".git", ".packmind", ".claude", "dist", "build", "out",
|
|
11
|
+
".next", ".nuxt", ".svelte-kit", "coverage", "__pycache__", ".cache",
|
|
12
|
+
"target", ".venv", "vendor", ".turbo", ".vercel", ".idea", ".vscode",
|
|
13
|
+
],
|
|
14
|
+
extraSecretGlobs: [],
|
|
15
|
+
},
|
|
16
|
+
cost: { exact: "auto" },
|
|
17
|
+
recall: {
|
|
18
|
+
enabled: true,
|
|
19
|
+
embedModel: "Xenova/all-MiniLM-L6-v2",
|
|
20
|
+
chunkChars: 1200,
|
|
21
|
+
topK: 6,
|
|
22
|
+
},
|
|
23
|
+
guard: { blockSecrets: false },
|
|
24
|
+
claude: {
|
|
25
|
+
settingsPath: ".claude/settings.json",
|
|
26
|
+
claudeMdPath: "CLAUDE.md",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
function isPlainObject(v) {
|
|
30
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
31
|
+
}
|
|
32
|
+
/** Recursively merge `patch` onto `base`; arrays and scalars are replaced. */
|
|
33
|
+
export function deepMerge(base, patch) {
|
|
34
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) {
|
|
35
|
+
return patch === undefined ? base : patch;
|
|
36
|
+
}
|
|
37
|
+
const merged = { ...base };
|
|
38
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
39
|
+
merged[key] = isPlainObject(value) && isPlainObject(merged[key])
|
|
40
|
+
? deepMerge(merged[key], value)
|
|
41
|
+
: value;
|
|
42
|
+
}
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
45
|
+
export function loadConfig(configPath) {
|
|
46
|
+
return deepMerge(DEFAULT_CONFIG, readJsonOr(configPath, {}));
|
|
47
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { brain } from "./files.js";
|
|
4
|
+
import { userRoot } from "../util/platform.js";
|
|
5
|
+
/** Root for all project backups: ~/.packmind/backups/<project>/<timestamp>/ */
|
|
6
|
+
export function backupsRoot() {
|
|
7
|
+
return path.join(userRoot(), "backups");
|
|
8
|
+
}
|
|
9
|
+
function projectBackupDir(projectRoot) {
|
|
10
|
+
return path.join(backupsRoot(), path.basename(path.resolve(projectRoot)));
|
|
11
|
+
}
|
|
12
|
+
function timestamp() {
|
|
13
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot the project's `.packmind/` into a timestamped backup. The large,
|
|
17
|
+
* fully-regenerable vector index is skipped (rebuild with `packmind index`),
|
|
18
|
+
* along with transient lock/temp files. Returns the backup path.
|
|
19
|
+
*/
|
|
20
|
+
export function createSnapshot(projectRoot, label) {
|
|
21
|
+
const src = brain(projectRoot).dir;
|
|
22
|
+
const dest = path.join(projectBackupDir(projectRoot), label ?? timestamp());
|
|
23
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
24
|
+
fs.cpSync(src, dest, {
|
|
25
|
+
recursive: true,
|
|
26
|
+
filter: (from) => {
|
|
27
|
+
const base = path.basename(from);
|
|
28
|
+
if (base.endsWith(".lock") || base.endsWith(".tmp"))
|
|
29
|
+
return false;
|
|
30
|
+
if (from.endsWith(`${path.sep}recall${path.sep}vectors.json`))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return dest;
|
|
36
|
+
}
|
|
37
|
+
export function listSnapshots(projectRoot) {
|
|
38
|
+
try {
|
|
39
|
+
return fs
|
|
40
|
+
.readdirSync(projectBackupDir(projectRoot))
|
|
41
|
+
.filter((d) => fs.statSync(path.join(projectBackupDir(projectRoot), d)).isDirectory())
|
|
42
|
+
.sort();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function restoreSnapshot(projectRoot, label) {
|
|
49
|
+
const src = path.join(projectBackupDir(projectRoot), label);
|
|
50
|
+
if (!fs.existsSync(src))
|
|
51
|
+
return false;
|
|
52
|
+
fs.cpSync(src, brain(projectRoot).dir, { recursive: true });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/** Keep only the most recent `keep` snapshots; returns how many were removed. */
|
|
56
|
+
export function pruneSnapshots(projectRoot, keep) {
|
|
57
|
+
const dir = projectBackupDir(projectRoot);
|
|
58
|
+
const all = listSnapshots(projectRoot);
|
|
59
|
+
const toRemove = all.slice(0, Math.max(0, all.length - keep));
|
|
60
|
+
for (const d of toRemove) {
|
|
61
|
+
try {
|
|
62
|
+
fs.rmSync(path.join(dir, d), { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* best effort */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return toRemove.length;
|
|
69
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import ignore from "ignore";
|
|
4
|
+
import { relativePosix } from "../util/paths.js";
|
|
5
|
+
import { looksSecret } from "../guard/secrets.js";
|
|
6
|
+
const BINARY_EXT = new Set([
|
|
7
|
+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".avif",
|
|
8
|
+
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
|
9
|
+
".zip", ".tar", ".gz", ".bz2", ".7z", ".rar", ".jar",
|
|
10
|
+
".exe", ".dll", ".so", ".dylib", ".bin", ".wasm",
|
|
11
|
+
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
|
12
|
+
".mp3", ".mp4", ".avi", ".mov", ".webm", ".ogg", ".flac",
|
|
13
|
+
".sqlite", ".db", ".lock",
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Walk the project, yielding text source files. Honors `.gitignore`, the config
|
|
17
|
+
* exclude list, the secrets denylist, a per-file size cap, and a max-file cap.
|
|
18
|
+
* Used by both the map scanner and the recall indexer so they always agree on
|
|
19
|
+
* which files exist.
|
|
20
|
+
*/
|
|
21
|
+
export function walkProject(projectRoot, config) {
|
|
22
|
+
const c = config.map;
|
|
23
|
+
const ig = ignore().add([".git", ".packmind"]);
|
|
24
|
+
if (c.respectGitignore) {
|
|
25
|
+
try {
|
|
26
|
+
ig.add(fs.readFileSync(path.join(projectRoot, ".gitignore"), "utf8"));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* no .gitignore */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const excluded = new Set(c.excludeDirs);
|
|
33
|
+
const results = [];
|
|
34
|
+
const walk = (dir) => {
|
|
35
|
+
if (results.length >= c.maxFiles)
|
|
36
|
+
return;
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (results.length >= c.maxFiles)
|
|
47
|
+
return;
|
|
48
|
+
const abs = path.join(dir, entry.name);
|
|
49
|
+
const rel = relativePosix(projectRoot, abs);
|
|
50
|
+
if (c.respectGitignore && ig.ignores(entry.isDirectory() ? rel + "/" : rel))
|
|
51
|
+
continue;
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
if (excluded.has(entry.name))
|
|
54
|
+
continue;
|
|
55
|
+
walk(abs);
|
|
56
|
+
}
|
|
57
|
+
else if (entry.isFile()) {
|
|
58
|
+
if (BINARY_EXT.has(path.extname(entry.name).toLowerCase()))
|
|
59
|
+
continue;
|
|
60
|
+
if (looksSecret(entry.name, c.extraSecretGlobs))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
if (fs.statSync(abs).size > 1_048_576)
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
results.push({ abs, rel });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
walk(projectRoot);
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
/**
|
|
5
|
+
* Crash-safe, concurrency-safe file IO.
|
|
6
|
+
*
|
|
7
|
+
* Writes go to a temp sibling and are atomically renamed into place. A coarse
|
|
8
|
+
* advisory lock (an exclusively-created `<file>.lock` directory) serializes
|
|
9
|
+
* writers so concurrent hook processes can't interleave and corrupt a shared
|
|
10
|
+
* file. Stale locks (older than LOCK_TTL_MS) are reclaimed.
|
|
11
|
+
*/
|
|
12
|
+
const FALLBACK_CODES = new Set(["EBUSY", "EACCES", "EPERM", "EXDEV"]);
|
|
13
|
+
const LOCK_TRIES = 60;
|
|
14
|
+
const LOCK_WAIT_MS = 20;
|
|
15
|
+
const LOCK_TTL_MS = 10_000;
|
|
16
|
+
function busyWait(ms) {
|
|
17
|
+
const until = Date.now() + ms;
|
|
18
|
+
while (Date.now() < until) {
|
|
19
|
+
/* hooks are short synchronous scripts; a brief spin is acceptable */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function withLock(target, body) {
|
|
23
|
+
const parent = path.dirname(target);
|
|
24
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
25
|
+
const lock = `${target}.lock`;
|
|
26
|
+
let held = false;
|
|
27
|
+
for (let i = 0; i < LOCK_TRIES; i++) {
|
|
28
|
+
try {
|
|
29
|
+
fs.mkdirSync(lock);
|
|
30
|
+
held = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err.code !== "EEXIST")
|
|
35
|
+
throw err;
|
|
36
|
+
try {
|
|
37
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_TTL_MS) {
|
|
38
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* lock disappeared; loop and retry */
|
|
44
|
+
}
|
|
45
|
+
busyWait(LOCK_WAIT_MS);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return body();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (held) {
|
|
53
|
+
try {
|
|
54
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* best effort */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function writeAtomic(target, data) {
|
|
63
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
64
|
+
const temp = `${target}.${crypto.randomBytes(5).toString("hex")}.tmp`;
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(temp, data, "utf8");
|
|
67
|
+
fs.renameSync(temp, target);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(temp);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* temp may not exist */
|
|
75
|
+
}
|
|
76
|
+
if (!FALLBACK_CODES.has(err.code ?? ""))
|
|
77
|
+
throw err;
|
|
78
|
+
fs.writeFileSync(target, data, "utf8");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function readTextOr(target, fallback = "") {
|
|
82
|
+
try {
|
|
83
|
+
return fs.readFileSync(target, "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function writeText(target, data) {
|
|
90
|
+
withLock(target, () => writeAtomic(target, data));
|
|
91
|
+
}
|
|
92
|
+
export function appendLine(target, line) {
|
|
93
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
94
|
+
withLock(target, () => fs.appendFileSync(target, line, "utf8"));
|
|
95
|
+
}
|
|
96
|
+
export function readJsonOr(target, fallback) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(fs.readFileSync(target, "utf8"));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return fallback;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function writeJson(target, value) {
|
|
105
|
+
withLock(target, () => writeAtomic(target, JSON.stringify(value, null, 2) + "\n"));
|
|
106
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const RANK = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
2
|
+
let threshold = process.env.PACKMIND_LOG || "info";
|
|
3
|
+
export function setLogLevel(level) {
|
|
4
|
+
threshold = level;
|
|
5
|
+
}
|
|
6
|
+
function line(level, msg) {
|
|
7
|
+
if (RANK[level] < RANK[threshold])
|
|
8
|
+
return;
|
|
9
|
+
const stream = level === "warn" || level === "error" ? process.stderr : process.stdout;
|
|
10
|
+
stream.write(`[packmind] ${msg}\n`);
|
|
11
|
+
}
|
|
12
|
+
export const log = {
|
|
13
|
+
debug: (m) => line("debug", m),
|
|
14
|
+
info: (m) => line("info", m),
|
|
15
|
+
warn: (m) => line("warn", m),
|
|
16
|
+
error: (m) => line("error", m),
|
|
17
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
/** Name of PackMind's per-project state directory. */
|
|
4
|
+
export const STATE_DIR = ".packmind";
|
|
5
|
+
/** Convert OS-specific separators to POSIX slashes for stable comparisons. */
|
|
6
|
+
export function toPosix(p) {
|
|
7
|
+
return p.split(path.sep).join("/");
|
|
8
|
+
}
|
|
9
|
+
export function stateDirFor(projectRoot) {
|
|
10
|
+
return path.join(projectRoot, STATE_DIR);
|
|
11
|
+
}
|
|
12
|
+
export function stateFile(projectRoot, ...parts) {
|
|
13
|
+
return path.join(projectRoot, STATE_DIR, ...parts);
|
|
14
|
+
}
|
|
15
|
+
export function ensureDir(dir) {
|
|
16
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
export function relativePosix(from, to) {
|
|
19
|
+
return toPosix(path.relative(from, to));
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
export const onWindows = process.platform === "win32";
|
|
3
|
+
export const onMac = process.platform === "darwin";
|
|
4
|
+
export function homeDirectory() {
|
|
5
|
+
return os.homedir();
|
|
6
|
+
}
|
|
7
|
+
/** PackMind's user-global cache/config root (models, registry). */
|
|
8
|
+
export function userRoot() {
|
|
9
|
+
return `${os.homedir()}/.packmind`;
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "packmind",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A second brain for Claude Code: project memory, real token & cost accounting, semantic recall, and active guardrails.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"packmind": "./dist/bin/packmind.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"prebuild": "node -e \"const fs=require('fs');if(fs.existsSync('dist'))fs.rmSync('dist',{recursive:true})\"",
|
|
11
|
+
"build": "tsc && tsc -p tsconfig.hooks.json",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"prepublishOnly": "pnpm build"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
19
|
+
"chalk": "^5.3.0",
|
|
20
|
+
"commander": "^12.1.0",
|
|
21
|
+
"ignore": "^6.0.2"
|
|
22
|
+
},
|
|
23
|
+
"optionalDependencies": {
|
|
24
|
+
"@xenova/transformers": "^2.17.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"typescript": "^5.7.0",
|
|
29
|
+
"vitest": "^2.1.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.0.0"
|
|
33
|
+
},
|
|
34
|
+
"pnpm": {
|
|
35
|
+
"onlyBuiltDependencies": [
|
|
36
|
+
"esbuild",
|
|
37
|
+
"onnxruntime-node",
|
|
38
|
+
"sharp",
|
|
39
|
+
"protobufjs"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"license": "Apache-2.0",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"author": "Michael Scherding",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/mchl-schrdng/packmind.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/mchl-schrdng/packmind",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/mchl-schrdng/packmind/issues"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"claude",
|
|
57
|
+
"claude-code",
|
|
58
|
+
"mcp",
|
|
59
|
+
"ai",
|
|
60
|
+
"context",
|
|
61
|
+
"tokens",
|
|
62
|
+
"cost",
|
|
63
|
+
"memory",
|
|
64
|
+
"developer-tools"
|
|
65
|
+
],
|
|
66
|
+
"files": [
|
|
67
|
+
"dist/",
|
|
68
|
+
"src/templates/",
|
|
69
|
+
"LICENSE",
|
|
70
|
+
"README.md"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# PACKMIND.md — Operating Protocol
|
|
2
|
+
|
|
3
|
+
PackMind gives you (Claude) a persistent second brain for this project, stored in
|
|
4
|
+
`.packmind/`. It surfaces context automatically through hooks and exposes tools
|
|
5
|
+
through the **packmind** MCP server. Follow this protocol each session.
|
|
6
|
+
|
|
7
|
+
## Before reading a file
|
|
8
|
+
- Check `map.md` first — if a file's description and token/cost estimate answer
|
|
9
|
+
your question, don't open the whole file.
|
|
10
|
+
- Don't re-read a file you already read this session unless it changed.
|
|
11
|
+
|
|
12
|
+
## Before writing code
|
|
13
|
+
- Heed any guardrail warnings (they reference `policy.json` and the secrets
|
|
14
|
+
denylist). A blocked write means policy forbids it — choose another path.
|
|
15
|
+
- Honor the `## Never Do` list in `knowledge.md`.
|
|
16
|
+
|
|
17
|
+
## Use the MCP tools
|
|
18
|
+
- `recall("…")` — semantic search across project memory. Use it before
|
|
19
|
+
investigating a bug or re-deriving how something works.
|
|
20
|
+
- `remember(note, kind)` — save a preference, decision, never-do rule, or note.
|
|
21
|
+
- `record_solution(error, cause, fix, tags)` — log a fix so it's never
|
|
22
|
+
rediscovered.
|
|
23
|
+
- `project_map(filter?)` — list files with descriptions and token estimates.
|
|
24
|
+
- `usage_report()` — token usage and dollar cost so far.
|
|
25
|
+
- `handoff("get"|"set", content?)` — read or update the resume note.
|
|
26
|
+
|
|
27
|
+
## When you finish meaningful work
|
|
28
|
+
- `remember` durable lessons/preferences/decisions.
|
|
29
|
+
- `record_solution` for any real bug you fixed.
|
|
30
|
+
- `handoff("set", …)` with where things stand and what's next.
|
|
31
|
+
|
|
32
|
+
## Files in `.packmind/`
|
|
33
|
+
| File | Purpose |
|
|
34
|
+
|------|---------|
|
|
35
|
+
| `map.md` | File map: description, token estimate, est. read cost |
|
|
36
|
+
| `knowledge.md` | Preferences, decisions, never-do list, notes |
|
|
37
|
+
| `journal.md` | Chronological action log + session summaries |
|
|
38
|
+
| `solutions.json` | Known bugs and their fixes |
|
|
39
|
+
| `usage.json` | Token + dollar-cost ledger |
|
|
40
|
+
| `handoff.md` | Session resume note |
|
|
41
|
+
| `policy.json` | Guardrail rules |
|
|
42
|
+
| `recall/` | Local semantic index (never leaves your machine) |
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- PACKMIND:START -->
|
|
2
|
+
## PackMind
|
|
3
|
+
|
|
4
|
+
This project uses **PackMind** — a second brain for Claude Code. Read
|
|
5
|
+
`.packmind/PACKMIND.md` and follow it: consult `.packmind/map.md` before reading
|
|
6
|
+
files, heed guardrail warnings before writing, and use the **packmind** MCP tools
|
|
7
|
+
(`recall`, `remember`, `record_solution`, `project_map`, `usage_report`,
|
|
8
|
+
`handoff`) to read and update project memory.
|
|
9
|
+
<!-- PACKMIND:END -->
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"model": "claude-opus-4-8",
|
|
4
|
+
"map": {
|
|
5
|
+
"autoScanOnInit": true,
|
|
6
|
+
"maxFiles": 600,
|
|
7
|
+
"respectGitignore": true,
|
|
8
|
+
"excludeDirs": [
|
|
9
|
+
"node_modules",
|
|
10
|
+
".git",
|
|
11
|
+
".packmind",
|
|
12
|
+
".claude",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
"out",
|
|
16
|
+
".next",
|
|
17
|
+
".nuxt",
|
|
18
|
+
".svelte-kit",
|
|
19
|
+
"coverage",
|
|
20
|
+
"__pycache__",
|
|
21
|
+
".cache",
|
|
22
|
+
"target",
|
|
23
|
+
".venv",
|
|
24
|
+
"vendor",
|
|
25
|
+
".turbo",
|
|
26
|
+
".vercel",
|
|
27
|
+
".idea",
|
|
28
|
+
".vscode"
|
|
29
|
+
],
|
|
30
|
+
"extraSecretGlobs": []
|
|
31
|
+
},
|
|
32
|
+
"cost": {
|
|
33
|
+
"exact": "auto"
|
|
34
|
+
},
|
|
35
|
+
"recall": {
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"embedModel": "Xenova/all-MiniLM-L6-v2",
|
|
38
|
+
"chunkChars": 1200,
|
|
39
|
+
"topK": 6
|
|
40
|
+
},
|
|
41
|
+
"guard": {
|
|
42
|
+
"blockSecrets": false
|
|
43
|
+
},
|
|
44
|
+
"claude": {
|
|
45
|
+
"settingsPath": ".claude/settings.json",
|
|
46
|
+
"claudeMdPath": "CLAUDE.md"
|
|
47
|
+
}
|
|
48
|
+
}
|