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.
@@ -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);