sawbuck 0.1.43 → 0.1.45
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/CONTRACT.md +2 -2
- package/README.md +20 -21
- package/agent-block.mjs +9 -16
- package/contract-cmd.mjs +32 -23
- package/contract.mjs +1 -1
- package/debt.mjs +3 -3
- package/exclude.mjs +16 -10
- package/memory.mjs +16 -156
- package/package.json +3 -8
- package/rank.mjs +5 -5
- package/reflect.mjs +2 -2
- package/repo-state.mjs +22 -0
- package/rules.mjs +1 -1
- package/sawbuck-hook.mjs +22 -37
- package/sawbuck.mjs +76 -223
- package/shape.mjs +35 -50
- package/status.mjs +6 -12
- package/steers.mjs +1 -1
- package/dispatch.mjs +0 -34
- package/exposure.mjs +0 -104
- package/providers/claude.mjs +0 -49
- package/providers/local.mjs +0 -55
- package/providers/mem0.mjs +0 -120
- package/providers/obsidian.mjs +0 -119
package/providers/obsidian.mjs
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// obsidian.mjs, a REFERENCE memory provider for Sawbuck's Memory Contract: an Obsidian vault as the
|
|
3
|
-
// memory. Shows how to fill the socket, and the second contract implementation. Zero-dep. Opt in with
|
|
4
|
-
// SAWBUCK_MEMORY=logbook,obsidian and OBSIDIAN_VAULT=/path/to/vault (compose it WITH logbook: logbook
|
|
5
|
-
// stays the git-truth + debt provider, this adds your written notes).
|
|
6
|
-
//
|
|
7
|
-
// recall --file P [--file Q] [--scope ID ...] --json notes relevant to those paths -> Finding[] (kind note)
|
|
8
|
-
// record --ref SHA --summary S --by WHO [--scope ID] write a note into the vault
|
|
9
|
-
// debt --json EXITS NONZERO: a note vault cannot know what is silenced in HEAD
|
|
10
|
-
//
|
|
11
|
-
// FOLDER SCOPING (isolation): --scope carries the folder scope chain, leaf-first, as paths relative to
|
|
12
|
-
// the memory root (Sawbuck computes them; "" is the vault root = shared/global). The vault MIRRORS the
|
|
13
|
-
// project tree: notes for scope "group/repo" live in `<vault>/group/repo/`. recall searches the LEAF
|
|
14
|
-
// scope recursively (so the repo's own notes + its Sawbuck/ folder are found) and each ANCESTOR scope
|
|
15
|
-
// TOP-LEVEL ONLY (so it never descends into a sibling repo's folder). With no --scope it searches the
|
|
16
|
-
// whole vault (direct calls / back-compat). record writes to the LEAF scope's Sawbuck/ folder.
|
|
17
|
-
|
|
18
|
-
import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, existsSync } from 'node:fs';
|
|
19
|
-
import { join, basename, relative, extname, resolve, sep } from 'node:path';
|
|
20
|
-
|
|
21
|
-
const argv = process.argv.slice(2);
|
|
22
|
-
const cmd = argv[0];
|
|
23
|
-
const flagAll = (n) => argv.reduce((a, v, i) => (argv[i - 1] === n ? [...a, v] : a), []);
|
|
24
|
-
const flag = (n, d) => { const i = argv.indexOf(n); return i > -1 ? (argv[i + 1] ?? true) : d; };
|
|
25
|
-
|
|
26
|
-
const VAULT = process.env.OBSIDIAN_VAULT ? resolve(process.env.OBSIDIAN_VAULT) : '';
|
|
27
|
-
const die = (msg, code = 2) => { process.stderr.write(`obsidian provider: ${msg}\n`); process.exit(code); };
|
|
28
|
-
|
|
29
|
-
const MAX_FILES = +(process.env.OBSIDIAN_MAX_FILES) || 4000;
|
|
30
|
-
const MAX_BYTES = 512 * 1024;
|
|
31
|
-
|
|
32
|
-
// resolve a scope id to a dir INSIDE the vault; null if it would escape (path-traversal guard).
|
|
33
|
-
function scopeDir(id) {
|
|
34
|
-
if (id === '' || id == null) return VAULT;
|
|
35
|
-
const p = resolve(VAULT, id);
|
|
36
|
-
return (p === VAULT || p.startsWith(VAULT + sep)) ? p : null;
|
|
37
|
-
}
|
|
38
|
-
// collect .md files; recursive for the leaf scope, top-level only for an ancestor (so an ancestor
|
|
39
|
-
// search never descends into a sibling repo's folder).
|
|
40
|
-
function walkMd(dir, recursive, out) {
|
|
41
|
-
let entries = [];
|
|
42
|
-
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
43
|
-
for (const e of entries) {
|
|
44
|
-
if (out.length >= MAX_FILES) return;
|
|
45
|
-
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
46
|
-
const p = join(dir, e.name);
|
|
47
|
-
if (e.isDirectory()) { if (recursive) walkMd(p, true, out); }
|
|
48
|
-
else if (extname(e.name).toLowerCase() === '.md') out.push(p);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// the dirs to search, with their scope id and recursion, from the --scope chain (leaf-first).
|
|
52
|
-
function searchScopes(scopes) {
|
|
53
|
-
if (!scopes.length) return [{ id: '', dir: VAULT, recursive: true }]; // no scope -> whole vault
|
|
54
|
-
const out = []; const seen = new Set();
|
|
55
|
-
scopes.forEach((id, i) => {
|
|
56
|
-
const dir = scopeDir(id);
|
|
57
|
-
if (!dir || seen.has(dir)) return; seen.add(dir);
|
|
58
|
-
out.push({ id, dir, recursive: i === 0 }); // leaf recursive; ancestors top-level only
|
|
59
|
-
});
|
|
60
|
-
if (!seen.has(VAULT)) out.push({ id: '', dir: VAULT, recursive: false }); // vault root is the shared scope
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (cmd === 'debt') {
|
|
65
|
-
die('debt is unsupported by a note vault (git-truth lives in a git provider like logbook)', 2);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (cmd === 'recall') {
|
|
69
|
-
if (!VAULT || !existsSync(VAULT)) { console.log('[]'); process.exit(0); }
|
|
70
|
-
const paths = flagAll('--file');
|
|
71
|
-
if (!paths.length) { console.log('[]'); process.exit(0); }
|
|
72
|
-
const needles = [...new Set(paths.flatMap((p) => [p, basename(p)]).filter(Boolean))];
|
|
73
|
-
const findings = [];
|
|
74
|
-
for (const { id, dir, recursive } of searchScopes(flagAll('--scope'))) {
|
|
75
|
-
const files = []; walkMd(dir, recursive, files);
|
|
76
|
-
for (const file of files) {
|
|
77
|
-
let text = '';
|
|
78
|
-
try { if (statSync(file).size > MAX_BYTES) continue; text = readFileSync(file, 'utf8'); } catch { continue; }
|
|
79
|
-
const hitPath = paths.find((p) => text.includes(p) || text.includes(basename(p)));
|
|
80
|
-
if (!hitPath) continue;
|
|
81
|
-
let hits = 0; for (const n of needles) hits += text.split(n).length - 1;
|
|
82
|
-
const title = (text.match(/^#\s+(.+)$/m) || [, basename(file, '.md')])[1].trim();
|
|
83
|
-
const lineWith = (text.split('\n').find((l) => needles.some((n) => l.includes(n))) || '').trim().slice(0, 160);
|
|
84
|
-
let date = null; try { date = new Date(statSync(file).mtimeMs).toISOString().slice(0, 10); } catch {}
|
|
85
|
-
findings.push({ kind: 'note', path: hitPath, ref: relative(VAULT, file), date, summary: `${title}: ${lineWith}`.slice(0, 280), scope: id, score: Math.min(1, hits / 5), _hits: hits });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
findings.sort((a, b) => b._hits - a._hits || String(b.date).localeCompare(String(a.date)));
|
|
89
|
-
console.log(JSON.stringify(findings.slice(0, 8).map(({ _hits, ...f }) => f)));
|
|
90
|
-
process.exit(0);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (cmd === 'record') {
|
|
94
|
-
if (!VAULT || !existsSync(VAULT)) die('cannot record: OBSIDIAN_VAULT is unset or missing', 2);
|
|
95
|
-
const ref = flag('--ref', ''), summary = String(flag('--summary', '')), by = String(flag('--by', 'sawbuck'));
|
|
96
|
-
const side = flag('--side', ''), evidence = flag('--evidence-file', '');
|
|
97
|
-
const leaf = flagAll('--scope')[0] ?? '';
|
|
98
|
-
const base = scopeDir(leaf) || VAULT; // the finding belongs to its most-specific scope
|
|
99
|
-
const dir = join(base, 'Sawbuck');
|
|
100
|
-
try { mkdirSync(dir, { recursive: true }); } catch {}
|
|
101
|
-
const safeRef = String(ref || Date.now()).replace(/[^\w.-]/g, '_').slice(0, 40);
|
|
102
|
-
const date = new Date().toISOString();
|
|
103
|
-
// a [[wikilink]] to the evidence path so Obsidian's graph connects the finding to the code note.
|
|
104
|
-
const link = evidence ? `\n- evidence: [[${String(evidence).replace(/[[\]]/g, '')}]]` : '';
|
|
105
|
-
const body = `---\nsawbuck: finding\nref: ${ref}\nby: ${by}\ndate: ${date}\nscope: ${leaf}\n---\n\n# ${summary.slice(0, 80)}\n\n${summary}\n\n- ref: \`${ref}\`${side ? `\n- side: ${side}` : ''}${link}\n`;
|
|
106
|
-
// no-clobber + dedupe: if this exact finding is already noted, skip; if a DIFFERENT note shares the
|
|
107
|
-
// ref, write a suffixed sibling instead of overwriting the earlier one.
|
|
108
|
-
let target = join(dir, `${safeRef}.md`);
|
|
109
|
-
if (existsSync(target)) {
|
|
110
|
-
let prev = ''; try { prev = readFileSync(target, 'utf8'); } catch {}
|
|
111
|
-
if (prev.includes(summary)) { console.log('recorded (already present)'); process.exit(0); }
|
|
112
|
-
let i = 2; while (existsSync(join(dir, `${safeRef}-${i}.md`))) i++;
|
|
113
|
-
target = join(dir, `${safeRef}-${i}.md`);
|
|
114
|
-
}
|
|
115
|
-
try { writeFileSync(target, body); console.log('recorded'); process.exit(0); }
|
|
116
|
-
catch (e) { die(`write failed: ${String(e.message).split('\n')[0]}`, 1); }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
die(`unknown command '${cmd || ''}' (recall | debt | record)`, 2);
|