@davesheffer/hunch 0.10.0 → 0.10.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/LICENSE +21 -21
- package/dist/extractors/parse.js +13 -13
- package/dist/store/hunchStore.js +39 -40
- package/dist/store/jsonStore.js +39 -3
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Dave Sheffer
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dave Sheffer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/extractors/parse.js
CHANGED
|
@@ -26,19 +26,19 @@ const BUILTIN_METHODS = new Set([
|
|
|
26
26
|
"log", "error", "warn", "info", "debug",
|
|
27
27
|
]);
|
|
28
28
|
/** Tree-sitter query capturing every construct we care about in one pass. */
|
|
29
|
-
const QUERY_SRC = `
|
|
30
|
-
(function_declaration name: (identifier) @fn.name) @fn.def
|
|
31
|
-
(generator_function_declaration name: (identifier) @fn.name) @fn.def
|
|
32
|
-
(method_definition name: (property_identifier) @method.name) @method.def
|
|
33
|
-
(class_declaration name: (type_identifier) @class.name) @class.def
|
|
34
|
-
(interface_declaration name: (type_identifier) @iface.name) @iface.def
|
|
35
|
-
(type_alias_declaration name: (type_identifier) @type.name) @type.def
|
|
36
|
-
(variable_declarator
|
|
37
|
-
name: (identifier) @arrow.name
|
|
38
|
-
value: [(arrow_function) (function_expression)]) @arrow.def
|
|
39
|
-
(import_statement source: (string) @import.src)
|
|
40
|
-
(call_expression function: (identifier) @call.id)
|
|
41
|
-
(call_expression function: (member_expression property: (property_identifier) @call.member))
|
|
29
|
+
const QUERY_SRC = `
|
|
30
|
+
(function_declaration name: (identifier) @fn.name) @fn.def
|
|
31
|
+
(generator_function_declaration name: (identifier) @fn.name) @fn.def
|
|
32
|
+
(method_definition name: (property_identifier) @method.name) @method.def
|
|
33
|
+
(class_declaration name: (type_identifier) @class.name) @class.def
|
|
34
|
+
(interface_declaration name: (type_identifier) @iface.name) @iface.def
|
|
35
|
+
(type_alias_declaration name: (type_identifier) @type.name) @type.def
|
|
36
|
+
(variable_declarator
|
|
37
|
+
name: (identifier) @arrow.name
|
|
38
|
+
value: [(arrow_function) (function_expression)]) @arrow.def
|
|
39
|
+
(import_statement source: (string) @import.src)
|
|
40
|
+
(call_expression function: (identifier) @call.id)
|
|
41
|
+
(call_expression function: (member_expression property: (property_identifier) @call.member))
|
|
42
42
|
`;
|
|
43
43
|
const cache = new Map();
|
|
44
44
|
function bundleFor(lang, key) {
|
package/dist/store/hunchStore.js
CHANGED
|
@@ -30,6 +30,11 @@ export class HunchStore {
|
|
|
30
30
|
const tx = db.transaction(() => {
|
|
31
31
|
db.exec(RESET_SQL);
|
|
32
32
|
const j = (s) => s; // readability marker for JSON-encoded columns
|
|
33
|
+
// Prepare the FTS insert ONCE (after RESET created the table), not per row.
|
|
34
|
+
const insFts = db.prepare(`INSERT INTO search (ref, kind, title, body) VALUES (?,?,?,?)`);
|
|
35
|
+
const fts = (ref, kind, title, body) => {
|
|
36
|
+
insFts.run(ref, kind, title, body ?? "");
|
|
37
|
+
};
|
|
33
38
|
const comps = this.json.loadAll("components");
|
|
34
39
|
const insComp = db.prepare(`INSERT INTO components VALUES (@id,@kind,@name,@responsibility,@paths,@status,@owners,@fragility,@ps,@pc,@pe,@created_at,@updated_at)`);
|
|
35
40
|
for (const c of comps) {
|
|
@@ -39,7 +44,7 @@ export class HunchStore {
|
|
|
39
44
|
fragility: c.fragility, ps: c.provenance.source, pc: c.provenance.confidence,
|
|
40
45
|
pe: JSON.stringify(c.provenance.evidence), created_at: c.created_at, updated_at: c.updated_at,
|
|
41
46
|
});
|
|
42
|
-
|
|
47
|
+
fts(c.id, "components", c.name, `${c.responsibility} ${c.paths.join(" ")}`);
|
|
43
48
|
}
|
|
44
49
|
counts.components = comps.length;
|
|
45
50
|
const edges = this.json.loadAll("edges");
|
|
@@ -56,7 +61,7 @@ export class HunchStore {
|
|
|
56
61
|
calls: JSON.stringify(s.calls), called_by: JSON.stringify(s.called_by),
|
|
57
62
|
loc: s.metrics.loc, churn: s.metrics.churn_90d, bug: s.metrics.bug_count,
|
|
58
63
|
fanin: s.metrics.fan_in, fanout: s.metrics.fan_out, last: s.last_changed });
|
|
59
|
-
|
|
64
|
+
fts(s.id, "symbols", `${s.name} (${s.kind})`, s.file);
|
|
60
65
|
}
|
|
61
66
|
counts.symbols = syms.length;
|
|
62
67
|
const decs = this.json.loadAll("decisions");
|
|
@@ -67,7 +72,7 @@ export class HunchStore {
|
|
|
67
72
|
rc: JSON.stringify(d.related_components), rf: JSON.stringify(d.related_files),
|
|
68
73
|
sup: d.supersedes, cbb: d.caused_by_bug, commit: d.commit,
|
|
69
74
|
ps: d.provenance.source, pc: d.provenance.confidence, pe: JSON.stringify(d.provenance.evidence), date: d.date });
|
|
70
|
-
|
|
75
|
+
fts(d.id, "decisions", d.title, `${d.context} ${d.decision} ${d.consequences.join(" ")}`);
|
|
71
76
|
}
|
|
72
77
|
counts.decisions = decs.length;
|
|
73
78
|
const bugs = this.json.loadAll("bugs");
|
|
@@ -76,7 +81,7 @@ export class HunchStore {
|
|
|
76
81
|
insBug.run({ id: b.id, title: b.title, symptom: b.symptom, rc: b.root_cause, sev: b.severity, status: b.status,
|
|
77
82
|
af: JSON.stringify(b.affected_files), as: JSON.stringify(b.affected_symbols), lin: JSON.stringify(b.lineage),
|
|
78
83
|
ps: b.provenance.source, pc: b.provenance.confidence, pe: JSON.stringify(b.provenance.evidence) });
|
|
79
|
-
|
|
84
|
+
fts(b.id, "bugs", b.title, `${b.symptom} ${b.root_cause}`);
|
|
80
85
|
}
|
|
81
86
|
counts.bugs = bugs.length;
|
|
82
87
|
const cons = this.json.loadAll("constraints");
|
|
@@ -85,7 +90,7 @@ export class HunchStore {
|
|
|
85
90
|
insCon.run({ id: c.id, type: c.type, statement: c.statement, scope: JSON.stringify(c.scope),
|
|
86
91
|
sev: c.severity, enf: c.enforcement, rat: c.rationale, sd: c.source_decision, viol: JSON.stringify(c.violations),
|
|
87
92
|
ps: c.provenance.source, pc: c.provenance.confidence, pe: JSON.stringify(c.provenance.evidence) });
|
|
88
|
-
|
|
93
|
+
fts(c.id, "constraints", c.statement, `${c.rationale} ${c.scope.join(" ")}`);
|
|
89
94
|
}
|
|
90
95
|
counts.constraints = cons.length;
|
|
91
96
|
void j;
|
|
@@ -97,9 +102,6 @@ export class HunchStore {
|
|
|
97
102
|
this.pruneStaleEmbeddings();
|
|
98
103
|
return { counts };
|
|
99
104
|
}
|
|
100
|
-
fts(db, ref, kind, title, body) {
|
|
101
|
-
db.prepare(`INSERT INTO search (ref, kind, title, body) VALUES (?,?,?,?)`).run(ref, kind, title, body ?? "");
|
|
102
|
-
}
|
|
103
105
|
// ---- read path ----------------------------------------------------------
|
|
104
106
|
/** FTS5 ranked search (hunch_query). Falls back to LIKE if the query has no
|
|
105
107
|
* FTS-tokenizable terms. */
|
|
@@ -305,7 +307,7 @@ export class HunchStore {
|
|
|
305
307
|
* `id`, via a recursive CTE over the edges graph (hunch_get_dependents). We
|
|
306
308
|
* walk edges BACKWARD (edges.to = current) following call/dep/import/contains. */
|
|
307
309
|
getDependents(id, maxDepth = 6) {
|
|
308
|
-
|
|
310
|
+
return this.db.prepare(
|
|
309
311
|
/* sql */ `
|
|
310
312
|
WITH RECURSIVE up(node, depth) AS (
|
|
311
313
|
SELECT ?, 0
|
|
@@ -314,13 +316,16 @@ export class HunchStore {
|
|
|
314
316
|
FROM edges e JOIN up ON e."to" = up.node
|
|
315
317
|
WHERE e.type IN ('calls','depends_on','imports','contains') AND up.depth < ?
|
|
316
318
|
)
|
|
317
|
-
SELECT
|
|
319
|
+
SELECT up.node AS id, MIN(up.depth) AS depth,
|
|
320
|
+
COALESCE(s.name || ' @ ' || s.file, c.name, up.node) AS via
|
|
321
|
+
FROM up
|
|
322
|
+
LEFT JOIN symbols s ON s.id = up.node
|
|
323
|
+
LEFT JOIN components c ON c.id = up.node
|
|
318
324
|
WHERE up.node <> ? GROUP BY up.node ORDER BY depth, id`).all(id, maxDepth, id);
|
|
319
|
-
return rows.map((r) => ({ id: r.id, depth: r.depth, via: this.labelFor(r.id) }));
|
|
320
325
|
}
|
|
321
326
|
/** Symbols/components this id depends ON (forward walk) — used for refactor blast radius. */
|
|
322
327
|
getDependencies(id, maxDepth = 6) {
|
|
323
|
-
|
|
328
|
+
return this.db.prepare(
|
|
324
329
|
/* sql */ `
|
|
325
330
|
WITH RECURSIVE down(node, depth) AS (
|
|
326
331
|
SELECT ?, 0
|
|
@@ -329,40 +334,34 @@ export class HunchStore {
|
|
|
329
334
|
FROM edges e JOIN down ON e."from" = down.node
|
|
330
335
|
WHERE e.type IN ('calls','depends_on','imports','contains') AND down.depth < ?
|
|
331
336
|
)
|
|
332
|
-
SELECT
|
|
337
|
+
SELECT down.node AS id, MIN(down.depth) AS depth,
|
|
338
|
+
COALESCE(s.name || ' @ ' || s.file, c.name, down.node) AS via
|
|
339
|
+
FROM down
|
|
340
|
+
LEFT JOIN symbols s ON s.id = down.node
|
|
341
|
+
LEFT JOIN components c ON c.id = down.node
|
|
333
342
|
WHERE down.node <> ? GROUP BY down.node ORDER BY depth, id`).all(id, maxDepth, id);
|
|
334
|
-
return rows.map((r) => ({ id: r.id, depth: r.depth, via: this.labelFor(r.id) }));
|
|
335
|
-
}
|
|
336
|
-
labelFor(id) {
|
|
337
|
-
if (id.startsWith("sym_")) {
|
|
338
|
-
const r = this.db.prepare(`SELECT name, file FROM symbols WHERE id=?`).get(id);
|
|
339
|
-
return r ? `${r.name} @ ${r.file}` : id;
|
|
340
|
-
}
|
|
341
|
-
if (id.startsWith("cmp_")) {
|
|
342
|
-
const r = this.db.prepare(`SELECT name FROM components WHERE id=?`).get(id);
|
|
343
|
-
return r ? r.name : id;
|
|
344
|
-
}
|
|
345
|
-
return id;
|
|
346
343
|
}
|
|
347
344
|
/** Files whose symbols (in)directly DEPEND ON a symbol defined in `file` — the
|
|
348
345
|
* blast radius of editing `file`, collapsed to file granularity (nearest depth
|
|
349
346
|
* wins per file). Powers `hunch check` near-violation detection and `--blast`. */
|
|
350
347
|
blastRadiusFiles(file, maxDepth = 4) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
348
|
+
// ONE backward CTE seeded by every symbol in `file`, joined to symbols for the
|
|
349
|
+
// dependent file/name. GROUP BY file + MIN(depth): SQLite's single-min rule makes
|
|
350
|
+
// the bare `via` take the name from the nearest-depth row. The inner JOIN drops
|
|
351
|
+
// non-symbol nodes; `s.file <> ?` drops self-file dependents.
|
|
352
|
+
return this.db.prepare(
|
|
353
|
+
/* sql */ `
|
|
354
|
+
WITH RECURSIVE up(node, depth) AS (
|
|
355
|
+
SELECT id, 0 FROM symbols WHERE file = ?
|
|
356
|
+
UNION
|
|
357
|
+
SELECT e."from", up.depth + 1
|
|
358
|
+
FROM edges e JOIN up ON e."to" = up.node
|
|
359
|
+
WHERE e.type IN ('calls','depends_on','imports','contains') AND up.depth < ?
|
|
360
|
+
)
|
|
361
|
+
SELECT s.file AS file, s.name AS via, MIN(up.depth) AS depth
|
|
362
|
+
FROM up JOIN symbols s ON s.id = up.node
|
|
363
|
+
WHERE up.depth > 0 AND s.file <> ?
|
|
364
|
+
GROUP BY s.file ORDER BY depth, file`).all(file, maxDepth, file);
|
|
366
365
|
}
|
|
367
366
|
/** Constraints whose scope glob matches a path/glob (hunch_check_constraints).
|
|
368
367
|
* By default only ACTIVE invariants are returned — a retired constraint is no
|
package/dist/store/jsonStore.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* in PRs, diffable, mergeable). This layer never touches SQLite — it is the
|
|
4
4
|
* authoritative read/write surface; SQLite is rebuilt from it.
|
|
5
5
|
*/
|
|
6
|
-
import { mkdirSync, readdirSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
6
|
+
import { mkdirSync, readdirSync, readFileSync, existsSync, rmSync, statSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { ENTITY_KINDS, SCHEMAS } from "../core/types.js";
|
|
9
9
|
import { migrateRaw, readManifest, writeManifest, SCHEMA_VERSION } from "../core/migrate.js";
|
|
@@ -17,9 +17,27 @@ const encode = (v) => JSON.stringify(v, null, 2) + "\n";
|
|
|
17
17
|
export class JsonStore {
|
|
18
18
|
paths;
|
|
19
19
|
_warnedForward = false;
|
|
20
|
+
/** Memoized validated records per kind. loadAll() is on every read-path method
|
|
21
|
+
* (why/checkConstraints/fragility/assembleContext…), each of which previously
|
|
22
|
+
* re-read the directory and re-ran Zod over every record. Cache keyed by the
|
|
23
|
+
* kind dir's mtime: every write goes through writeFileAtomic (temp + rename) or
|
|
24
|
+
* rmSync, both of which bump the dir mtime — so an OUT-OF-BAND write by another
|
|
25
|
+
* process self-invalidates on the next read. In-process writes also invalidate
|
|
26
|
+
* explicitly (exact, independent of mtime granularity). */
|
|
27
|
+
cache = new Map();
|
|
20
28
|
constructor(paths) {
|
|
21
29
|
this.paths = paths;
|
|
22
30
|
}
|
|
31
|
+
/** Drop memoized loadAll results. Writes through this store invalidate the
|
|
32
|
+
* affected kind automatically; call this for OUT-OF-BAND changes to .hunch/
|
|
33
|
+
* JSON (e.g. the long-lived MCP server reacting to a file-watch, or after an
|
|
34
|
+
* external `hunch migrate`). */
|
|
35
|
+
clearCache() {
|
|
36
|
+
this.cache.clear();
|
|
37
|
+
}
|
|
38
|
+
invalidate(kind) {
|
|
39
|
+
this.cache.delete(kind);
|
|
40
|
+
}
|
|
23
41
|
/** Create .hunch/<kind>/ directories. Stamp the manifest at the CURRENT version
|
|
24
42
|
* only when scaffolding a FRESH .hunch/ (so `hunch index`/`sync` on a brand-new
|
|
25
43
|
* repo records the version too, not just `init`). A pre-existing .hunch/ without
|
|
@@ -56,9 +74,22 @@ export class JsonStore {
|
|
|
56
74
|
return join(this.paths.dir(kind), single);
|
|
57
75
|
return join(this.paths.dir(kind), `${id}.json`);
|
|
58
76
|
}
|
|
59
|
-
/** Load every record of a kind, validated against its schema.
|
|
60
|
-
*
|
|
77
|
+
/** Load every record of a kind, validated against its schema. Memoized — the
|
|
78
|
+
* returned array is shared and MUST be treated read-only (every caller already
|
|
79
|
+
* derives via filter/map/sort, which copy). Invalidated on write. */
|
|
61
80
|
loadAll(kind) {
|
|
81
|
+
const dir = this.paths.dir(kind);
|
|
82
|
+
const mtimeMs = existsSync(dir) ? statSync(dir).mtimeMs : 0;
|
|
83
|
+
const hit = this.cache.get(kind);
|
|
84
|
+
if (hit && hit.mtimeMs === mtimeMs)
|
|
85
|
+
return hit.data;
|
|
86
|
+
const data = this.readAllFromDisk(kind);
|
|
87
|
+
this.cache.set(kind, { mtimeMs, data });
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
/** Uncached disk read + validate. Invalid records are skipped with a warning
|
|
91
|
+
* rather than crashing the whole load. */
|
|
92
|
+
readAllFromDisk(kind) {
|
|
62
93
|
const dir = this.paths.dir(kind);
|
|
63
94
|
if (!existsSync(dir))
|
|
64
95
|
return [];
|
|
@@ -126,12 +157,14 @@ export class JsonStore {
|
|
|
126
157
|
else {
|
|
127
158
|
writeFileAtomic(this.fileFor(kind, validated.id), encode(validated));
|
|
128
159
|
}
|
|
160
|
+
this.invalidate(kind);
|
|
129
161
|
return validated;
|
|
130
162
|
}
|
|
131
163
|
/** Bulk replace all records of a kind (used by the extractor for symbols/edges). */
|
|
132
164
|
replaceAll(kind, records) {
|
|
133
165
|
const schema = SCHEMAS[kind];
|
|
134
166
|
const validated = records.map((r) => schema.parse(r));
|
|
167
|
+
this.invalidate(kind);
|
|
135
168
|
mkdirSync(this.paths.dir(kind), { recursive: true });
|
|
136
169
|
const single = SINGLE_FILE[kind];
|
|
137
170
|
if (single) {
|
|
@@ -186,12 +219,14 @@ export class JsonStore {
|
|
|
186
219
|
if (next.length === arr.length)
|
|
187
220
|
return false;
|
|
188
221
|
writeFileAtomic(f, encode(next));
|
|
222
|
+
this.invalidate(kind);
|
|
189
223
|
return true;
|
|
190
224
|
}
|
|
191
225
|
const f = this.fileFor(kind, id);
|
|
192
226
|
if (!existsSync(f))
|
|
193
227
|
return false;
|
|
194
228
|
rmSync(f);
|
|
229
|
+
this.invalidate(kind);
|
|
195
230
|
return true;
|
|
196
231
|
}
|
|
197
232
|
/** Persist a schema migration: rewrite every LOADABLE record in its current shape.
|
|
@@ -201,6 +236,7 @@ export class JsonStore {
|
|
|
201
236
|
persistMigration() {
|
|
202
237
|
let migrated = 0;
|
|
203
238
|
let skipped = 0;
|
|
239
|
+
this.cache.clear(); // every record is rewritten; drop all memoized loads
|
|
204
240
|
const version = this.schemaVersion(); // read once; we're migrating FROM this
|
|
205
241
|
for (const kind of ENTITY_KINDS) {
|
|
206
242
|
const dir = this.paths.dir(kind);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
6
6
|
"description": "Hunch — an Engineering Memory OS: a persistent, git-native reasoning graph over a codebase, exposed to Claude Code via MCP.",
|
|
7
|
-
"homepage": "https://hunch.
|
|
7
|
+
"homepage": "https://hunch-pi.vercel.app",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/davesheffer/hunch.git"
|