clawvault 3.0.0 → 3.2.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/README.md +352 -20
- package/bin/clawvault.js +8 -2
- package/bin/command-registration.test.js +3 -1
- package/bin/command-runtime.js +9 -1
- package/bin/register-core-commands.js +23 -10
- package/bin/register-maintenance-commands.js +39 -3
- package/bin/register-query-commands.js +58 -29
- package/bin/register-task-commands.js +18 -1
- package/bin/register-task-commands.test.js +16 -0
- package/bin/register-vault-operations-commands.js +29 -1
- package/bin/register-workgraph-commands.js +1368 -0
- package/dashboard/lib/graph-diff.js +104 -0
- package/dashboard/lib/graph-diff.test.js +75 -0
- package/dashboard/lib/vault-parser.js +556 -0
- package/dashboard/lib/vault-parser.test.js +254 -0
- package/dashboard/public/app.js +796 -0
- package/dashboard/public/index.html +52 -0
- package/dashboard/public/styles.css +221 -0
- package/dashboard/server.js +374 -0
- package/dist/{chunk-F2JEUD4J.js → chunk-23YDQ3QU.js} +6 -8
- package/dist/{chunk-C7OK5WKP.js → chunk-2JQ3O2YL.js} +4 -4
- package/dist/{chunk-VR5NE7PZ.js → chunk-2RAZ4ZFE.js} +1 -1
- package/dist/chunk-2ZDO52B4.js +52 -0
- package/dist/{chunk-ZZA73MFY.js → chunk-33DOSHTA.js} +176 -36
- package/dist/chunk-33VSQP4J.js +37 -0
- package/dist/chunk-4BQTQMJP.js +93 -0
- package/dist/{chunk-GUKMRGM7.js → chunk-4OXMU5S2.js} +1 -1
- package/dist/{chunk-62YTUT6J.js → chunk-4PY655YM.js} +15 -3
- package/dist/chunk-6FH3IULF.js +352 -0
- package/dist/{chunk-3NSBOUT3.js → chunk-77Q5CSPJ.js} +404 -80
- package/dist/{chunk-4VQTUVH7.js → chunk-7YZWHM36.js} +52 -26
- package/dist/chunk-BSJ6RIT7.js +447 -0
- package/dist/chunk-BUEW6IIK.js +364 -0
- package/dist/{chunk-WGRQ6HDV.js → chunk-CLJTREDS.js} +74 -14
- package/dist/chunk-EK6S23ZB.js +469 -0
- package/dist/{chunk-LNJA2UGL.js → chunk-ESFLMDRB.js} +9 -86
- package/dist/{chunk-H34S76MB.js → chunk-ESVS6K2B.js} +6 -6
- package/dist/{chunk-WAZ3NLWL.js → chunk-F55HGNU4.js} +0 -47
- package/dist/{chunk-QK3UCXWL.js → chunk-FHFUXL6G.js} +2 -2
- package/dist/{chunk-YKTA5JOJ.js → chunk-GAOWA7GR.js} +212 -46
- package/dist/chunk-GGA32J2R.js +784 -0
- package/dist/chunk-GNJL4YGR.js +79 -0
- package/dist/chunk-MDIH26GC.js +183 -0
- package/dist/{chunk-LYHGEHXG.js → chunk-MFAWT5O5.js} +0 -1
- package/dist/chunk-MM6QGW3P.js +207 -0
- package/dist/{chunk-P5EPF6MB.js → chunk-MW5C6ZQA.js} +110 -13
- package/dist/chunk-NCKFNBHJ.js +257 -0
- package/dist/{chunk-QBLMXKF2.js → chunk-OIWVQYQF.js} +1 -1
- package/dist/{chunk-42MXU7A6.js → chunk-P62WHA27.js} +58 -47
- package/dist/chunk-PBACDKKP.js +66 -0
- package/dist/{chunk-VGLOTGAS.js → chunk-QSHD36LH.js} +2 -2
- package/dist/{chunk-OZ7RIXTO.js → chunk-QSRRMEYM.js} +2 -2
- package/dist/chunk-QVEERJSP.js +152 -0
- package/dist/{chunk-N2AXRYLC.js → chunk-QWQ3TIKS.js} +1 -1
- package/dist/{chunk-3DHXQHYG.js → chunk-R2MIW5G7.js} +1 -1
- package/dist/{chunk-SJSFRIYS.js → chunk-SLXOR3CC.js} +2 -2
- package/dist/chunk-SS4B7P7V.js +99 -0
- package/dist/{chunk-JY6FYXIT.js → chunk-STCQGCEQ.js} +6 -11
- package/dist/chunk-U4O6C46S.js +154 -0
- package/dist/{chunk-ITPEXLHA.js → chunk-URXDAUVH.js} +24 -5
- package/dist/chunk-VSL7KY3M.js +189 -0
- package/dist/{chunk-U55BGUAU.js → chunk-W4SPAEE7.js} +6 -6
- package/dist/chunk-WMGIIABP.js +15 -0
- package/dist/{chunk-3D6BCTP6.js → chunk-X3SPPUFG.js} +51 -39
- package/dist/{chunk-THRJVD4L.js → chunk-Y6VJKXGL.js} +1 -1
- package/dist/{chunk-ZVVFWOLW.js → chunk-ZN54U2OZ.js} +123 -10
- package/dist/cli/index.js +32 -25
- package/dist/commands/archive.js +3 -3
- package/dist/commands/backlog.js +3 -3
- package/dist/commands/blocked.js +3 -3
- package/dist/commands/canvas.d.ts +15 -0
- package/dist/commands/canvas.js +200 -0
- package/dist/commands/checkpoint.js +2 -2
- package/dist/commands/compat.js +2 -2
- package/dist/commands/context.js +8 -6
- package/dist/commands/doctor.d.ts +11 -7
- package/dist/commands/doctor.js +18 -16
- package/dist/commands/embed.js +5 -6
- package/dist/commands/entities.js +2 -2
- package/dist/commands/graph.js +4 -4
- package/dist/commands/inject.d.ts +1 -1
- package/dist/commands/inject.js +5 -6
- package/dist/commands/kanban.js +4 -4
- package/dist/commands/link.js +5 -5
- package/dist/commands/migrate-observations.js +4 -4
- package/dist/commands/observe.d.ts +0 -1
- package/dist/commands/observe.js +14 -13
- package/dist/commands/project.js +5 -5
- package/dist/commands/rebuild-embeddings.d.ts +21 -0
- package/dist/commands/rebuild-embeddings.js +91 -0
- package/dist/commands/rebuild.js +12 -11
- package/dist/commands/recover.js +3 -3
- package/dist/commands/reflect.js +6 -7
- package/dist/commands/repair-session.js +1 -1
- package/dist/commands/replay.js +14 -14
- package/dist/commands/session-recap.js +1 -1
- package/dist/commands/setup.d.ts +2 -90
- package/dist/commands/setup.js +3 -21
- package/dist/commands/shell-init.js +1 -1
- package/dist/commands/sleep.d.ts +1 -1
- package/dist/commands/sleep.js +20 -19
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +57 -35
- package/dist/commands/sync-bd.d.ts +10 -0
- package/dist/commands/sync-bd.js +10 -0
- package/dist/commands/tailscale.js +3 -3
- package/dist/commands/task.js +4 -4
- package/dist/commands/template.js +2 -2
- package/dist/commands/wake.d.ts +1 -1
- package/dist/commands/wake.js +11 -10
- package/dist/commands/workgraph.d.ts +124 -0
- package/dist/commands/workgraph.js +38 -0
- package/dist/index.d.ts +337 -191
- package/dist/index.js +387 -118
- package/dist/{inject-Bzi5E-By.d.cts → inject-DYUrDqQO.d.ts} +3 -3
- package/dist/ledger-B7g7jhqG.d.ts +44 -0
- package/dist/lib/auto-linker.js +2 -2
- package/dist/lib/canvas-layout.d.ts +100 -16
- package/dist/lib/canvas-layout.js +21 -78
- package/dist/lib/config.d.ts +27 -3
- package/dist/lib/config.js +4 -2
- package/dist/lib/entity-index.js +1 -1
- package/dist/lib/project-utils.js +4 -4
- package/dist/lib/session-repair.js +1 -1
- package/dist/lib/session-utils.js +1 -1
- package/dist/lib/tailscale.js +1 -1
- package/dist/lib/task-utils.js +3 -3
- package/dist/lib/template-engine.js +1 -1
- package/dist/lib/webdav.js +1 -1
- package/dist/onnxruntime_binding-5QEF3SUC.node +0 -0
- package/dist/onnxruntime_binding-BKPKNEGC.node +0 -0
- package/dist/onnxruntime_binding-FMOXGIUT.node +0 -0
- package/dist/onnxruntime_binding-OI2KMXC5.node +0 -0
- package/dist/onnxruntime_binding-UX44MLAZ.node +0 -0
- package/dist/onnxruntime_binding-Y2W7N7WY.node +0 -0
- package/dist/openclaw-plugin.d.ts +8 -0
- package/dist/openclaw-plugin.js +14 -0
- package/dist/registry-BR4326o0.d.ts +30 -0
- package/dist/store-CA-6sKCJ.d.ts +34 -0
- package/dist/thread-B9LhXNU0.d.ts +41 -0
- package/dist/transformers.node-A2ZRORSQ.js +46775 -0
- package/dist/{types-Y2_Um2Ls.d.cts → types-BbWJoC1c.d.ts} +1 -44
- package/dist/workgraph/index.d.ts +5 -0
- package/dist/workgraph/index.js +23 -0
- package/dist/workgraph/ledger.d.ts +2 -0
- package/dist/workgraph/ledger.js +25 -0
- package/dist/workgraph/registry.d.ts +2 -0
- package/dist/workgraph/registry.js +19 -0
- package/dist/workgraph/store.d.ts +2 -0
- package/dist/workgraph/store.js +25 -0
- package/dist/workgraph/thread.d.ts +2 -0
- package/dist/workgraph/thread.js +25 -0
- package/dist/workgraph/types.d.ts +54 -0
- package/dist/workgraph/types.js +7 -0
- package/hooks/clawvault/HOOK.md +34 -4
- package/hooks/clawvault/handler.js +760 -78
- package/hooks/clawvault/handler.test.js +235 -79
- package/hooks/clawvault/openclaw.plugin.json +72 -0
- package/openclaw.plugin.json +65 -38
- package/package.json +15 -18
- package/dist/chunk-3RG5ZIWI.js +0 -10
- package/dist/chunk-6U6MK36V.js +0 -205
- package/dist/chunk-7R7O6STJ.js +0 -88
- package/dist/chunk-CMB7UL7C.js +0 -327
- package/dist/chunk-DEFFDRVP.js +0 -938
- package/dist/chunk-E7MFQB6D.js +0 -163
- package/dist/chunk-GAJV4IGR.js +0 -82
- package/dist/chunk-GQSLDZTS.js +0 -560
- package/dist/chunk-K234IDRJ.js +0 -1073
- package/dist/chunk-MFM6K7PU.js +0 -374
- package/dist/chunk-MXSSG3QU.js +0 -42
- package/dist/chunk-PAH27GSN.js +0 -108
- package/dist/cli/index.cjs +0 -10033
- package/dist/cli/index.d.cts +0 -5
- package/dist/commands/archive.cjs +0 -287
- package/dist/commands/archive.d.cts +0 -11
- package/dist/commands/backlog.cjs +0 -721
- package/dist/commands/backlog.d.cts +0 -53
- package/dist/commands/blocked.cjs +0 -204
- package/dist/commands/blocked.d.cts +0 -26
- package/dist/commands/checkpoint.cjs +0 -244
- package/dist/commands/checkpoint.d.cts +0 -41
- package/dist/commands/compat.cjs +0 -369
- package/dist/commands/compat.d.cts +0 -28
- package/dist/commands/context.cjs +0 -2989
- package/dist/commands/context.d.cts +0 -2
- package/dist/commands/doctor.cjs +0 -3062
- package/dist/commands/doctor.d.cts +0 -21
- package/dist/commands/embed.cjs +0 -232
- package/dist/commands/embed.d.cts +0 -17
- package/dist/commands/entities.cjs +0 -141
- package/dist/commands/entities.d.cts +0 -7
- package/dist/commands/graph.cjs +0 -501
- package/dist/commands/graph.d.cts +0 -21
- package/dist/commands/inject.cjs +0 -1636
- package/dist/commands/inject.d.cts +0 -2
- package/dist/commands/kanban.cjs +0 -884
- package/dist/commands/kanban.d.cts +0 -63
- package/dist/commands/link.cjs +0 -965
- package/dist/commands/link.d.cts +0 -11
- package/dist/commands/migrate-observations.cjs +0 -362
- package/dist/commands/migrate-observations.d.cts +0 -19
- package/dist/commands/observe.cjs +0 -4099
- package/dist/commands/observe.d.cts +0 -23
- package/dist/commands/project.cjs +0 -1341
- package/dist/commands/project.d.cts +0 -85
- package/dist/commands/rebuild.cjs +0 -3136
- package/dist/commands/rebuild.d.cts +0 -11
- package/dist/commands/recover.cjs +0 -361
- package/dist/commands/recover.d.cts +0 -38
- package/dist/commands/reflect.cjs +0 -1008
- package/dist/commands/reflect.d.cts +0 -11
- package/dist/commands/repair-session.cjs +0 -457
- package/dist/commands/repair-session.d.cts +0 -38
- package/dist/commands/replay.cjs +0 -4103
- package/dist/commands/replay.d.cts +0 -16
- package/dist/commands/session-recap.cjs +0 -353
- package/dist/commands/session-recap.d.cts +0 -27
- package/dist/commands/setup.cjs +0 -1345
- package/dist/commands/setup.d.cts +0 -100
- package/dist/commands/shell-init.cjs +0 -75
- package/dist/commands/shell-init.d.cts +0 -7
- package/dist/commands/sleep.cjs +0 -6028
- package/dist/commands/sleep.d.cts +0 -36
- package/dist/commands/status.cjs +0 -2736
- package/dist/commands/status.d.cts +0 -52
- package/dist/commands/tailscale.cjs +0 -1532
- package/dist/commands/tailscale.d.cts +0 -52
- package/dist/commands/task.cjs +0 -1236
- package/dist/commands/task.d.cts +0 -97
- package/dist/commands/template.cjs +0 -457
- package/dist/commands/template.d.cts +0 -36
- package/dist/commands/wake.cjs +0 -2626
- package/dist/commands/wake.d.cts +0 -22
- package/dist/context-BUGaWpyL.d.cts +0 -46
- package/dist/index.cjs +0 -14526
- package/dist/index.d.cts +0 -858
- package/dist/inject-Bzi5E-By.d.ts +0 -137
- package/dist/lib/auto-linker.cjs +0 -176
- package/dist/lib/auto-linker.d.cts +0 -26
- package/dist/lib/canvas-layout.cjs +0 -136
- package/dist/lib/canvas-layout.d.cts +0 -31
- package/dist/lib/config.cjs +0 -78
- package/dist/lib/config.d.cts +0 -11
- package/dist/lib/entity-index.cjs +0 -84
- package/dist/lib/entity-index.d.cts +0 -26
- package/dist/lib/project-utils.cjs +0 -864
- package/dist/lib/project-utils.d.cts +0 -97
- package/dist/lib/session-repair.cjs +0 -239
- package/dist/lib/session-repair.d.cts +0 -110
- package/dist/lib/session-utils.cjs +0 -209
- package/dist/lib/session-utils.d.cts +0 -63
- package/dist/lib/tailscale.cjs +0 -1183
- package/dist/lib/tailscale.d.cts +0 -225
- package/dist/lib/task-utils.cjs +0 -1137
- package/dist/lib/task-utils.d.cts +0 -208
- package/dist/lib/template-engine.cjs +0 -47
- package/dist/lib/template-engine.d.cts +0 -11
- package/dist/lib/webdav.cjs +0 -568
- package/dist/lib/webdav.d.cts +0 -109
- package/dist/plugin/index.cjs +0 -1907
- package/dist/plugin/index.d.cts +0 -36
- package/dist/plugin/index.d.ts +0 -36
- package/dist/plugin/index.js +0 -572
- package/dist/plugin/inject.cjs +0 -356
- package/dist/plugin/inject.d.cts +0 -54
- package/dist/plugin/inject.d.ts +0 -54
- package/dist/plugin/inject.js +0 -17
- package/dist/plugin/observe.cjs +0 -631
- package/dist/plugin/observe.d.cts +0 -39
- package/dist/plugin/observe.d.ts +0 -39
- package/dist/plugin/observe.js +0 -18
- package/dist/plugin/templates.cjs +0 -593
- package/dist/plugin/templates.d.cts +0 -52
- package/dist/plugin/templates.d.ts +0 -52
- package/dist/plugin/templates.js +0 -25
- package/dist/plugin/types.cjs +0 -18
- package/dist/plugin/types.d.cts +0 -209
- package/dist/plugin/types.d.ts +0 -209
- package/dist/plugin/types.js +0 -0
- package/dist/plugin/vault.cjs +0 -927
- package/dist/plugin/vault.d.cts +0 -68
- package/dist/plugin/vault.d.ts +0 -68
- package/dist/plugin/vault.js +0 -22
- package/dist/types-Y2_Um2Ls.d.ts +0 -205
- package/templates/memory-event.md +0 -67
- package/templates/party.md +0 -63
- package/templates/primitive-registry.yaml +0 -551
- package/templates/run.md +0 -68
- package/templates/trigger.md +0 -68
- package/templates/workspace.md +0 -50
|
@@ -1,2989 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/commands/context.ts
|
|
31
|
-
var context_exports = {};
|
|
32
|
-
__export(context_exports, {
|
|
33
|
-
buildContext: () => buildContext,
|
|
34
|
-
contextCommand: () => contextCommand,
|
|
35
|
-
formatContextMarkdown: () => formatContextMarkdown,
|
|
36
|
-
registerContextCommand: () => registerContextCommand
|
|
37
|
-
});
|
|
38
|
-
module.exports = __toCommonJS(context_exports);
|
|
39
|
-
var path5 = __toESM(require("path"), 1);
|
|
40
|
-
|
|
41
|
-
// src/lib/vault.ts
|
|
42
|
-
var fs4 = __toESM(require("fs"), 1);
|
|
43
|
-
var path4 = __toESM(require("path"), 1);
|
|
44
|
-
var import_url = require("url");
|
|
45
|
-
var import_gray_matter2 = __toESM(require("gray-matter"), 1);
|
|
46
|
-
var import_glob2 = require("glob");
|
|
47
|
-
|
|
48
|
-
// src/types.ts
|
|
49
|
-
var DEFAULT_CATEGORIES = [
|
|
50
|
-
"rules",
|
|
51
|
-
"preferences",
|
|
52
|
-
"decisions",
|
|
53
|
-
"patterns",
|
|
54
|
-
"people",
|
|
55
|
-
"projects",
|
|
56
|
-
"goals",
|
|
57
|
-
"transcripts",
|
|
58
|
-
"inbox",
|
|
59
|
-
"templates",
|
|
60
|
-
"lessons",
|
|
61
|
-
"agents",
|
|
62
|
-
"commitments",
|
|
63
|
-
"handoffs",
|
|
64
|
-
"research",
|
|
65
|
-
"tasks",
|
|
66
|
-
"backlog"
|
|
67
|
-
];
|
|
68
|
-
var TYPE_TO_CATEGORY = {
|
|
69
|
-
fact: "facts",
|
|
70
|
-
feeling: "feelings",
|
|
71
|
-
decision: "decisions",
|
|
72
|
-
lesson: "lessons",
|
|
73
|
-
commitment: "commitments",
|
|
74
|
-
preference: "preferences",
|
|
75
|
-
relationship: "people",
|
|
76
|
-
project: "projects"
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// src/lib/search.ts
|
|
80
|
-
var import_child_process = require("child_process");
|
|
81
|
-
var fs2 = __toESM(require("fs"), 1);
|
|
82
|
-
var path2 = __toESM(require("path"), 1);
|
|
83
|
-
|
|
84
|
-
// src/lib/observation-format.ts
|
|
85
|
-
var DATE_HEADING_RE = /^##\s+(\d{4}-\d{2}-\d{2})\s*$/;
|
|
86
|
-
var SCORED_LINE_RE = /^(?:-\s*)?\[(decision|preference|fact|commitment|task|todo|commitment-unresolved|milestone|lesson|relationship|project)\|c=(0(?:\.\d+)?|1(?:\.0+)?)\|i=(0(?:\.\d+)?|1(?:\.0+)?)\]\s+(.+)$/i;
|
|
87
|
-
var EMOJI_LINE_RE = /^(?:-\s*)?(🔴|🟡|🟢)\s+(\d{2}:\d{2})?\s*(.+)$/u;
|
|
88
|
-
var DECISION_RE = /\b(decis(?:ion|ions)?|decid(?:e|ed|ing)|chose|selected|opted|went with|picked)\b/i;
|
|
89
|
-
var PREFERENCE_RE = /\b(prefer(?:ence|s|red)?|likes?|dislikes?|default to|always use|never use|enjoys?|loves?|favou?rite|fan of|interested in|go-to|tend(?:s)? to use|passionate about|hobby|hobbies|(?:I|my|our)\s+(?:own|have|use|got|bought|drive|wear|eat|drink|cook|play|watch|read|listen)|(?:I(?:'m| am))\s+(?:a |an |into |allergic|vegetarian|vegan|gluten|lactose|trying to|learning)|usually|every (?:morning|evening|night|day|week)|routine)\b/i;
|
|
90
|
-
var COMMITMENT_RE = /\b(commit(?:ment|ted)?|promised|deadline|due|scheduled|will deliver|agreed to)\b/i;
|
|
91
|
-
var TODO_RE = /(?:\btodo:\s*|\bwe need to\b|\bdon't forget(?: to)?\b|\bremember to\b|\bmake sure to\b)/i;
|
|
92
|
-
var COMMITMENT_TASK_RE = /\b(?:i'?ll|i will|let me|(?:i'?m\s+)?going to|plan to|should)\b/i;
|
|
93
|
-
var UNRESOLVED_RE = /\b(?:need to figure out|tbd|to be determined)\b/i;
|
|
94
|
-
var DEADLINE_RE = /\b(?:by\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow)|before\s+the\s+\w+|deadline is)\b/i;
|
|
95
|
-
var MILESTONE_RE = /\b(released?|shipped|launched|merged|published|milestone|v\d+\.\d+)\b/i;
|
|
96
|
-
var LESSON_RE = /\b(learn(?:ed|ing|t)|lesson|insight|realized|discovered|never again)\b/i;
|
|
97
|
-
var RELATIONSHIP_RE = /\b(talked to|met with|spoke with|asked|client|partner|teammate|colleague)\b/i;
|
|
98
|
-
var PROJECT_RE = /\b(project|feature|service|repo|api|roadmap|sprint)\b/i;
|
|
99
|
-
function clamp01(value) {
|
|
100
|
-
if (!Number.isFinite(value)) return 0;
|
|
101
|
-
if (value < 0) return 0;
|
|
102
|
-
if (value > 1) return 1;
|
|
103
|
-
return value;
|
|
104
|
-
}
|
|
105
|
-
function scoreFromLegacyPriority(priority) {
|
|
106
|
-
if (priority === "\u{1F534}") return 0.9;
|
|
107
|
-
if (priority === "\u{1F7E1}") return 0.6;
|
|
108
|
-
return 0.2;
|
|
109
|
-
}
|
|
110
|
-
function confidenceFromLegacyPriority(priority) {
|
|
111
|
-
if (priority === "\u{1F534}") return 0.9;
|
|
112
|
-
if (priority === "\u{1F7E1}") return 0.8;
|
|
113
|
-
return 0.7;
|
|
114
|
-
}
|
|
115
|
-
function inferObservationType(content) {
|
|
116
|
-
if (DECISION_RE.test(content)) return "decision";
|
|
117
|
-
if (UNRESOLVED_RE.test(content)) return "commitment-unresolved";
|
|
118
|
-
if (TODO_RE.test(content)) return "todo";
|
|
119
|
-
if (PREFERENCE_RE.test(content)) return "preference";
|
|
120
|
-
if (COMMITMENT_TASK_RE.test(content) || DEADLINE_RE.test(content)) return "task";
|
|
121
|
-
if (COMMITMENT_RE.test(content)) return "commitment";
|
|
122
|
-
if (MILESTONE_RE.test(content)) return "milestone";
|
|
123
|
-
if (LESSON_RE.test(content)) return "lesson";
|
|
124
|
-
if (RELATIONSHIP_RE.test(content)) return "relationship";
|
|
125
|
-
if (PROJECT_RE.test(content)) return "project";
|
|
126
|
-
return "fact";
|
|
127
|
-
}
|
|
128
|
-
function parseObservationLine(line, date) {
|
|
129
|
-
const scored = line.match(SCORED_LINE_RE);
|
|
130
|
-
if (scored) {
|
|
131
|
-
return {
|
|
132
|
-
date,
|
|
133
|
-
type: scored[1].toLowerCase(),
|
|
134
|
-
confidence: clamp01(Number.parseFloat(scored[2])),
|
|
135
|
-
importance: clamp01(Number.parseFloat(scored[3])),
|
|
136
|
-
content: scored[4].trim(),
|
|
137
|
-
format: "scored",
|
|
138
|
-
rawLine: line
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
const emoji = line.match(EMOJI_LINE_RE);
|
|
142
|
-
if (!emoji) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
const priority = emoji[1];
|
|
146
|
-
const time = emoji[2]?.trim();
|
|
147
|
-
const text = emoji[3].trim();
|
|
148
|
-
const content = time ? `${time} ${text}` : text;
|
|
149
|
-
return {
|
|
150
|
-
date,
|
|
151
|
-
type: inferObservationType(content),
|
|
152
|
-
confidence: confidenceFromLegacyPriority(priority),
|
|
153
|
-
importance: scoreFromLegacyPriority(priority),
|
|
154
|
-
content,
|
|
155
|
-
format: "emoji",
|
|
156
|
-
priority,
|
|
157
|
-
time,
|
|
158
|
-
rawLine: line
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
function parseObservationMarkdown(markdown) {
|
|
162
|
-
const parsed = [];
|
|
163
|
-
let currentDate = "";
|
|
164
|
-
for (const line of markdown.split(/\r?\n/)) {
|
|
165
|
-
const heading = line.match(DATE_HEADING_RE);
|
|
166
|
-
if (heading) {
|
|
167
|
-
currentDate = heading[1];
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (!currentDate) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const record = parseObservationLine(line.trim(), currentDate);
|
|
174
|
-
if (record) {
|
|
175
|
-
parsed.push(record);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return parsed;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// src/lib/ledger.ts
|
|
182
|
-
var fs = __toESM(require("fs"), 1);
|
|
183
|
-
var path = __toESM(require("path"), 1);
|
|
184
|
-
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
185
|
-
var YEAR_RE = /^\d{4}$/;
|
|
186
|
-
var MONTH_RE = /^(0[1-9]|1[0-2])$/;
|
|
187
|
-
var DAY_FILE_RE = /^(0[1-9]|[12]\d|3[01])\.md$/;
|
|
188
|
-
var RAW_DAY_FILE_RE = /^(0[1-9]|[12]\d|3[01])\.jsonl$/;
|
|
189
|
-
function walkThreeLevelDateTree(rootPath, extension) {
|
|
190
|
-
if (!fs.existsSync(rootPath)) {
|
|
191
|
-
return [];
|
|
192
|
-
}
|
|
193
|
-
const results = [];
|
|
194
|
-
for (const yearEntry of fs.readdirSync(rootPath, { withFileTypes: true })) {
|
|
195
|
-
if (!yearEntry.isDirectory() || !YEAR_RE.test(yearEntry.name)) continue;
|
|
196
|
-
const yearDir = path.join(rootPath, yearEntry.name);
|
|
197
|
-
for (const monthEntry of fs.readdirSync(yearDir, { withFileTypes: true })) {
|
|
198
|
-
if (!monthEntry.isDirectory() || !MONTH_RE.test(monthEntry.name)) continue;
|
|
199
|
-
const monthDir = path.join(yearDir, monthEntry.name);
|
|
200
|
-
for (const dayEntry of fs.readdirSync(monthDir, { withFileTypes: true })) {
|
|
201
|
-
if (!dayEntry.isFile()) continue;
|
|
202
|
-
const matches = extension === ".md" ? DAY_FILE_RE.test(dayEntry.name) : RAW_DAY_FILE_RE.test(dayEntry.name);
|
|
203
|
-
if (!matches) continue;
|
|
204
|
-
const day = dayEntry.name.slice(0, extension.length * -1);
|
|
205
|
-
const date = `${yearEntry.name}-${monthEntry.name}-${day}`;
|
|
206
|
-
if (!DATE_RE.test(date)) continue;
|
|
207
|
-
results.push({
|
|
208
|
-
date,
|
|
209
|
-
absolutePath: path.join(monthDir, dayEntry.name)
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return results;
|
|
215
|
-
}
|
|
216
|
-
function inDateRange(date, fromDate, toDate) {
|
|
217
|
-
if (fromDate && date < fromDate) {
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
if (toDate && date > toDate) {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
function getLedgerRoot(vaultPath) {
|
|
226
|
-
return path.join(path.resolve(vaultPath), "ledger");
|
|
227
|
-
}
|
|
228
|
-
function getObservationsRoot(vaultPath) {
|
|
229
|
-
return path.join(getLedgerRoot(vaultPath), "observations");
|
|
230
|
-
}
|
|
231
|
-
function getArchiveObservationsRoot(vaultPath) {
|
|
232
|
-
return path.join(getLedgerRoot(vaultPath), "archive", "observations");
|
|
233
|
-
}
|
|
234
|
-
function getLegacyObservationsRoot(vaultPath) {
|
|
235
|
-
return path.join(path.resolve(vaultPath), "observations");
|
|
236
|
-
}
|
|
237
|
-
function listLedgerObservationFiles(vaultPath, options = {}) {
|
|
238
|
-
return walkThreeLevelDateTree(getObservationsRoot(vaultPath), ".md").filter((entry) => inDateRange(entry.date, options.fromDate, options.toDate)).map((entry) => ({
|
|
239
|
-
date: entry.date,
|
|
240
|
-
path: entry.absolutePath,
|
|
241
|
-
location: "ledger"
|
|
242
|
-
})).sort((left, right) => left.date.localeCompare(right.date));
|
|
243
|
-
}
|
|
244
|
-
function listArchiveObservationFiles(vaultPath, options = {}) {
|
|
245
|
-
return walkThreeLevelDateTree(getArchiveObservationsRoot(vaultPath), ".md").filter((entry) => inDateRange(entry.date, options.fromDate, options.toDate)).map((entry) => ({
|
|
246
|
-
date: entry.date,
|
|
247
|
-
path: entry.absolutePath,
|
|
248
|
-
location: "archive"
|
|
249
|
-
})).sort((left, right) => left.date.localeCompare(right.date));
|
|
250
|
-
}
|
|
251
|
-
function listLegacyObservationFiles(vaultPath, options = {}) {
|
|
252
|
-
const legacyRoot = getLegacyObservationsRoot(vaultPath);
|
|
253
|
-
if (!fs.existsSync(legacyRoot)) {
|
|
254
|
-
return [];
|
|
255
|
-
}
|
|
256
|
-
return fs.readdirSync(legacyRoot, { withFileTypes: true }).filter((entry) => entry.isFile() && DATE_RE.test(entry.name.replace(/\.md$/, "")) && entry.name.endsWith(".md")).map((entry) => {
|
|
257
|
-
const date = entry.name.replace(/\.md$/, "");
|
|
258
|
-
return {
|
|
259
|
-
date,
|
|
260
|
-
path: path.join(legacyRoot, entry.name),
|
|
261
|
-
location: "legacy"
|
|
262
|
-
};
|
|
263
|
-
}).filter((entry) => inDateRange(entry.date, options.fromDate, options.toDate)).sort((left, right) => left.date.localeCompare(right.date));
|
|
264
|
-
}
|
|
265
|
-
function listObservationFiles(vaultPath, options = {}) {
|
|
266
|
-
const includeLegacy = options.includeLegacy ?? true;
|
|
267
|
-
const includeArchive = options.includeArchive ?? false;
|
|
268
|
-
const dedupeByDate = options.dedupeByDate ?? true;
|
|
269
|
-
const files = [
|
|
270
|
-
...listLedgerObservationFiles(vaultPath, options),
|
|
271
|
-
...includeLegacy ? listLegacyObservationFiles(vaultPath, options) : [],
|
|
272
|
-
...includeArchive ? listArchiveObservationFiles(vaultPath, options) : []
|
|
273
|
-
];
|
|
274
|
-
if (!dedupeByDate) {
|
|
275
|
-
return files.sort((left, right) => left.date.localeCompare(right.date));
|
|
276
|
-
}
|
|
277
|
-
const byDate = /* @__PURE__ */ new Map();
|
|
278
|
-
const locationRank = {
|
|
279
|
-
ledger: 3,
|
|
280
|
-
legacy: 2,
|
|
281
|
-
archive: 1
|
|
282
|
-
};
|
|
283
|
-
for (const file of files) {
|
|
284
|
-
const existing = byDate.get(file.date);
|
|
285
|
-
if (!existing || locationRank[file.location] > locationRank[existing.location]) {
|
|
286
|
-
byDate.set(file.date, file);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return [...byDate.values()].sort((left, right) => left.date.localeCompare(right.date));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// src/lib/reweave.ts
|
|
293
|
-
var SUPERSEDED_MARKER_RE = /\[superseded\|by=([^\]|]+)\|detected=([^\]]+)\]/;
|
|
294
|
-
function isSuperseded(line) {
|
|
295
|
-
return SUPERSEDED_MARKER_RE.test(line);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// src/lib/search.ts
|
|
299
|
-
var QMD_INSTALL_COMMAND = "bun install -g github:tobi/qmd";
|
|
300
|
-
var QMD_NOT_INSTALLED_MESSAGE = `ClawVault requires qmd. Install: ${QMD_INSTALL_COMMAND}`;
|
|
301
|
-
var QMD_INDEX_ENV_VAR = "CLAWVAULT_QMD_INDEX";
|
|
302
|
-
var QmdUnavailableError = class extends Error {
|
|
303
|
-
constructor(message = QMD_NOT_INSTALLED_MESSAGE) {
|
|
304
|
-
super(message);
|
|
305
|
-
this.name = "QmdUnavailableError";
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
function ensureJsonArgs(args) {
|
|
309
|
-
return args.includes("--json") ? args : [...args, "--json"];
|
|
310
|
-
}
|
|
311
|
-
function resolveQmdIndexName(indexName) {
|
|
312
|
-
const explicit = indexName?.trim();
|
|
313
|
-
if (explicit) {
|
|
314
|
-
return explicit;
|
|
315
|
-
}
|
|
316
|
-
const fromEnv = process.env[QMD_INDEX_ENV_VAR]?.trim();
|
|
317
|
-
return fromEnv || void 0;
|
|
318
|
-
}
|
|
319
|
-
function withQmdIndexArgs(args, indexName) {
|
|
320
|
-
if (args.includes("--index")) {
|
|
321
|
-
return [...args];
|
|
322
|
-
}
|
|
323
|
-
const resolvedIndexName = resolveQmdIndexName(indexName);
|
|
324
|
-
if (!resolvedIndexName) {
|
|
325
|
-
return [...args];
|
|
326
|
-
}
|
|
327
|
-
return ["--index", resolvedIndexName, ...args];
|
|
328
|
-
}
|
|
329
|
-
function tryParseJson(raw) {
|
|
330
|
-
try {
|
|
331
|
-
return JSON.parse(raw);
|
|
332
|
-
} catch {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
function extractJsonPayload(raw) {
|
|
337
|
-
const start = raw.search(/[\[{]/);
|
|
338
|
-
if (start === -1) return null;
|
|
339
|
-
const end = Math.max(raw.lastIndexOf("]"), raw.lastIndexOf("}"));
|
|
340
|
-
if (end <= start) return null;
|
|
341
|
-
return raw.slice(start, end + 1);
|
|
342
|
-
}
|
|
343
|
-
function stripQmdNoise(raw) {
|
|
344
|
-
return raw.split("\n").filter((line) => {
|
|
345
|
-
const t = line.trim();
|
|
346
|
-
if (!t) return true;
|
|
347
|
-
if (t.startsWith("[node-llama-cpp]")) return false;
|
|
348
|
-
if (t.startsWith("Expanding query")) return false;
|
|
349
|
-
if (t.startsWith("Searching ") && t.endsWith("queries...")) return false;
|
|
350
|
-
if (/^[├└─│]/.test(t)) return false;
|
|
351
|
-
return true;
|
|
352
|
-
}).join("\n");
|
|
353
|
-
}
|
|
354
|
-
function parseQmdOutput(raw) {
|
|
355
|
-
const trimmed = stripQmdNoise(raw).trim();
|
|
356
|
-
if (!trimmed) return [];
|
|
357
|
-
const direct = tryParseJson(trimmed);
|
|
358
|
-
const extracted = direct ? null : extractJsonPayload(trimmed);
|
|
359
|
-
const parsed = direct ?? (extracted ? tryParseJson(extracted) : null);
|
|
360
|
-
if (!parsed) {
|
|
361
|
-
throw new Error("qmd returned non-JSON output. Ensure qmd supports --json.");
|
|
362
|
-
}
|
|
363
|
-
if (Array.isArray(parsed)) {
|
|
364
|
-
return parsed;
|
|
365
|
-
}
|
|
366
|
-
if (parsed && typeof parsed === "object") {
|
|
367
|
-
const candidate = parsed.results ?? parsed.items ?? parsed.data;
|
|
368
|
-
if (Array.isArray(candidate)) {
|
|
369
|
-
return candidate;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
throw new Error("qmd returned an unexpected JSON shape.");
|
|
373
|
-
}
|
|
374
|
-
function ensureQmdAvailable() {
|
|
375
|
-
if (!hasQmd()) {
|
|
376
|
-
throw new QmdUnavailableError();
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
function execQmd(args, indexName) {
|
|
380
|
-
ensureQmdAvailable();
|
|
381
|
-
const finalArgs = withQmdIndexArgs(ensureJsonArgs(args), indexName);
|
|
382
|
-
try {
|
|
383
|
-
const result = (0, import_child_process.execFileSync)("qmd", finalArgs, {
|
|
384
|
-
encoding: "utf-8",
|
|
385
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
386
|
-
maxBuffer: 10 * 1024 * 1024
|
|
387
|
-
// 10MB
|
|
388
|
-
});
|
|
389
|
-
return parseQmdOutput(result);
|
|
390
|
-
} catch (err) {
|
|
391
|
-
if (err?.code === "ENOENT") {
|
|
392
|
-
throw new QmdUnavailableError();
|
|
393
|
-
}
|
|
394
|
-
const output = [err?.stdout, err?.stderr].filter(Boolean).join("\n");
|
|
395
|
-
if (output) {
|
|
396
|
-
try {
|
|
397
|
-
return parseQmdOutput(output);
|
|
398
|
-
} catch {
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
const message = err?.message ? `qmd failed: ${err.message}` : "qmd failed";
|
|
402
|
-
throw new Error(message);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
function hasQmd() {
|
|
406
|
-
const result = (0, import_child_process.spawnSync)("qmd", ["--version"], { stdio: "ignore" });
|
|
407
|
-
return !result.error;
|
|
408
|
-
}
|
|
409
|
-
function qmdUpdate(collection, indexName) {
|
|
410
|
-
ensureQmdAvailable();
|
|
411
|
-
const args = ["update"];
|
|
412
|
-
if (collection) {
|
|
413
|
-
args.push("-c", collection);
|
|
414
|
-
}
|
|
415
|
-
(0, import_child_process.execFileSync)("qmd", withQmdIndexArgs(args, indexName), { stdio: "inherit" });
|
|
416
|
-
}
|
|
417
|
-
function qmdEmbed(collection, indexName) {
|
|
418
|
-
ensureQmdAvailable();
|
|
419
|
-
const args = ["embed"];
|
|
420
|
-
if (collection) {
|
|
421
|
-
args.push("-c", collection);
|
|
422
|
-
}
|
|
423
|
-
(0, import_child_process.execFileSync)("qmd", withQmdIndexArgs(args, indexName), { stdio: "inherit" });
|
|
424
|
-
}
|
|
425
|
-
function sentenceChunk(text, maxChars = 600, overlapSentences = 1) {
|
|
426
|
-
const sentences = text.split(/(?<=[.!?])\s+|\n{2,}/).map((s) => s.trim()).filter(Boolean);
|
|
427
|
-
if (sentences.length === 0) return text.trim() ? [text] : [];
|
|
428
|
-
const chunks = [];
|
|
429
|
-
let i = 0;
|
|
430
|
-
while (i < sentences.length) {
|
|
431
|
-
const chunkSents = [];
|
|
432
|
-
let chunkLen = 0;
|
|
433
|
-
let j = i;
|
|
434
|
-
while (j < sentences.length && chunkLen + sentences[j].length < maxChars) {
|
|
435
|
-
chunkSents.push(sentences[j]);
|
|
436
|
-
chunkLen += sentences[j].length + 1;
|
|
437
|
-
j++;
|
|
438
|
-
}
|
|
439
|
-
if (chunkSents.length === 0) {
|
|
440
|
-
chunkSents.push(sentences[j].slice(0, maxChars));
|
|
441
|
-
j++;
|
|
442
|
-
}
|
|
443
|
-
chunks.push(chunkSents.join(" "));
|
|
444
|
-
i = Math.max(j - overlapSentences, i + 1);
|
|
445
|
-
}
|
|
446
|
-
return chunks;
|
|
447
|
-
}
|
|
448
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
449
|
-
"what",
|
|
450
|
-
"when",
|
|
451
|
-
"where",
|
|
452
|
-
"which",
|
|
453
|
-
"that",
|
|
454
|
-
"this",
|
|
455
|
-
"have",
|
|
456
|
-
"from",
|
|
457
|
-
"with",
|
|
458
|
-
"they",
|
|
459
|
-
"been",
|
|
460
|
-
"were",
|
|
461
|
-
"will",
|
|
462
|
-
"about",
|
|
463
|
-
"would",
|
|
464
|
-
"could",
|
|
465
|
-
"should",
|
|
466
|
-
"their",
|
|
467
|
-
"there",
|
|
468
|
-
"does",
|
|
469
|
-
"your",
|
|
470
|
-
"more",
|
|
471
|
-
"some",
|
|
472
|
-
"than",
|
|
473
|
-
"into",
|
|
474
|
-
"also",
|
|
475
|
-
"just",
|
|
476
|
-
"very",
|
|
477
|
-
"much",
|
|
478
|
-
"most",
|
|
479
|
-
"many",
|
|
480
|
-
"only",
|
|
481
|
-
"other",
|
|
482
|
-
"each",
|
|
483
|
-
"every",
|
|
484
|
-
"after",
|
|
485
|
-
"before",
|
|
486
|
-
"did",
|
|
487
|
-
"the",
|
|
488
|
-
"and",
|
|
489
|
-
"for",
|
|
490
|
-
"are",
|
|
491
|
-
"was",
|
|
492
|
-
"not",
|
|
493
|
-
"but",
|
|
494
|
-
"can",
|
|
495
|
-
"had",
|
|
496
|
-
"has",
|
|
497
|
-
"how",
|
|
498
|
-
"who",
|
|
499
|
-
"why",
|
|
500
|
-
"its",
|
|
501
|
-
"you",
|
|
502
|
-
"my",
|
|
503
|
-
"me",
|
|
504
|
-
"is",
|
|
505
|
-
"it",
|
|
506
|
-
"do",
|
|
507
|
-
"so",
|
|
508
|
-
"if",
|
|
509
|
-
"or",
|
|
510
|
-
"an",
|
|
511
|
-
"on",
|
|
512
|
-
"at",
|
|
513
|
-
"by",
|
|
514
|
-
"no",
|
|
515
|
-
"up",
|
|
516
|
-
"to",
|
|
517
|
-
"in",
|
|
518
|
-
"of",
|
|
519
|
-
"am",
|
|
520
|
-
"be"
|
|
521
|
-
]);
|
|
522
|
-
function tokenize(text) {
|
|
523
|
-
return text.toLowerCase().split(/\s+/).map((w) => w.replace(/^[?.,!"'\-():;[\]{}*]+|[?.,!"'\-():;[\]{}*]+$/g, "")).filter((w) => w.length > 1);
|
|
524
|
-
}
|
|
525
|
-
function queryTerms(query) {
|
|
526
|
-
return tokenize(query).filter((w) => !STOPWORDS.has(w));
|
|
527
|
-
}
|
|
528
|
-
function bm25RankChunks(chunks, terms, max = 5) {
|
|
529
|
-
if (chunks.length === 0) return [];
|
|
530
|
-
const termSet = new Set(terms);
|
|
531
|
-
const scored = chunks.map((text, idx) => {
|
|
532
|
-
const words = new Set(tokenize(text));
|
|
533
|
-
let overlap = 0;
|
|
534
|
-
for (const t of termSet) if (words.has(t)) overlap++;
|
|
535
|
-
return { text, score: overlap, idx };
|
|
536
|
-
});
|
|
537
|
-
scored.sort((a, b) => b.score - a.score);
|
|
538
|
-
const seen = /* @__PURE__ */ new Set();
|
|
539
|
-
const result = [];
|
|
540
|
-
seen.add(0);
|
|
541
|
-
result.push({ text: chunks[0], score: scored.find((s) => s.idx === 0)?.score ?? 0 });
|
|
542
|
-
for (const s of scored) {
|
|
543
|
-
if (result.length >= max) break;
|
|
544
|
-
if (!seen.has(s.idx) && s.score > 0) {
|
|
545
|
-
seen.add(s.idx);
|
|
546
|
-
result.push({ text: s.text, score: s.score });
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
return result;
|
|
550
|
-
}
|
|
551
|
-
var MONTH_NAMES = {
|
|
552
|
-
january: 1,
|
|
553
|
-
february: 2,
|
|
554
|
-
march: 3,
|
|
555
|
-
april: 4,
|
|
556
|
-
may: 5,
|
|
557
|
-
june: 6,
|
|
558
|
-
july: 7,
|
|
559
|
-
august: 8,
|
|
560
|
-
september: 9,
|
|
561
|
-
october: 10,
|
|
562
|
-
november: 11,
|
|
563
|
-
december: 12,
|
|
564
|
-
jan: 1,
|
|
565
|
-
feb: 2,
|
|
566
|
-
mar: 3,
|
|
567
|
-
apr: 4,
|
|
568
|
-
jun: 6,
|
|
569
|
-
jul: 7,
|
|
570
|
-
aug: 8,
|
|
571
|
-
sep: 9,
|
|
572
|
-
sept: 9,
|
|
573
|
-
oct: 10,
|
|
574
|
-
nov: 11,
|
|
575
|
-
dec: 12
|
|
576
|
-
};
|
|
577
|
-
var MONTH_RE_PART = Object.keys(MONTH_NAMES).join("|");
|
|
578
|
-
var DATE_ISO_RE = /\b(\d{4})[/-](\d{1,2})[/-](\d{1,2})\b/g;
|
|
579
|
-
var DATE_US_RE = /\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/g;
|
|
580
|
-
var DATE_MONTH_DAY_YEAR_RE = new RegExp(
|
|
581
|
-
`\\b(${MONTH_RE_PART})\\s+(\\d{1,2})(?:st|nd|rd|th)?,?\\s*(\\d{4})\\b`,
|
|
582
|
-
"gi"
|
|
583
|
-
);
|
|
584
|
-
var DATE_DAY_MONTH_YEAR_RE = new RegExp(
|
|
585
|
-
`\\b(\\d{1,2})(?:st|nd|rd|th)?\\s+(${MONTH_RE_PART}),?\\s*(\\d{4})\\b`,
|
|
586
|
-
"gi"
|
|
587
|
-
);
|
|
588
|
-
var DATE_MONTH_DAY_RE = new RegExp(
|
|
589
|
-
`\\b(${MONTH_RE_PART})\\s+(\\d{1,2})(?:st|nd|rd|th)?\\b`,
|
|
590
|
-
"gi"
|
|
591
|
-
);
|
|
592
|
-
var RELATIVE_AGO_RE = /\b(\d+)\s+(days?|weeks?|months?|years?)\s+ago\b/gi;
|
|
593
|
-
var RELATIVE_IN_RE = /\bin\s+(\d+)\s+(days?|weeks?|months?|years?)\b/gi;
|
|
594
|
-
var DURATION_RE = /(?:for|took|spent|lasted|about|approximately|around)\s+(\d+)\s+(days?|weeks?|months?|years?|hours?|minutes?)/gi;
|
|
595
|
-
function tryParseISODate(y, m, d) {
|
|
596
|
-
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
597
|
-
if (dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d) return dt;
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
function unitToDays(n, unit) {
|
|
601
|
-
const u = unit.toLowerCase().replace(/s$/, "");
|
|
602
|
-
switch (u) {
|
|
603
|
-
case "day":
|
|
604
|
-
return n;
|
|
605
|
-
case "week":
|
|
606
|
-
return n * 7;
|
|
607
|
-
case "month":
|
|
608
|
-
return n * 30;
|
|
609
|
-
case "year":
|
|
610
|
-
return n * 365;
|
|
611
|
-
default:
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
function contextSnippet(text, start, end, maxLen = 150) {
|
|
616
|
-
const s = Math.max(0, start - Math.floor(maxLen / 2));
|
|
617
|
-
const e = Math.min(text.length, end + Math.floor(maxLen / 2));
|
|
618
|
-
return text.slice(s, e).replace(/\n/g, " ").trim();
|
|
619
|
-
}
|
|
620
|
-
function isoStr(d) {
|
|
621
|
-
const yy = d.getUTCFullYear();
|
|
622
|
-
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
623
|
-
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
624
|
-
return `${yy}-${mm}-${dd}`;
|
|
625
|
-
}
|
|
626
|
-
function extractDates(text, sessionDateStr) {
|
|
627
|
-
const results = [];
|
|
628
|
-
const sessionDate = sessionDateStr ? new Date(sessionDateStr) : null;
|
|
629
|
-
const seen = /* @__PURE__ */ new Set();
|
|
630
|
-
function push(date, ctx, docId = "") {
|
|
631
|
-
const key = `${date}|${ctx.slice(0, 60)}`;
|
|
632
|
-
if (seen.has(key)) return;
|
|
633
|
-
seen.add(key);
|
|
634
|
-
results.push({ date, context: ctx, documentId: docId });
|
|
635
|
-
}
|
|
636
|
-
for (const m of text.matchAll(DATE_ISO_RE)) {
|
|
637
|
-
const dt = tryParseISODate(+m[1], +m[2], +m[3]);
|
|
638
|
-
if (dt) push(isoStr(dt), contextSnippet(text, m.index, m.index + m[0].length));
|
|
639
|
-
}
|
|
640
|
-
for (const m of text.matchAll(DATE_US_RE)) {
|
|
641
|
-
const dt = tryParseISODate(+m[3], +m[1], +m[2]);
|
|
642
|
-
if (dt) push(isoStr(dt), contextSnippet(text, m.index, m.index + m[0].length));
|
|
643
|
-
}
|
|
644
|
-
for (const m of text.matchAll(DATE_MONTH_DAY_YEAR_RE)) {
|
|
645
|
-
const mon = MONTH_NAMES[m[1].toLowerCase()];
|
|
646
|
-
if (mon) {
|
|
647
|
-
const dt = tryParseISODate(+m[3], mon, +m[2]);
|
|
648
|
-
if (dt) push(isoStr(dt), contextSnippet(text, m.index, m.index + m[0].length));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
for (const m of text.matchAll(DATE_DAY_MONTH_YEAR_RE)) {
|
|
652
|
-
const mon = MONTH_NAMES[m[2].toLowerCase()];
|
|
653
|
-
if (mon) {
|
|
654
|
-
const dt = tryParseISODate(+m[3], mon, +m[1]);
|
|
655
|
-
if (dt) push(isoStr(dt), contextSnippet(text, m.index, m.index + m[0].length));
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (sessionDate) {
|
|
659
|
-
for (const m of text.matchAll(DATE_MONTH_DAY_RE)) {
|
|
660
|
-
const mon = MONTH_NAMES[m[1].toLowerCase()];
|
|
661
|
-
if (mon) {
|
|
662
|
-
const dt = tryParseISODate(sessionDate.getFullYear(), mon, +m[2]);
|
|
663
|
-
if (dt) push(isoStr(dt), contextSnippet(text, m.index, m.index + m[0].length));
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
if (sessionDate) {
|
|
668
|
-
for (const m of text.matchAll(RELATIVE_AGO_RE)) {
|
|
669
|
-
const days = unitToDays(+m[1], m[2]);
|
|
670
|
-
if (days !== null) {
|
|
671
|
-
const dt = new Date(sessionDate.getTime() - days * 864e5);
|
|
672
|
-
push(isoStr(dt), contextSnippet(text, m.index, m.index + m[0].length));
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
for (const m of text.matchAll(RELATIVE_IN_RE)) {
|
|
676
|
-
const days = unitToDays(+m[1], m[2]);
|
|
677
|
-
if (days !== null) {
|
|
678
|
-
const dt = new Date(sessionDate.getTime() + days * 864e5);
|
|
679
|
-
push(isoStr(dt), contextSnippet(text, m.index, m.index + m[0].length));
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
for (const m of text.matchAll(DURATION_RE)) {
|
|
684
|
-
push(`duration:${m[1]} ${m[2]}`, contextSnippet(text, m.index, m.index + m[0].length));
|
|
685
|
-
}
|
|
686
|
-
return results;
|
|
687
|
-
}
|
|
688
|
-
var PREF_PATTERNS = [
|
|
689
|
-
// "I use/prefer/like/love/enjoy X"
|
|
690
|
-
/\bi\s+(?:use|prefer|like|love|enjoy|favor|chose|switched to|started using|always use|usually use)\s+(.{3,60}?)(?:[.,;!?\n]|$)/gi,
|
|
691
|
-
// "my favorite X is Y"
|
|
692
|
-
/\bmy\s+(?:favorite|preferred|go-to|usual)\s+\w+\s+(?:is|are|was)\s+(.{3,60}?)(?:[.,;!?\n]|$)/gi,
|
|
693
|
-
// "I'm a big fan of X"
|
|
694
|
-
/\bi(?:'m| am)\s+(?:a )?(?:big |huge )?fan of\s+(.{3,60}?)(?:[.,;!?\n]|$)/gi,
|
|
695
|
-
// "I switched from X to Y"
|
|
696
|
-
/\bi\s+switched\s+from\s+(.{3,40}?)\s+to\s+(.{3,40}?)(?:[.,;!?\n]|$)/gi
|
|
697
|
-
];
|
|
698
|
-
function extractPreferences(text, documentId = "") {
|
|
699
|
-
const results = [];
|
|
700
|
-
const seen = /* @__PURE__ */ new Set();
|
|
701
|
-
for (const pattern of PREF_PATTERNS) {
|
|
702
|
-
for (const m of text.matchAll(pattern)) {
|
|
703
|
-
const value = (m[1] || "").trim();
|
|
704
|
-
if (!value || value.length < 3) continue;
|
|
705
|
-
const key = value.toLowerCase();
|
|
706
|
-
if (seen.has(key)) continue;
|
|
707
|
-
seen.add(key);
|
|
708
|
-
const ctx = contextSnippet(text, m.index, m.index + m[0].length, 200);
|
|
709
|
-
let category = "general";
|
|
710
|
-
if (/tool|software|app|editor|ide|framework|library|language/i.test(ctx)) category = "tool";
|
|
711
|
-
else if (/hobby|sport|exercise|game|play/i.test(ctx)) category = "hobby";
|
|
712
|
-
else if (/brand|product|model|device|hardware/i.test(ctx)) category = "brand";
|
|
713
|
-
else if (/food|drink|restaurant|cuisine|recipe/i.test(ctx)) category = "food";
|
|
714
|
-
else if (/music|movie|show|book|podcast|artist|band/i.test(ctx)) category = "entertainment";
|
|
715
|
-
results.push({ category, value, documentId, context: ctx });
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
return results;
|
|
719
|
-
}
|
|
720
|
-
var PREFERENCE_Q_RE = /(?:can you (?:recommend|suggest)|any (?:tips|advice|suggestions|recommendations)|what .*(?:recommend|suggest)|what should i|where should i|which .* should i|please (?:recommend|suggest)|based on .* (?:interest|preference|taste)|personalized|tailored to (?:my|me))/i;
|
|
721
|
-
var TEMPORAL_Q_RE = /(?:how many (?:days|weeks|months|years|hours|minutes) (?:passed|did|have|ago|between|since|in total|took)|how long (?:did|was|were|have|has|does)|how long ago|what (?:is the )?order|in order|which .* (?:first|last|earlier|later|before|after|most recent|oldest|newest)|chronological|(?:earlier|later|sooner|newer|older) than)/i;
|
|
722
|
-
var AGGREGATION_Q_RE = /(?:how many|how much|total|all the|count|list all|every|what are all|name all)/i;
|
|
723
|
-
function classifyQuestion(q) {
|
|
724
|
-
if (PREFERENCE_Q_RE.test(q)) return "preference";
|
|
725
|
-
if (TEMPORAL_Q_RE.test(q)) return "temporal";
|
|
726
|
-
if (!TEMPORAL_Q_RE.test(q) && AGGREGATION_Q_RE.test(q)) return "aggregation";
|
|
727
|
-
return "default";
|
|
728
|
-
}
|
|
729
|
-
var SearchEngine = class {
|
|
730
|
-
documents = /* @__PURE__ */ new Map();
|
|
731
|
-
collection = "clawvault";
|
|
732
|
-
vaultPath = "";
|
|
733
|
-
collectionRoot = "";
|
|
734
|
-
qmdIndexName;
|
|
735
|
-
/** v2.7 — Per-document date index built at ingest time */
|
|
736
|
-
dateIndex = /* @__PURE__ */ new Map();
|
|
737
|
-
/** v2.7 — Per-document preference index built at ingest time */
|
|
738
|
-
preferenceIndex = /* @__PURE__ */ new Map();
|
|
739
|
-
/** v2.7 — Per-document chunk cache for BM25 pre-filtering */
|
|
740
|
-
chunkCache = /* @__PURE__ */ new Map();
|
|
741
|
-
/**
|
|
742
|
-
* Set the collection name (usually vault name)
|
|
743
|
-
*/
|
|
744
|
-
setCollection(name) {
|
|
745
|
-
this.collection = name;
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Set the vault path for file resolution
|
|
749
|
-
*/
|
|
750
|
-
setVaultPath(vaultPath) {
|
|
751
|
-
this.vaultPath = vaultPath;
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Set the collection root for qmd:// URI resolution
|
|
755
|
-
*/
|
|
756
|
-
setCollectionRoot(root) {
|
|
757
|
-
this.collectionRoot = path2.resolve(root);
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Set qmd index name (defaults to qmd global default when omitted)
|
|
761
|
-
*/
|
|
762
|
-
setIndexName(indexName) {
|
|
763
|
-
this.qmdIndexName = indexName;
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Add or update a document in the local cache.
|
|
767
|
-
* v2.7: also extracts dates, preferences, and chunks at ingest time.
|
|
768
|
-
* Note: qmd indexing happens via qmd update command
|
|
769
|
-
*/
|
|
770
|
-
addDocument(doc) {
|
|
771
|
-
this.documents.set(doc.id, doc);
|
|
772
|
-
if (doc.content) {
|
|
773
|
-
const sessionDate = doc.modified ? isoStr(doc.modified) : void 0;
|
|
774
|
-
const dates = extractDates(doc.content, sessionDate);
|
|
775
|
-
for (const d of dates) d.documentId = doc.id;
|
|
776
|
-
if (dates.length > 0) this.dateIndex.set(doc.id, dates);
|
|
777
|
-
const prefs = extractPreferences(doc.content, doc.id);
|
|
778
|
-
if (prefs.length > 0) this.preferenceIndex.set(doc.id, prefs);
|
|
779
|
-
const chunks = sentenceChunk(doc.content, 600, 1);
|
|
780
|
-
if (chunks.length > 0) this.chunkCache.set(doc.id, chunks);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Remove a document from the local cache
|
|
785
|
-
*/
|
|
786
|
-
removeDocument(id) {
|
|
787
|
-
this.documents.delete(id);
|
|
788
|
-
this.dateIndex.delete(id);
|
|
789
|
-
this.preferenceIndex.delete(id);
|
|
790
|
-
this.chunkCache.delete(id);
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* No-op for qmd - indexing is managed externally
|
|
794
|
-
*/
|
|
795
|
-
rebuildIDF() {
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* BM25 search via qmd
|
|
799
|
-
*/
|
|
800
|
-
search(query, options = {}) {
|
|
801
|
-
return this.runQmdQuery("search", query, options);
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Vector/semantic search via qmd vsearch
|
|
805
|
-
*/
|
|
806
|
-
vsearch(query, options = {}) {
|
|
807
|
-
return this.runQmdQuery("vsearch", query, options);
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Combined search with query expansion (qmd query command)
|
|
811
|
-
*/
|
|
812
|
-
query(query, options = {}) {
|
|
813
|
-
return this.runQmdQuery("query", query, options);
|
|
814
|
-
}
|
|
815
|
-
runQmdQuery(command, query, options) {
|
|
816
|
-
const {
|
|
817
|
-
limit = 10,
|
|
818
|
-
minScore = 0,
|
|
819
|
-
category,
|
|
820
|
-
tags,
|
|
821
|
-
fullContent = false,
|
|
822
|
-
temporalBoost = false,
|
|
823
|
-
relevanceThreshold,
|
|
824
|
-
thresholdMaxResults = 40
|
|
825
|
-
} = options;
|
|
826
|
-
if (!query.trim()) return [];
|
|
827
|
-
const fetchLimit = relevanceThreshold !== void 0 ? thresholdMaxResults * 2 : limit * 2;
|
|
828
|
-
const args = [
|
|
829
|
-
command,
|
|
830
|
-
query,
|
|
831
|
-
"-n",
|
|
832
|
-
String(fetchLimit),
|
|
833
|
-
"--json"
|
|
834
|
-
];
|
|
835
|
-
if (this.collection) {
|
|
836
|
-
args.push("-c", this.collection);
|
|
837
|
-
}
|
|
838
|
-
const qmdResults = execQmd(args, this.qmdIndexName);
|
|
839
|
-
const effectiveLimit = relevanceThreshold !== void 0 ? thresholdMaxResults : limit;
|
|
840
|
-
const results = this.convertResults(qmdResults, {
|
|
841
|
-
limit: effectiveLimit,
|
|
842
|
-
minScore: relevanceThreshold !== void 0 ? relevanceThreshold : minScore,
|
|
843
|
-
category,
|
|
844
|
-
tags,
|
|
845
|
-
fullContent,
|
|
846
|
-
temporalBoost
|
|
847
|
-
});
|
|
848
|
-
return results;
|
|
849
|
-
}
|
|
850
|
-
// -------------------------------------------------------------------------
|
|
851
|
-
// v2.7 — New public APIs
|
|
852
|
-
// -------------------------------------------------------------------------
|
|
853
|
-
/**
|
|
854
|
-
* v2.7 — Chunk-level BM25 pre-filtered search. Ranks chunks within each
|
|
855
|
-
* document by keyword relevance before semantic ranking, so relevant
|
|
856
|
-
* content deep in long documents isn't missed.
|
|
857
|
-
*
|
|
858
|
-
* Returns results with snippets from the best-matching chunks.
|
|
859
|
-
*/
|
|
860
|
-
chunkPrefilterSearch(query, options = {}) {
|
|
861
|
-
const terms = queryTerms(query);
|
|
862
|
-
const results = this.runQmdQuery("query", query, options);
|
|
863
|
-
for (const r of results) {
|
|
864
|
-
const chunks = this.chunkCache.get(r.document.id);
|
|
865
|
-
if (chunks && chunks.length > 0 && terms.length > 0) {
|
|
866
|
-
const ranked = bm25RankChunks(chunks, terms, 3);
|
|
867
|
-
if (ranked.length > 0 && ranked[0].score > 0) {
|
|
868
|
-
r.snippet = ranked.map((c) => c.text).join("\n...\n").slice(0, 600);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
return results;
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* v2.7 — Exhaustive threshold-based search for aggregation queries.
|
|
876
|
-
* Keeps pulling results until relevance drops below threshold.
|
|
877
|
-
*/
|
|
878
|
-
exhaustiveSearch(query, threshold = 0.01, maxResults = 40) {
|
|
879
|
-
return this.runQmdQuery("query", query, {
|
|
880
|
-
relevanceThreshold: threshold,
|
|
881
|
-
thresholdMaxResults: maxResults,
|
|
882
|
-
fullContent: false
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* v2.7 — Get all extracted dates, optionally filtered by document ids.
|
|
887
|
-
*/
|
|
888
|
-
getDates(documentIds) {
|
|
889
|
-
const all = [];
|
|
890
|
-
const iter = documentIds ? documentIds.map((id) => [id, this.dateIndex.get(id)]).filter(([, v]) => v) : this.dateIndex.entries();
|
|
891
|
-
for (const [, dates] of iter) {
|
|
892
|
-
if (dates) all.push(...dates);
|
|
893
|
-
}
|
|
894
|
-
return all;
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* v2.7 — Get all extracted preferences, optionally filtered by document ids.
|
|
898
|
-
*/
|
|
899
|
-
getPreferences(documentIds) {
|
|
900
|
-
const all = [];
|
|
901
|
-
const iter = documentIds ? documentIds.map((id) => [id, this.preferenceIndex.get(id)]).filter(([, v]) => v) : this.preferenceIndex.entries();
|
|
902
|
-
for (const [, prefs] of iter) {
|
|
903
|
-
if (prefs) all.push(...prefs);
|
|
904
|
-
}
|
|
905
|
-
return all;
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* v2.7 — Search with automatic strategy selection based on question type.
|
|
909
|
-
* Classifies the query and routes to the appropriate pipeline.
|
|
910
|
-
*/
|
|
911
|
-
smartQuery(query, options = {}) {
|
|
912
|
-
const qtype = classifyQuestion(query);
|
|
913
|
-
switch (qtype) {
|
|
914
|
-
case "aggregation":
|
|
915
|
-
return this.exhaustiveSearch(query, 0.01, options.thresholdMaxResults ?? 40);
|
|
916
|
-
case "preference":
|
|
917
|
-
case "temporal":
|
|
918
|
-
default:
|
|
919
|
-
return this.chunkPrefilterSearch(query, { ...options, limit: options.limit ?? 10 });
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
/**
|
|
923
|
-
* Convert qmd results to ClawVault SearchResult format
|
|
924
|
-
*/
|
|
925
|
-
convertResults(qmdResults, options) {
|
|
926
|
-
const { limit = 10, minScore = 0, category, tags, fullContent = false, temporalBoost = false } = options;
|
|
927
|
-
const results = [];
|
|
928
|
-
const maxScore = qmdResults[0]?.score || 1;
|
|
929
|
-
for (const qr of qmdResults) {
|
|
930
|
-
const filePath = this.qmdUriToPath(qr.file);
|
|
931
|
-
const relativePath = this.vaultPath ? path2.relative(this.vaultPath, filePath) : filePath;
|
|
932
|
-
const normalizedRelativePath = relativePath.replace(/\\/g, "/");
|
|
933
|
-
if (normalizedRelativePath.startsWith("ledger/archive/") || normalizedRelativePath.includes("/ledger/archive/")) {
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
const docId = normalizedRelativePath.replace(/\.md$/, "");
|
|
937
|
-
let doc = this.documents.get(docId) ?? this.documents.get(docId.split("/").join(path2.sep));
|
|
938
|
-
const modifiedAt = this.resolveModifiedAt(doc, filePath);
|
|
939
|
-
const parts = normalizedRelativePath.split("/");
|
|
940
|
-
const docCategory = parts.length > 1 ? parts[0] : "root";
|
|
941
|
-
if (category && docCategory !== category) continue;
|
|
942
|
-
if (tags && tags.length > 0 && doc) {
|
|
943
|
-
const docTags = new Set(doc.tags);
|
|
944
|
-
if (!tags.some((t) => docTags.has(t))) continue;
|
|
945
|
-
}
|
|
946
|
-
const normalizedScore = maxScore > 0 ? qr.score / maxScore : 0;
|
|
947
|
-
const finalScore = temporalBoost ? normalizedScore * this.getRecencyFactor(modifiedAt) : normalizedScore;
|
|
948
|
-
if (finalScore < minScore) continue;
|
|
949
|
-
if (!doc) {
|
|
950
|
-
doc = {
|
|
951
|
-
id: docId,
|
|
952
|
-
path: filePath,
|
|
953
|
-
category: docCategory,
|
|
954
|
-
title: qr.title || path2.basename(relativePath, ".md"),
|
|
955
|
-
content: "",
|
|
956
|
-
// Content loaded separately if needed
|
|
957
|
-
frontmatter: {},
|
|
958
|
-
links: [],
|
|
959
|
-
tags: [],
|
|
960
|
-
modified: modifiedAt
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
results.push({
|
|
964
|
-
document: fullContent ? doc : { ...doc, content: "" },
|
|
965
|
-
score: finalScore,
|
|
966
|
-
snippet: this.stripSupersededFromSnippet(this.cleanSnippet(qr.snippet)),
|
|
967
|
-
matchedTerms: []
|
|
968
|
-
// qmd doesn't provide this
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
972
|
-
}
|
|
973
|
-
resolveModifiedAt(doc, filePath) {
|
|
974
|
-
if (doc) return doc.modified;
|
|
975
|
-
try {
|
|
976
|
-
return fs2.statSync(filePath).mtime;
|
|
977
|
-
} catch {
|
|
978
|
-
return /* @__PURE__ */ new Date(0);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
getRecencyFactor(modifiedAt) {
|
|
982
|
-
const ageMs = Math.max(0, Date.now() - modifiedAt.getTime());
|
|
983
|
-
const ageDays = ageMs / (24 * 60 * 60 * 1e3);
|
|
984
|
-
if (ageDays < 1) return 1;
|
|
985
|
-
if (ageDays <= 7) return 0.9;
|
|
986
|
-
return 0.7;
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Convert qmd:// URI to file path
|
|
990
|
-
*/
|
|
991
|
-
qmdUriToPath(uri) {
|
|
992
|
-
if (uri.startsWith("qmd://")) {
|
|
993
|
-
const withoutScheme = uri.slice(6);
|
|
994
|
-
const slashIndex = withoutScheme.indexOf("/");
|
|
995
|
-
if (slashIndex > -1) {
|
|
996
|
-
const relativePath = withoutScheme.slice(slashIndex + 1);
|
|
997
|
-
const root = this.collectionRoot || this.vaultPath;
|
|
998
|
-
if (root) {
|
|
999
|
-
return path2.join(root, relativePath);
|
|
1000
|
-
}
|
|
1001
|
-
return relativePath;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
return uri;
|
|
1005
|
-
}
|
|
1006
|
-
/**
|
|
1007
|
-
* v2.8 — Filter superseded observation lines from snippet text.
|
|
1008
|
-
* Ensures search results prefer the latest version of knowledge.
|
|
1009
|
-
*/
|
|
1010
|
-
stripSupersededFromSnippet(snippet) {
|
|
1011
|
-
if (!snippet) return snippet;
|
|
1012
|
-
return snippet.split("\n").filter((line) => !isSuperseded(line)).join("\n");
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Clean up qmd snippet format
|
|
1016
|
-
*/
|
|
1017
|
-
cleanSnippet(snippet) {
|
|
1018
|
-
if (!snippet) return "";
|
|
1019
|
-
return snippet.replace(/@@ [-+]?\d+,?\d* @@ \([^)]+\)/g, "").trim().split("\n").slice(0, 3).join("\n").slice(0, 300);
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Get all cached documents
|
|
1023
|
-
*/
|
|
1024
|
-
getAllDocuments() {
|
|
1025
|
-
return [...this.documents.values()];
|
|
1026
|
-
}
|
|
1027
|
-
/**
|
|
1028
|
-
* Get document count
|
|
1029
|
-
*/
|
|
1030
|
-
get size() {
|
|
1031
|
-
return this.documents.size;
|
|
1032
|
-
}
|
|
1033
|
-
/**
|
|
1034
|
-
* Clear the local document cache and all v2.7 indices
|
|
1035
|
-
*/
|
|
1036
|
-
clear() {
|
|
1037
|
-
this.documents.clear();
|
|
1038
|
-
this.dateIndex.clear();
|
|
1039
|
-
this.preferenceIndex.clear();
|
|
1040
|
-
this.chunkCache.clear();
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Export documents for persistence
|
|
1044
|
-
*/
|
|
1045
|
-
export() {
|
|
1046
|
-
return {
|
|
1047
|
-
documents: [...this.documents.values()]
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Import from persisted data
|
|
1052
|
-
*/
|
|
1053
|
-
import(data) {
|
|
1054
|
-
this.clear();
|
|
1055
|
-
for (const doc of data.documents) {
|
|
1056
|
-
this.addDocument(doc);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
};
|
|
1060
|
-
function extractWikiLinks(content) {
|
|
1061
|
-
const matches = content.match(/\[\[([^\]]+)\]\]/g) || [];
|
|
1062
|
-
return matches.map((m) => m.slice(2, -2).toLowerCase());
|
|
1063
|
-
}
|
|
1064
|
-
function extractTags(content) {
|
|
1065
|
-
const matches = content.match(/#[\w-]+/g) || [];
|
|
1066
|
-
return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// src/lib/memory-graph.ts
|
|
1070
|
-
var fs3 = __toESM(require("fs"), 1);
|
|
1071
|
-
var path3 = __toESM(require("path"), 1);
|
|
1072
|
-
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
1073
|
-
var import_glob = require("glob");
|
|
1074
|
-
var MEMORY_GRAPH_SCHEMA_VERSION = 1;
|
|
1075
|
-
var GRAPH_INDEX_RELATIVE_PATH = path3.join(".clawvault", "graph-index.json");
|
|
1076
|
-
var WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
1077
|
-
var HASH_TAG_RE = /(^|\s)#([\w-]+)/g;
|
|
1078
|
-
var FRONTMATTER_RELATION_FIELDS = [
|
|
1079
|
-
"related",
|
|
1080
|
-
"depends_on",
|
|
1081
|
-
"dependsOn",
|
|
1082
|
-
"blocked_by",
|
|
1083
|
-
"blocks",
|
|
1084
|
-
"owner",
|
|
1085
|
-
"project",
|
|
1086
|
-
"people",
|
|
1087
|
-
"links"
|
|
1088
|
-
];
|
|
1089
|
-
function normalizeRelativePath(value) {
|
|
1090
|
-
return value.split(path3.sep).join("/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
1091
|
-
}
|
|
1092
|
-
function toNoteKey(relativePath) {
|
|
1093
|
-
const normalized = normalizeRelativePath(relativePath);
|
|
1094
|
-
return normalized.toLowerCase().endsWith(".md") ? normalized.slice(0, -3) : normalized;
|
|
1095
|
-
}
|
|
1096
|
-
function toNoteNodeId(noteKey) {
|
|
1097
|
-
return `note:${noteKey}`;
|
|
1098
|
-
}
|
|
1099
|
-
function toTagNodeId(tag) {
|
|
1100
|
-
return `tag:${tag.toLowerCase()}`;
|
|
1101
|
-
}
|
|
1102
|
-
function normalizeUnresolvedKey(raw) {
|
|
1103
|
-
const normalized = raw.trim().toLowerCase().replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\.md$/, "").replace(/[^a-z0-9/_-]+/g, "-").replace(/\/+/g, "/").replace(/-+/g, "-").replace(/^[-/]+|[-/]+$/g, "");
|
|
1104
|
-
return normalized || "unknown";
|
|
1105
|
-
}
|
|
1106
|
-
function toUnresolvedNodeId(raw) {
|
|
1107
|
-
return `unresolved:${normalizeUnresolvedKey(raw)}`;
|
|
1108
|
-
}
|
|
1109
|
-
function titleFromNoteKey(noteKey) {
|
|
1110
|
-
const basename4 = noteKey.split("/").pop() ?? noteKey;
|
|
1111
|
-
return basename4.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1112
|
-
}
|
|
1113
|
-
function inferNodeType(relativePath, frontmatter) {
|
|
1114
|
-
const normalized = normalizeRelativePath(relativePath).toLowerCase();
|
|
1115
|
-
const category = normalized.split("/")[0] ?? "note";
|
|
1116
|
-
const explicitType = typeof frontmatter.type === "string" ? frontmatter.type.toLowerCase() : "";
|
|
1117
|
-
if (category.includes("daily") || explicitType === "daily") return "daily";
|
|
1118
|
-
if (category === "observations" || explicitType === "observation") return "observation";
|
|
1119
|
-
if (category === "handoffs" || explicitType === "handoff") return "handoff";
|
|
1120
|
-
if (category === "decisions" || explicitType === "decision") return "decision";
|
|
1121
|
-
if (category === "lessons" || explicitType === "lesson") return "lesson";
|
|
1122
|
-
if (category === "projects" || explicitType === "project") return "project";
|
|
1123
|
-
if (category === "people" || explicitType === "person") return "person";
|
|
1124
|
-
if (category === "commitments" || explicitType === "commitment") return "commitment";
|
|
1125
|
-
return "note";
|
|
1126
|
-
}
|
|
1127
|
-
function ensureClawvaultDir(vaultPath) {
|
|
1128
|
-
const dirPath = path3.join(vaultPath, ".clawvault");
|
|
1129
|
-
if (!fs3.existsSync(dirPath)) {
|
|
1130
|
-
fs3.mkdirSync(dirPath, { recursive: true });
|
|
1131
|
-
}
|
|
1132
|
-
return dirPath;
|
|
1133
|
-
}
|
|
1134
|
-
function getGraphIndexPath(vaultPath) {
|
|
1135
|
-
return path3.join(vaultPath, GRAPH_INDEX_RELATIVE_PATH);
|
|
1136
|
-
}
|
|
1137
|
-
function normalizeWikiTarget(target) {
|
|
1138
|
-
let value = target.trim();
|
|
1139
|
-
if (!value) return "";
|
|
1140
|
-
const pipeIndex = value.indexOf("|");
|
|
1141
|
-
if (pipeIndex >= 0) {
|
|
1142
|
-
value = value.slice(0, pipeIndex);
|
|
1143
|
-
}
|
|
1144
|
-
const hashIndex = value.indexOf("#");
|
|
1145
|
-
if (hashIndex >= 0) {
|
|
1146
|
-
value = value.slice(0, hashIndex);
|
|
1147
|
-
}
|
|
1148
|
-
value = value.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
1149
|
-
if (value.toLowerCase().endsWith(".md")) {
|
|
1150
|
-
value = value.slice(0, -3);
|
|
1151
|
-
}
|
|
1152
|
-
return value.trim();
|
|
1153
|
-
}
|
|
1154
|
-
function collectTags(frontmatter, markdownContent) {
|
|
1155
|
-
const tags = /* @__PURE__ */ new Set();
|
|
1156
|
-
const fmTags = frontmatter.tags;
|
|
1157
|
-
if (Array.isArray(fmTags)) {
|
|
1158
|
-
for (const tag of fmTags) {
|
|
1159
|
-
if (typeof tag === "string" && tag.trim()) tags.add(tag.trim().toLowerCase());
|
|
1160
|
-
}
|
|
1161
|
-
} else if (typeof fmTags === "string") {
|
|
1162
|
-
for (const token of fmTags.split(",")) {
|
|
1163
|
-
const normalized = token.trim().toLowerCase();
|
|
1164
|
-
if (normalized) tags.add(normalized);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
const markdownMatches = markdownContent.matchAll(HASH_TAG_RE);
|
|
1168
|
-
for (const match of markdownMatches) {
|
|
1169
|
-
const tag = match[2]?.trim().toLowerCase();
|
|
1170
|
-
if (tag) tags.add(tag);
|
|
1171
|
-
}
|
|
1172
|
-
return [...tags].sort((a, b) => a.localeCompare(b));
|
|
1173
|
-
}
|
|
1174
|
-
function extractWikiTargets(markdownContent) {
|
|
1175
|
-
const targets = /* @__PURE__ */ new Set();
|
|
1176
|
-
for (const match of markdownContent.matchAll(WIKI_LINK_RE)) {
|
|
1177
|
-
const candidate = match[1];
|
|
1178
|
-
if (!candidate) continue;
|
|
1179
|
-
const normalized = normalizeWikiTarget(candidate);
|
|
1180
|
-
if (normalized) targets.add(normalized);
|
|
1181
|
-
}
|
|
1182
|
-
return [...targets];
|
|
1183
|
-
}
|
|
1184
|
-
function toStringArray(value) {
|
|
1185
|
-
if (typeof value === "string") {
|
|
1186
|
-
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1187
|
-
}
|
|
1188
|
-
if (Array.isArray(value)) {
|
|
1189
|
-
return value.flatMap((entry) => typeof entry === "string" ? entry.split(",") : []).map((entry) => entry.trim()).filter(Boolean);
|
|
1190
|
-
}
|
|
1191
|
-
return [];
|
|
1192
|
-
}
|
|
1193
|
-
function extractFrontmatterRelations(frontmatter) {
|
|
1194
|
-
const relations = [];
|
|
1195
|
-
for (const field of FRONTMATTER_RELATION_FIELDS) {
|
|
1196
|
-
const raw = frontmatter[field];
|
|
1197
|
-
for (const value of toStringArray(raw)) {
|
|
1198
|
-
const normalized = normalizeWikiTarget(value);
|
|
1199
|
-
if (normalized) relations.push({ field, target: normalized });
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
return relations;
|
|
1203
|
-
}
|
|
1204
|
-
function buildNoteRegistry(relativePaths) {
|
|
1205
|
-
const byLowerPath = /* @__PURE__ */ new Map();
|
|
1206
|
-
const byLowerBasename = /* @__PURE__ */ new Map();
|
|
1207
|
-
for (const relativePath of relativePaths) {
|
|
1208
|
-
const noteKey = toNoteKey(relativePath);
|
|
1209
|
-
const lowerKey = noteKey.toLowerCase();
|
|
1210
|
-
if (!byLowerPath.has(lowerKey)) {
|
|
1211
|
-
byLowerPath.set(lowerKey, noteKey);
|
|
1212
|
-
}
|
|
1213
|
-
const base = noteKey.split("/").pop() ?? noteKey;
|
|
1214
|
-
const lowerBase = base.toLowerCase();
|
|
1215
|
-
const existing = byLowerBasename.get(lowerBase) ?? [];
|
|
1216
|
-
existing.push(noteKey);
|
|
1217
|
-
byLowerBasename.set(lowerBase, existing);
|
|
1218
|
-
}
|
|
1219
|
-
return { byLowerPath, byLowerBasename };
|
|
1220
|
-
}
|
|
1221
|
-
function resolveTargetNodeId(rawTarget, registry) {
|
|
1222
|
-
const normalized = normalizeWikiTarget(rawTarget);
|
|
1223
|
-
if (!normalized) {
|
|
1224
|
-
return toUnresolvedNodeId(rawTarget);
|
|
1225
|
-
}
|
|
1226
|
-
const lowerTarget = normalized.toLowerCase();
|
|
1227
|
-
const direct = registry.byLowerPath.get(lowerTarget);
|
|
1228
|
-
if (direct) {
|
|
1229
|
-
return toNoteNodeId(direct);
|
|
1230
|
-
}
|
|
1231
|
-
if (!normalized.includes("/")) {
|
|
1232
|
-
const basenameMatches = registry.byLowerBasename.get(lowerTarget) ?? [];
|
|
1233
|
-
if (basenameMatches.length === 1) {
|
|
1234
|
-
return toNoteNodeId(basenameMatches[0]);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
return toUnresolvedNodeId(normalized);
|
|
1238
|
-
}
|
|
1239
|
-
function createEdgeId(type, source, target, label) {
|
|
1240
|
-
const suffix = label ? `:${label}` : "";
|
|
1241
|
-
return `${type}:${source}->${target}${suffix}`;
|
|
1242
|
-
}
|
|
1243
|
-
function buildFragmentNode(id, title, type, category, pathValue, tags, missing, modifiedAt) {
|
|
1244
|
-
return {
|
|
1245
|
-
id,
|
|
1246
|
-
title,
|
|
1247
|
-
type,
|
|
1248
|
-
category,
|
|
1249
|
-
path: pathValue,
|
|
1250
|
-
tags,
|
|
1251
|
-
missing,
|
|
1252
|
-
degree: 0,
|
|
1253
|
-
modifiedAt
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
function parseFileFragment(vaultPath, relativePath, mtimeMs, registry) {
|
|
1257
|
-
const absolutePath = path3.join(vaultPath, relativePath);
|
|
1258
|
-
const raw = fs3.readFileSync(absolutePath, "utf-8");
|
|
1259
|
-
const parsed = (0, import_gray_matter.default)(raw);
|
|
1260
|
-
const frontmatter = parsed.data ?? {};
|
|
1261
|
-
const noteKey = toNoteKey(relativePath);
|
|
1262
|
-
const noteNodeId = toNoteNodeId(noteKey);
|
|
1263
|
-
const noteType = inferNodeType(relativePath, frontmatter);
|
|
1264
|
-
const tags = collectTags(frontmatter, parsed.content);
|
|
1265
|
-
const modifiedAt = new Date(mtimeMs).toISOString();
|
|
1266
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
1267
|
-
const edges = /* @__PURE__ */ new Map();
|
|
1268
|
-
nodes.set(
|
|
1269
|
-
noteNodeId,
|
|
1270
|
-
buildFragmentNode(
|
|
1271
|
-
noteNodeId,
|
|
1272
|
-
typeof frontmatter.title === "string" && frontmatter.title.trim() ? frontmatter.title.trim() : titleFromNoteKey(noteKey),
|
|
1273
|
-
noteType,
|
|
1274
|
-
noteType,
|
|
1275
|
-
normalizeRelativePath(relativePath),
|
|
1276
|
-
tags,
|
|
1277
|
-
false,
|
|
1278
|
-
modifiedAt
|
|
1279
|
-
)
|
|
1280
|
-
);
|
|
1281
|
-
for (const tag of tags) {
|
|
1282
|
-
const tagNodeId = toTagNodeId(tag);
|
|
1283
|
-
if (!nodes.has(tagNodeId)) {
|
|
1284
|
-
nodes.set(tagNodeId, buildFragmentNode(tagNodeId, `#${tag}`, "tag", "tag", null, [], false, null));
|
|
1285
|
-
}
|
|
1286
|
-
const edgeId = createEdgeId("tag", noteNodeId, tagNodeId);
|
|
1287
|
-
edges.set(edgeId, {
|
|
1288
|
-
id: edgeId,
|
|
1289
|
-
source: noteNodeId,
|
|
1290
|
-
target: tagNodeId,
|
|
1291
|
-
type: "tag"
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
const wikiTargets = extractWikiTargets(parsed.content);
|
|
1295
|
-
for (const target of wikiTargets) {
|
|
1296
|
-
const targetNodeId = resolveTargetNodeId(target, registry);
|
|
1297
|
-
if (targetNodeId.startsWith("unresolved:") && !nodes.has(targetNodeId)) {
|
|
1298
|
-
nodes.set(
|
|
1299
|
-
targetNodeId,
|
|
1300
|
-
buildFragmentNode(targetNodeId, titleFromNoteKey(normalizeUnresolvedKey(target)), "unresolved", "unresolved", null, [], true, null)
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
const edgeId = createEdgeId("wiki_link", noteNodeId, targetNodeId);
|
|
1304
|
-
edges.set(edgeId, {
|
|
1305
|
-
id: edgeId,
|
|
1306
|
-
source: noteNodeId,
|
|
1307
|
-
target: targetNodeId,
|
|
1308
|
-
type: "wiki_link"
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
for (const relation of extractFrontmatterRelations(frontmatter)) {
|
|
1312
|
-
const targetNodeId = resolveTargetNodeId(relation.target, registry);
|
|
1313
|
-
if (targetNodeId.startsWith("unresolved:") && !nodes.has(targetNodeId)) {
|
|
1314
|
-
nodes.set(
|
|
1315
|
-
targetNodeId,
|
|
1316
|
-
buildFragmentNode(
|
|
1317
|
-
targetNodeId,
|
|
1318
|
-
titleFromNoteKey(normalizeUnresolvedKey(relation.target)),
|
|
1319
|
-
"unresolved",
|
|
1320
|
-
"unresolved",
|
|
1321
|
-
null,
|
|
1322
|
-
[],
|
|
1323
|
-
true,
|
|
1324
|
-
null
|
|
1325
|
-
)
|
|
1326
|
-
);
|
|
1327
|
-
}
|
|
1328
|
-
const edgeId = createEdgeId("frontmatter_relation", noteNodeId, targetNodeId, relation.field);
|
|
1329
|
-
edges.set(edgeId, {
|
|
1330
|
-
id: edgeId,
|
|
1331
|
-
source: noteNodeId,
|
|
1332
|
-
target: targetNodeId,
|
|
1333
|
-
type: "frontmatter_relation",
|
|
1334
|
-
label: relation.field
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
return {
|
|
1338
|
-
relativePath: normalizeRelativePath(relativePath),
|
|
1339
|
-
mtimeMs,
|
|
1340
|
-
nodes: [...nodes.values()],
|
|
1341
|
-
edges: [...edges.values()]
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
function combineFragments(fragments, generatedAt) {
|
|
1345
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
1346
|
-
const edges = /* @__PURE__ */ new Map();
|
|
1347
|
-
for (const fragment of Object.values(fragments)) {
|
|
1348
|
-
for (const node of fragment.nodes) {
|
|
1349
|
-
const existing = nodes.get(node.id);
|
|
1350
|
-
if (!existing) {
|
|
1351
|
-
nodes.set(node.id, { ...node, degree: 0 });
|
|
1352
|
-
} else if (node.modifiedAt && (!existing.modifiedAt || node.modifiedAt > existing.modifiedAt)) {
|
|
1353
|
-
nodes.set(node.id, { ...existing, ...node, degree: 0 });
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
for (const edge of fragment.edges) {
|
|
1357
|
-
edges.set(edge.id, edge);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
const degreeByNode = /* @__PURE__ */ new Map();
|
|
1361
|
-
for (const edge of edges.values()) {
|
|
1362
|
-
degreeByNode.set(edge.source, (degreeByNode.get(edge.source) ?? 0) + 1);
|
|
1363
|
-
degreeByNode.set(edge.target, (degreeByNode.get(edge.target) ?? 0) + 1);
|
|
1364
|
-
}
|
|
1365
|
-
for (const node of nodes.values()) {
|
|
1366
|
-
node.degree = degreeByNode.get(node.id) ?? 0;
|
|
1367
|
-
}
|
|
1368
|
-
const nodeTypeCounts = {};
|
|
1369
|
-
for (const node of nodes.values()) {
|
|
1370
|
-
nodeTypeCounts[node.type] = (nodeTypeCounts[node.type] ?? 0) + 1;
|
|
1371
|
-
}
|
|
1372
|
-
const edgeTypeCounts = {};
|
|
1373
|
-
for (const edge of edges.values()) {
|
|
1374
|
-
edgeTypeCounts[edge.type] = (edgeTypeCounts[edge.type] ?? 0) + 1;
|
|
1375
|
-
}
|
|
1376
|
-
const sortedNodes = [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
1377
|
-
const sortedEdges = [...edges.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
1378
|
-
return {
|
|
1379
|
-
schemaVersion: MEMORY_GRAPH_SCHEMA_VERSION,
|
|
1380
|
-
nodes: sortedNodes,
|
|
1381
|
-
edges: sortedEdges,
|
|
1382
|
-
stats: {
|
|
1383
|
-
generatedAt,
|
|
1384
|
-
nodeCount: sortedNodes.length,
|
|
1385
|
-
edgeCount: sortedEdges.length,
|
|
1386
|
-
nodeTypeCounts,
|
|
1387
|
-
edgeTypeCounts
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
}
|
|
1391
|
-
function isValidIndex(index) {
|
|
1392
|
-
if (!index || typeof index !== "object") return false;
|
|
1393
|
-
const typed = index;
|
|
1394
|
-
return typed.schemaVersion === MEMORY_GRAPH_SCHEMA_VERSION && typeof typed.vaultPath === "string" && typeof typed.generatedAt === "string" && Boolean(typed.files && typeof typed.files === "object") && Boolean(typed.graph && typeof typed.graph === "object");
|
|
1395
|
-
}
|
|
1396
|
-
function loadMemoryGraphIndex(vaultPath) {
|
|
1397
|
-
const indexPath = getGraphIndexPath(path3.resolve(vaultPath));
|
|
1398
|
-
if (!fs3.existsSync(indexPath)) {
|
|
1399
|
-
return null;
|
|
1400
|
-
}
|
|
1401
|
-
try {
|
|
1402
|
-
const parsed = JSON.parse(fs3.readFileSync(indexPath, "utf-8"));
|
|
1403
|
-
if (!isValidIndex(parsed)) {
|
|
1404
|
-
return null;
|
|
1405
|
-
}
|
|
1406
|
-
return parsed;
|
|
1407
|
-
} catch {
|
|
1408
|
-
return null;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
async function buildOrUpdateMemoryGraphIndex(vaultPathInput, options = {}) {
|
|
1412
|
-
const vaultPath = path3.resolve(vaultPathInput);
|
|
1413
|
-
ensureClawvaultDir(vaultPath);
|
|
1414
|
-
const existing = options.forceFull ? null : loadMemoryGraphIndex(vaultPath);
|
|
1415
|
-
const markdownFiles = await (0, import_glob.glob)("**/*.md", {
|
|
1416
|
-
cwd: vaultPath,
|
|
1417
|
-
ignore: ["**/node_modules/**", "**/.git/**", "**/.obsidian/**", "**/.trash/**", "**/ledger/archive/**"]
|
|
1418
|
-
});
|
|
1419
|
-
const normalizedFiles = markdownFiles.map(normalizeRelativePath).sort((a, b) => a.localeCompare(b));
|
|
1420
|
-
const registry = buildNoteRegistry(normalizedFiles);
|
|
1421
|
-
const nextFragments = {};
|
|
1422
|
-
const existingFragments = existing?.files ?? {};
|
|
1423
|
-
const currentFileSet = new Set(normalizedFiles);
|
|
1424
|
-
for (const relativePath of normalizedFiles) {
|
|
1425
|
-
const absolutePath = path3.join(vaultPath, relativePath);
|
|
1426
|
-
const stat = fs3.statSync(absolutePath);
|
|
1427
|
-
const existingFragment = existingFragments[relativePath];
|
|
1428
|
-
if (!options.forceFull && existingFragment && existingFragment.mtimeMs === stat.mtimeMs) {
|
|
1429
|
-
nextFragments[relativePath] = existingFragment;
|
|
1430
|
-
continue;
|
|
1431
|
-
}
|
|
1432
|
-
nextFragments[relativePath] = parseFileFragment(vaultPath, relativePath, stat.mtimeMs, registry);
|
|
1433
|
-
}
|
|
1434
|
-
for (const [relativePath, fragment] of Object.entries(existingFragments)) {
|
|
1435
|
-
if (!currentFileSet.has(relativePath)) {
|
|
1436
|
-
continue;
|
|
1437
|
-
}
|
|
1438
|
-
if (!nextFragments[relativePath]) {
|
|
1439
|
-
nextFragments[relativePath] = fragment;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1443
|
-
const graph = combineFragments(nextFragments, generatedAt);
|
|
1444
|
-
const nextIndex = {
|
|
1445
|
-
schemaVersion: MEMORY_GRAPH_SCHEMA_VERSION,
|
|
1446
|
-
vaultPath,
|
|
1447
|
-
generatedAt,
|
|
1448
|
-
files: nextFragments,
|
|
1449
|
-
graph
|
|
1450
|
-
};
|
|
1451
|
-
fs3.writeFileSync(getGraphIndexPath(vaultPath), JSON.stringify(nextIndex, null, 2));
|
|
1452
|
-
return nextIndex;
|
|
1453
|
-
}
|
|
1454
|
-
async function getMemoryGraph(vaultPath, options = {}) {
|
|
1455
|
-
if (options.refresh === true) {
|
|
1456
|
-
return (await buildOrUpdateMemoryGraphIndex(vaultPath, { forceFull: true })).graph;
|
|
1457
|
-
}
|
|
1458
|
-
return (await buildOrUpdateMemoryGraphIndex(vaultPath)).graph;
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// src/lib/vault.ts
|
|
1462
|
-
var import_meta = {};
|
|
1463
|
-
var CONFIG_FILE = ".clawvault.json";
|
|
1464
|
-
var INDEX_FILE = ".clawvault-index.json";
|
|
1465
|
-
var ClawVault = class {
|
|
1466
|
-
config;
|
|
1467
|
-
search;
|
|
1468
|
-
initialized = false;
|
|
1469
|
-
constructor(vaultPath) {
|
|
1470
|
-
if (!hasQmd()) {
|
|
1471
|
-
throw new QmdUnavailableError();
|
|
1472
|
-
}
|
|
1473
|
-
this.config = {
|
|
1474
|
-
path: path4.resolve(vaultPath),
|
|
1475
|
-
name: path4.basename(vaultPath),
|
|
1476
|
-
categories: DEFAULT_CATEGORIES,
|
|
1477
|
-
qmdCollection: void 0,
|
|
1478
|
-
qmdRoot: void 0
|
|
1479
|
-
};
|
|
1480
|
-
this.search = new SearchEngine();
|
|
1481
|
-
this.applyQmdConfig();
|
|
1482
|
-
}
|
|
1483
|
-
/**
|
|
1484
|
-
* Initialize a new vault
|
|
1485
|
-
*/
|
|
1486
|
-
async init(options = {}, initFlags) {
|
|
1487
|
-
if (!hasQmd()) {
|
|
1488
|
-
throw new QmdUnavailableError();
|
|
1489
|
-
}
|
|
1490
|
-
const vaultPath = this.config.path;
|
|
1491
|
-
const flags = initFlags || {};
|
|
1492
|
-
this.config = { ...this.config, ...options };
|
|
1493
|
-
this.applyQmdConfig();
|
|
1494
|
-
if (flags.skipTasks) {
|
|
1495
|
-
this.config.categories = this.config.categories.filter(
|
|
1496
|
-
(c) => !["tasks", "backlog"].includes(c)
|
|
1497
|
-
);
|
|
1498
|
-
}
|
|
1499
|
-
if (!fs4.existsSync(vaultPath)) {
|
|
1500
|
-
fs4.mkdirSync(vaultPath, { recursive: true });
|
|
1501
|
-
}
|
|
1502
|
-
for (const category of this.config.categories) {
|
|
1503
|
-
const catPath = path4.join(vaultPath, category);
|
|
1504
|
-
if (!fs4.existsSync(catPath)) {
|
|
1505
|
-
fs4.mkdirSync(catPath, { recursive: true });
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
const ledgerDirs = ["ledger/raw", "ledger/observations", "ledger/reflections"];
|
|
1509
|
-
for (const dir of ledgerDirs) {
|
|
1510
|
-
const dirPath = path4.join(vaultPath, dir);
|
|
1511
|
-
if (!fs4.existsSync(dirPath)) {
|
|
1512
|
-
fs4.mkdirSync(dirPath, { recursive: true });
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
await this.createTemplates();
|
|
1516
|
-
const readmePath = path4.join(vaultPath, "README.md");
|
|
1517
|
-
if (!fs4.existsSync(readmePath)) {
|
|
1518
|
-
fs4.writeFileSync(readmePath, this.generateReadme());
|
|
1519
|
-
}
|
|
1520
|
-
await this.createWelcomeNote();
|
|
1521
|
-
const configPath = path4.join(vaultPath, CONFIG_FILE);
|
|
1522
|
-
const meta = {
|
|
1523
|
-
name: this.config.name,
|
|
1524
|
-
version: "1.0.0",
|
|
1525
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1526
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1527
|
-
categories: this.config.categories,
|
|
1528
|
-
documentCount: 0,
|
|
1529
|
-
qmdCollection: this.getQmdCollection(),
|
|
1530
|
-
qmdRoot: this.getQmdRoot()
|
|
1531
|
-
};
|
|
1532
|
-
fs4.writeFileSync(configPath, JSON.stringify(meta, null, 2));
|
|
1533
|
-
if (!flags.skipBases && this.config.categories.includes("tasks")) {
|
|
1534
|
-
this.createBasesFiles();
|
|
1535
|
-
}
|
|
1536
|
-
if (!flags.skipGraph) {
|
|
1537
|
-
await this.syncMemoryGraphIndex({ forceFull: true });
|
|
1538
|
-
}
|
|
1539
|
-
this.initialized = true;
|
|
1540
|
-
}
|
|
1541
|
-
createBasesFiles() {
|
|
1542
|
-
const vaultPath = this.config.path;
|
|
1543
|
-
const basesFiles = {
|
|
1544
|
-
"all-tasks.base": [
|
|
1545
|
-
"filters:",
|
|
1546
|
-
" and:",
|
|
1547
|
-
' - file.inFolder("tasks")',
|
|
1548
|
-
' - status != "done"',
|
|
1549
|
-
"formulas:",
|
|
1550
|
-
" age: (now() - file.ctime).days",
|
|
1551
|
-
' status_icon: if(status == "blocked", "\u{1F534}", if(status == "in-progress", "\u{1F528}", if(status == "open", "\u26AA", "\u2705")))',
|
|
1552
|
-
"views:",
|
|
1553
|
-
" - type: table",
|
|
1554
|
-
" name: All Active Tasks",
|
|
1555
|
-
" groupBy:",
|
|
1556
|
-
" property: status",
|
|
1557
|
-
" direction: ASC",
|
|
1558
|
-
" order:",
|
|
1559
|
-
" - formula.status_icon",
|
|
1560
|
-
" - file.name",
|
|
1561
|
-
" - status",
|
|
1562
|
-
" - owner",
|
|
1563
|
-
" - project",
|
|
1564
|
-
" - priority",
|
|
1565
|
-
" - blocked_by",
|
|
1566
|
-
" - formula.age",
|
|
1567
|
-
" - type: cards",
|
|
1568
|
-
" name: Task Board",
|
|
1569
|
-
" groupBy:",
|
|
1570
|
-
" property: status",
|
|
1571
|
-
" direction: ASC",
|
|
1572
|
-
" order:",
|
|
1573
|
-
" - file.name",
|
|
1574
|
-
" - owner",
|
|
1575
|
-
" - project",
|
|
1576
|
-
" - priority"
|
|
1577
|
-
].join("\n"),
|
|
1578
|
-
"blocked.base": [
|
|
1579
|
-
"filters:",
|
|
1580
|
-
" and:",
|
|
1581
|
-
' - file.inFolder("tasks")',
|
|
1582
|
-
' - status == "blocked"',
|
|
1583
|
-
"formulas:",
|
|
1584
|
-
" days_blocked: (now() - file.ctime).days",
|
|
1585
|
-
"views:",
|
|
1586
|
-
" - type: table",
|
|
1587
|
-
" name: Blocked Tasks",
|
|
1588
|
-
" order:",
|
|
1589
|
-
" - file.name",
|
|
1590
|
-
" - owner",
|
|
1591
|
-
" - project",
|
|
1592
|
-
" - blocked_by",
|
|
1593
|
-
" - formula.days_blocked",
|
|
1594
|
-
" - priority"
|
|
1595
|
-
].join("\n"),
|
|
1596
|
-
"by-project.base": [
|
|
1597
|
-
"filters:",
|
|
1598
|
-
" and:",
|
|
1599
|
-
' - file.inFolder("tasks")',
|
|
1600
|
-
' - status != "done"',
|
|
1601
|
-
"formulas:",
|
|
1602
|
-
' status_icon: if(status == "blocked", "\u{1F534}", if(status == "in-progress", "\u{1F528}", "\u26AA"))',
|
|
1603
|
-
"views:",
|
|
1604
|
-
" - type: table",
|
|
1605
|
-
" name: By Project",
|
|
1606
|
-
" groupBy:",
|
|
1607
|
-
" property: project",
|
|
1608
|
-
" direction: ASC",
|
|
1609
|
-
" order:",
|
|
1610
|
-
" - formula.status_icon",
|
|
1611
|
-
" - file.name",
|
|
1612
|
-
" - status",
|
|
1613
|
-
" - owner",
|
|
1614
|
-
" - priority"
|
|
1615
|
-
].join("\n"),
|
|
1616
|
-
"backlog.base": [
|
|
1617
|
-
"filters:",
|
|
1618
|
-
" and:",
|
|
1619
|
-
' - file.inFolder("backlog")',
|
|
1620
|
-
"views:",
|
|
1621
|
-
" - type: table",
|
|
1622
|
-
" name: Backlog",
|
|
1623
|
-
" order:",
|
|
1624
|
-
" - file.name",
|
|
1625
|
-
" - source",
|
|
1626
|
-
" - project",
|
|
1627
|
-
" - file.ctime"
|
|
1628
|
-
].join("\n")
|
|
1629
|
-
};
|
|
1630
|
-
for (const [filename, content] of Object.entries(basesFiles)) {
|
|
1631
|
-
const filePath = path4.join(vaultPath, filename);
|
|
1632
|
-
if (!fs4.existsSync(filePath)) {
|
|
1633
|
-
fs4.writeFileSync(filePath, content);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
/**
|
|
1638
|
-
* Load an existing vault
|
|
1639
|
-
*/
|
|
1640
|
-
async load() {
|
|
1641
|
-
if (!hasQmd()) {
|
|
1642
|
-
throw new QmdUnavailableError();
|
|
1643
|
-
}
|
|
1644
|
-
const vaultPath = this.config.path;
|
|
1645
|
-
const configPath = path4.join(vaultPath, CONFIG_FILE);
|
|
1646
|
-
if (!fs4.existsSync(configPath)) {
|
|
1647
|
-
throw new Error(`Not a ClawVault: ${vaultPath} (missing ${CONFIG_FILE})`);
|
|
1648
|
-
}
|
|
1649
|
-
const meta = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
1650
|
-
this.config.name = meta.name;
|
|
1651
|
-
this.config.categories = meta.categories;
|
|
1652
|
-
this.config.qmdCollection = meta.qmdCollection;
|
|
1653
|
-
this.config.qmdRoot = meta.qmdRoot;
|
|
1654
|
-
if (!meta.qmdCollection || !meta.qmdRoot) {
|
|
1655
|
-
meta.qmdCollection = meta.qmdCollection || meta.name;
|
|
1656
|
-
meta.qmdRoot = meta.qmdRoot || this.config.path;
|
|
1657
|
-
fs4.writeFileSync(configPath, JSON.stringify(meta, null, 2));
|
|
1658
|
-
}
|
|
1659
|
-
this.applyQmdConfig(meta);
|
|
1660
|
-
await this.reindex();
|
|
1661
|
-
this.initialized = true;
|
|
1662
|
-
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Reindex all documents
|
|
1665
|
-
*/
|
|
1666
|
-
async reindex() {
|
|
1667
|
-
this.search.clear();
|
|
1668
|
-
const files = await (0, import_glob2.glob)("**/*.md", {
|
|
1669
|
-
cwd: this.config.path,
|
|
1670
|
-
ignore: ["**/node_modules/**", "**/.*", "**/ledger/archive/**"]
|
|
1671
|
-
});
|
|
1672
|
-
for (const file of files) {
|
|
1673
|
-
const doc = await this.loadDocument(file);
|
|
1674
|
-
if (doc) {
|
|
1675
|
-
this.search.addDocument(doc);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
await this.saveIndex();
|
|
1679
|
-
await this.syncMemoryGraphIndex();
|
|
1680
|
-
return this.search.size;
|
|
1681
|
-
}
|
|
1682
|
-
/**
|
|
1683
|
-
* Load a document from disk
|
|
1684
|
-
*/
|
|
1685
|
-
async loadDocument(relativePath) {
|
|
1686
|
-
try {
|
|
1687
|
-
const fullPath = path4.join(this.config.path, relativePath);
|
|
1688
|
-
const content = fs4.readFileSync(fullPath, "utf-8");
|
|
1689
|
-
const { data: frontmatter, content: body } = (0, import_gray_matter2.default)(content);
|
|
1690
|
-
const stats = fs4.statSync(fullPath);
|
|
1691
|
-
const parts = relativePath.split(path4.sep);
|
|
1692
|
-
const category = parts.length > 1 ? parts[0] : "root";
|
|
1693
|
-
const filename = path4.basename(relativePath, ".md");
|
|
1694
|
-
return {
|
|
1695
|
-
id: relativePath.replace(/\.md$/, ""),
|
|
1696
|
-
path: fullPath,
|
|
1697
|
-
category,
|
|
1698
|
-
title: frontmatter.title || filename,
|
|
1699
|
-
content: body,
|
|
1700
|
-
frontmatter,
|
|
1701
|
-
links: extractWikiLinks(body),
|
|
1702
|
-
tags: extractTags(body),
|
|
1703
|
-
modified: stats.mtime
|
|
1704
|
-
};
|
|
1705
|
-
} catch (err) {
|
|
1706
|
-
console.error(`Error loading ${relativePath}:`, err);
|
|
1707
|
-
return null;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
/**
|
|
1711
|
-
* Store a new document
|
|
1712
|
-
*/
|
|
1713
|
-
async store(options) {
|
|
1714
|
-
const {
|
|
1715
|
-
category,
|
|
1716
|
-
title,
|
|
1717
|
-
content,
|
|
1718
|
-
frontmatter = {},
|
|
1719
|
-
overwrite = false,
|
|
1720
|
-
qmdUpdate: triggerUpdate = false,
|
|
1721
|
-
qmdEmbed: triggerEmbed = false,
|
|
1722
|
-
qmdIndexName
|
|
1723
|
-
} = options;
|
|
1724
|
-
const filename = this.slugify(title) + ".md";
|
|
1725
|
-
const relativePath = path4.join(category, filename);
|
|
1726
|
-
const fullPath = path4.join(this.config.path, relativePath);
|
|
1727
|
-
if (fs4.existsSync(fullPath) && !overwrite) {
|
|
1728
|
-
throw new Error(`Document already exists: ${relativePath}. Use overwrite: true to replace.`);
|
|
1729
|
-
}
|
|
1730
|
-
const categoryPath = path4.join(this.config.path, category);
|
|
1731
|
-
if (!fs4.existsSync(categoryPath)) {
|
|
1732
|
-
fs4.mkdirSync(categoryPath, { recursive: true });
|
|
1733
|
-
}
|
|
1734
|
-
const fm = {
|
|
1735
|
-
title,
|
|
1736
|
-
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
1737
|
-
...frontmatter
|
|
1738
|
-
};
|
|
1739
|
-
const fileContent = import_gray_matter2.default.stringify(content, fm);
|
|
1740
|
-
fs4.writeFileSync(fullPath, fileContent);
|
|
1741
|
-
const doc = await this.loadDocument(relativePath);
|
|
1742
|
-
if (doc) {
|
|
1743
|
-
this.search.addDocument(doc);
|
|
1744
|
-
await this.saveIndex();
|
|
1745
|
-
await this.syncMemoryGraphIndex();
|
|
1746
|
-
}
|
|
1747
|
-
if (triggerUpdate || triggerEmbed) {
|
|
1748
|
-
qmdUpdate(this.getQmdCollection(), qmdIndexName);
|
|
1749
|
-
if (triggerEmbed) {
|
|
1750
|
-
qmdEmbed(this.getQmdCollection(), qmdIndexName);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
return doc;
|
|
1754
|
-
}
|
|
1755
|
-
/**
|
|
1756
|
-
* Quick store to inbox
|
|
1757
|
-
*/
|
|
1758
|
-
async capture(note, title) {
|
|
1759
|
-
const autoTitle = title || `note-${Date.now()}`;
|
|
1760
|
-
return this.store({
|
|
1761
|
-
category: "inbox",
|
|
1762
|
-
title: autoTitle,
|
|
1763
|
-
content: note
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Search the vault (BM25 via qmd)
|
|
1768
|
-
*/
|
|
1769
|
-
async find(query, options = {}) {
|
|
1770
|
-
return this.search.search(query, options);
|
|
1771
|
-
}
|
|
1772
|
-
/**
|
|
1773
|
-
* Semantic/vector search (via qmd vsearch)
|
|
1774
|
-
*/
|
|
1775
|
-
async vsearch(query, options = {}) {
|
|
1776
|
-
return this.search.vsearch(query, options);
|
|
1777
|
-
}
|
|
1778
|
-
/**
|
|
1779
|
-
* Combined search with query expansion (via qmd query)
|
|
1780
|
-
*/
|
|
1781
|
-
async query(query, options = {}) {
|
|
1782
|
-
return this.search.query(query, options);
|
|
1783
|
-
}
|
|
1784
|
-
/**
|
|
1785
|
-
* Get a document by ID or path
|
|
1786
|
-
*/
|
|
1787
|
-
async get(idOrPath) {
|
|
1788
|
-
const normalized = idOrPath.replace(/\.md$/, "");
|
|
1789
|
-
const docs = this.search.getAllDocuments();
|
|
1790
|
-
return docs.find((d) => d.id === normalized) || null;
|
|
1791
|
-
}
|
|
1792
|
-
/**
|
|
1793
|
-
* List documents in a category
|
|
1794
|
-
*/
|
|
1795
|
-
async list(category) {
|
|
1796
|
-
const docs = this.search.getAllDocuments();
|
|
1797
|
-
if (category) {
|
|
1798
|
-
return docs.filter((d) => d.category === category);
|
|
1799
|
-
}
|
|
1800
|
-
return docs;
|
|
1801
|
-
}
|
|
1802
|
-
/**
|
|
1803
|
-
* Sync vault to another location (for Obsidian on Windows, etc.)
|
|
1804
|
-
*/
|
|
1805
|
-
async sync(options) {
|
|
1806
|
-
const { target, deleteOrphans = false, dryRun = false } = options;
|
|
1807
|
-
const result = {
|
|
1808
|
-
copied: [],
|
|
1809
|
-
deleted: [],
|
|
1810
|
-
unchanged: [],
|
|
1811
|
-
errors: []
|
|
1812
|
-
};
|
|
1813
|
-
const sourceFiles = await (0, import_glob2.glob)("**/*.md", {
|
|
1814
|
-
cwd: this.config.path,
|
|
1815
|
-
ignore: ["**/node_modules/**"]
|
|
1816
|
-
});
|
|
1817
|
-
if (!dryRun && !fs4.existsSync(target)) {
|
|
1818
|
-
fs4.mkdirSync(target, { recursive: true });
|
|
1819
|
-
}
|
|
1820
|
-
for (const file of sourceFiles) {
|
|
1821
|
-
const sourcePath = path4.join(this.config.path, file);
|
|
1822
|
-
const targetPath = path4.join(target, file);
|
|
1823
|
-
try {
|
|
1824
|
-
const sourceStats = fs4.statSync(sourcePath);
|
|
1825
|
-
let shouldCopy = true;
|
|
1826
|
-
if (fs4.existsSync(targetPath)) {
|
|
1827
|
-
const targetStats = fs4.statSync(targetPath);
|
|
1828
|
-
if (sourceStats.mtime <= targetStats.mtime) {
|
|
1829
|
-
result.unchanged.push(file);
|
|
1830
|
-
shouldCopy = false;
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
if (shouldCopy) {
|
|
1834
|
-
if (!dryRun) {
|
|
1835
|
-
const targetDir = path4.dirname(targetPath);
|
|
1836
|
-
if (!fs4.existsSync(targetDir)) {
|
|
1837
|
-
fs4.mkdirSync(targetDir, { recursive: true });
|
|
1838
|
-
}
|
|
1839
|
-
fs4.copyFileSync(sourcePath, targetPath);
|
|
1840
|
-
}
|
|
1841
|
-
result.copied.push(file);
|
|
1842
|
-
}
|
|
1843
|
-
} catch (err) {
|
|
1844
|
-
result.errors.push(`${file}: ${err}`);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
if (deleteOrphans) {
|
|
1848
|
-
const targetFiles = await (0, import_glob2.glob)("**/*.md", { cwd: target });
|
|
1849
|
-
const sourceSet = new Set(sourceFiles);
|
|
1850
|
-
for (const file of targetFiles) {
|
|
1851
|
-
if (!sourceSet.has(file)) {
|
|
1852
|
-
if (!dryRun) {
|
|
1853
|
-
fs4.unlinkSync(path4.join(target, file));
|
|
1854
|
-
}
|
|
1855
|
-
result.deleted.push(file);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
return result;
|
|
1860
|
-
}
|
|
1861
|
-
/**
|
|
1862
|
-
* Get vault statistics
|
|
1863
|
-
*/
|
|
1864
|
-
async stats() {
|
|
1865
|
-
const docs = this.search.getAllDocuments();
|
|
1866
|
-
const categories = {};
|
|
1867
|
-
const allTags = /* @__PURE__ */ new Set();
|
|
1868
|
-
let totalLinks = 0;
|
|
1869
|
-
for (const doc of docs) {
|
|
1870
|
-
categories[doc.category] = (categories[doc.category] || 0) + 1;
|
|
1871
|
-
totalLinks += doc.links.length;
|
|
1872
|
-
doc.tags.forEach((t) => allTags.add(t));
|
|
1873
|
-
}
|
|
1874
|
-
return {
|
|
1875
|
-
documents: docs.length,
|
|
1876
|
-
categories,
|
|
1877
|
-
links: totalLinks,
|
|
1878
|
-
tags: [...allTags].sort()
|
|
1879
|
-
};
|
|
1880
|
-
}
|
|
1881
|
-
/**
|
|
1882
|
-
* Get all categories
|
|
1883
|
-
*/
|
|
1884
|
-
getCategories() {
|
|
1885
|
-
return this.config.categories;
|
|
1886
|
-
}
|
|
1887
|
-
/**
|
|
1888
|
-
* Check if vault is initialized
|
|
1889
|
-
*/
|
|
1890
|
-
isInitialized() {
|
|
1891
|
-
return this.initialized;
|
|
1892
|
-
}
|
|
1893
|
-
/**
|
|
1894
|
-
* Get vault path
|
|
1895
|
-
*/
|
|
1896
|
-
getPath() {
|
|
1897
|
-
return this.config.path;
|
|
1898
|
-
}
|
|
1899
|
-
/**
|
|
1900
|
-
* Get vault name
|
|
1901
|
-
*/
|
|
1902
|
-
getName() {
|
|
1903
|
-
return this.config.name;
|
|
1904
|
-
}
|
|
1905
|
-
/**
|
|
1906
|
-
* Get qmd collection name
|
|
1907
|
-
*/
|
|
1908
|
-
getQmdCollection() {
|
|
1909
|
-
return this.config.qmdCollection || this.config.name;
|
|
1910
|
-
}
|
|
1911
|
-
/**
|
|
1912
|
-
* Get qmd collection root
|
|
1913
|
-
*/
|
|
1914
|
-
getQmdRoot() {
|
|
1915
|
-
return this.config.qmdRoot || this.config.path;
|
|
1916
|
-
}
|
|
1917
|
-
// === Memory Type System ===
|
|
1918
|
-
/**
|
|
1919
|
-
* Store a memory with type classification
|
|
1920
|
-
* Automatically routes to correct category based on type
|
|
1921
|
-
*/
|
|
1922
|
-
async remember(type, title, content, frontmatter = {}) {
|
|
1923
|
-
const category = TYPE_TO_CATEGORY[type];
|
|
1924
|
-
return this.store({
|
|
1925
|
-
category,
|
|
1926
|
-
title,
|
|
1927
|
-
content,
|
|
1928
|
-
frontmatter: { ...frontmatter, memoryType: type }
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
// === Handoff System ===
|
|
1932
|
-
/**
|
|
1933
|
-
* Create a session handoff document
|
|
1934
|
-
* Call this before context death or long pauses
|
|
1935
|
-
*/
|
|
1936
|
-
async createHandoff(handoff) {
|
|
1937
|
-
const now = /* @__PURE__ */ new Date();
|
|
1938
|
-
const dateStr = now.toISOString().split("T")[0];
|
|
1939
|
-
const timeStr = now.toISOString().split("T")[1].slice(0, 5).replace(":", "");
|
|
1940
|
-
const fullHandoff = {
|
|
1941
|
-
...handoff,
|
|
1942
|
-
created: now.toISOString()
|
|
1943
|
-
};
|
|
1944
|
-
const content = this.formatHandoff(fullHandoff);
|
|
1945
|
-
const frontmatter = {
|
|
1946
|
-
type: "handoff",
|
|
1947
|
-
workingOn: handoff.workingOn,
|
|
1948
|
-
blocked: handoff.blocked,
|
|
1949
|
-
nextSteps: handoff.nextSteps
|
|
1950
|
-
};
|
|
1951
|
-
if (handoff.sessionKey) frontmatter.sessionKey = handoff.sessionKey;
|
|
1952
|
-
if (handoff.feeling) frontmatter.feeling = handoff.feeling;
|
|
1953
|
-
if (handoff.decisions) frontmatter.decisions = handoff.decisions;
|
|
1954
|
-
if (handoff.openQuestions) frontmatter.openQuestions = handoff.openQuestions;
|
|
1955
|
-
return this.store({
|
|
1956
|
-
category: "handoffs",
|
|
1957
|
-
title: `handoff-${dateStr}-${timeStr}`,
|
|
1958
|
-
content,
|
|
1959
|
-
frontmatter
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
1962
|
-
/**
|
|
1963
|
-
* Format handoff as readable markdown
|
|
1964
|
-
*/
|
|
1965
|
-
formatHandoff(h) {
|
|
1966
|
-
let md = `# Session Handoff
|
|
1967
|
-
|
|
1968
|
-
`;
|
|
1969
|
-
md += `**Created:** ${h.created}
|
|
1970
|
-
`;
|
|
1971
|
-
if (h.sessionKey) md += `**Session:** ${h.sessionKey}
|
|
1972
|
-
`;
|
|
1973
|
-
if (h.feeling) md += `**Feeling:** ${h.feeling}
|
|
1974
|
-
`;
|
|
1975
|
-
md += `
|
|
1976
|
-
`;
|
|
1977
|
-
md += `## Working On
|
|
1978
|
-
`;
|
|
1979
|
-
h.workingOn.forEach((w) => md += `- ${w}
|
|
1980
|
-
`);
|
|
1981
|
-
md += `
|
|
1982
|
-
`;
|
|
1983
|
-
md += `## Blocked
|
|
1984
|
-
`;
|
|
1985
|
-
if (h.blocked.length === 0) md += `- Nothing currently blocked
|
|
1986
|
-
`;
|
|
1987
|
-
else h.blocked.forEach((b) => md += `- ${b}
|
|
1988
|
-
`);
|
|
1989
|
-
md += `
|
|
1990
|
-
`;
|
|
1991
|
-
md += `## Next Steps
|
|
1992
|
-
`;
|
|
1993
|
-
h.nextSteps.forEach((n) => md += `- ${n}
|
|
1994
|
-
`);
|
|
1995
|
-
if (h.decisions && h.decisions.length > 0) {
|
|
1996
|
-
md += `
|
|
1997
|
-
## Decisions Made
|
|
1998
|
-
`;
|
|
1999
|
-
h.decisions.forEach((d) => md += `- ${d}
|
|
2000
|
-
`);
|
|
2001
|
-
}
|
|
2002
|
-
if (h.openQuestions && h.openQuestions.length > 0) {
|
|
2003
|
-
md += `
|
|
2004
|
-
## Open Questions
|
|
2005
|
-
`;
|
|
2006
|
-
h.openQuestions.forEach((q) => md += `- ${q}
|
|
2007
|
-
`);
|
|
2008
|
-
}
|
|
2009
|
-
return md;
|
|
2010
|
-
}
|
|
2011
|
-
// === Session Recap (Bootstrap Hook) ===
|
|
2012
|
-
/**
|
|
2013
|
-
* Generate a session recap - who I was
|
|
2014
|
-
* Call this on bootstrap to restore context
|
|
2015
|
-
*/
|
|
2016
|
-
async generateRecap(options = {}) {
|
|
2017
|
-
const { handoffLimit = 3, brief = false } = options;
|
|
2018
|
-
const handoffDocs = await this.list("handoffs");
|
|
2019
|
-
const recentHandoffs = handoffDocs.sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, handoffLimit).map((doc) => this.parseHandoff(doc));
|
|
2020
|
-
const projectDocs = await this.list("projects");
|
|
2021
|
-
const activeProjects = projectDocs.filter((d) => d.frontmatter.status !== "completed" && d.frontmatter.status !== "archived").map((d) => d.title);
|
|
2022
|
-
const commitmentDocs = await this.list("commitments");
|
|
2023
|
-
const pendingCommitments = commitmentDocs.filter((d) => d.frontmatter.status !== "done").map((d) => d.title);
|
|
2024
|
-
const decisionDocs = await this.list("decisions");
|
|
2025
|
-
const recentDecisions = decisionDocs.sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, brief ? 3 : 5).map((d) => d.title);
|
|
2026
|
-
const lessonDocs = await this.list("lessons");
|
|
2027
|
-
const recentLessons = lessonDocs.sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, brief ? 3 : 5).map((d) => d.title);
|
|
2028
|
-
let keyRelationships = [];
|
|
2029
|
-
if (!brief) {
|
|
2030
|
-
const peopleDocs = await this.list("people");
|
|
2031
|
-
keyRelationships = peopleDocs.filter((d) => d.frontmatter.importance === "high" || d.frontmatter.role).map((d) => `${d.title}${d.frontmatter.role ? ` (${d.frontmatter.role})` : ""}`);
|
|
2032
|
-
}
|
|
2033
|
-
const feelings = recentHandoffs.map((h) => h.feeling).filter(Boolean);
|
|
2034
|
-
const emotionalArc = feelings.length > 0 ? feelings.join(" \u2192 ") : void 0;
|
|
2035
|
-
return {
|
|
2036
|
-
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2037
|
-
recentHandoffs,
|
|
2038
|
-
activeProjects,
|
|
2039
|
-
pendingCommitments,
|
|
2040
|
-
recentDecisions,
|
|
2041
|
-
recentLessons,
|
|
2042
|
-
keyRelationships,
|
|
2043
|
-
emotionalArc
|
|
2044
|
-
};
|
|
2045
|
-
}
|
|
2046
|
-
/**
|
|
2047
|
-
* Format recap as readable markdown for injection
|
|
2048
|
-
*/
|
|
2049
|
-
formatRecap(recap, options = {}) {
|
|
2050
|
-
const { brief = false } = options;
|
|
2051
|
-
let md = `# Who I Was
|
|
2052
|
-
|
|
2053
|
-
`;
|
|
2054
|
-
md += `*Generated: ${recap.generated}*
|
|
2055
|
-
|
|
2056
|
-
`;
|
|
2057
|
-
if (recap.emotionalArc) {
|
|
2058
|
-
md += `**Emotional arc:** ${recap.emotionalArc}
|
|
2059
|
-
|
|
2060
|
-
`;
|
|
2061
|
-
}
|
|
2062
|
-
if (recap.recentHandoffs.length > 0) {
|
|
2063
|
-
md += `## Recent Sessions
|
|
2064
|
-
`;
|
|
2065
|
-
for (const h of recap.recentHandoffs) {
|
|
2066
|
-
if (brief) {
|
|
2067
|
-
md += `- **${h.created.split("T")[0]}:** ${h.workingOn.slice(0, 2).join(", ")}`;
|
|
2068
|
-
if (h.nextSteps.length > 0) md += ` \u2192 ${h.nextSteps[0]}`;
|
|
2069
|
-
md += `
|
|
2070
|
-
`;
|
|
2071
|
-
} else {
|
|
2072
|
-
md += `
|
|
2073
|
-
### ${h.created.split("T")[0]}
|
|
2074
|
-
`;
|
|
2075
|
-
md += `**Working on:** ${h.workingOn.join(", ")}
|
|
2076
|
-
`;
|
|
2077
|
-
if (h.blocked.length > 0) md += `**Blocked:** ${h.blocked.join(", ")}
|
|
2078
|
-
`;
|
|
2079
|
-
md += `**Next:** ${h.nextSteps.join(", ")}
|
|
2080
|
-
`;
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
md += `
|
|
2084
|
-
`;
|
|
2085
|
-
}
|
|
2086
|
-
if (recap.activeProjects.length > 0) {
|
|
2087
|
-
md += `## Active Projects
|
|
2088
|
-
`;
|
|
2089
|
-
recap.activeProjects.forEach((p) => md += `- ${p}
|
|
2090
|
-
`);
|
|
2091
|
-
md += `
|
|
2092
|
-
`;
|
|
2093
|
-
}
|
|
2094
|
-
if (recap.pendingCommitments.length > 0) {
|
|
2095
|
-
md += `## Pending Commitments
|
|
2096
|
-
`;
|
|
2097
|
-
recap.pendingCommitments.forEach((c) => md += `- ${c}
|
|
2098
|
-
`);
|
|
2099
|
-
md += `
|
|
2100
|
-
`;
|
|
2101
|
-
}
|
|
2102
|
-
if (recap.recentDecisions && recap.recentDecisions.length > 0) {
|
|
2103
|
-
md += `## Recent Decisions
|
|
2104
|
-
`;
|
|
2105
|
-
recap.recentDecisions.forEach((d) => md += `- ${d}
|
|
2106
|
-
`);
|
|
2107
|
-
md += `
|
|
2108
|
-
`;
|
|
2109
|
-
}
|
|
2110
|
-
if (recap.recentLessons.length > 0) {
|
|
2111
|
-
md += `## Recent Lessons
|
|
2112
|
-
`;
|
|
2113
|
-
recap.recentLessons.forEach((l) => md += `- ${l}
|
|
2114
|
-
`);
|
|
2115
|
-
md += `
|
|
2116
|
-
`;
|
|
2117
|
-
}
|
|
2118
|
-
if (!brief && recap.keyRelationships.length > 0) {
|
|
2119
|
-
md += `## Key People
|
|
2120
|
-
`;
|
|
2121
|
-
recap.keyRelationships.forEach((r) => md += `- ${r}
|
|
2122
|
-
`);
|
|
2123
|
-
}
|
|
2124
|
-
return md;
|
|
2125
|
-
}
|
|
2126
|
-
/**
|
|
2127
|
-
* Parse a handoff document back into structured form
|
|
2128
|
-
*/
|
|
2129
|
-
parseHandoff(doc) {
|
|
2130
|
-
return {
|
|
2131
|
-
created: doc.frontmatter.date || doc.modified.toISOString(),
|
|
2132
|
-
sessionKey: doc.frontmatter.sessionKey,
|
|
2133
|
-
workingOn: doc.frontmatter.workingOn || [],
|
|
2134
|
-
blocked: doc.frontmatter.blocked || [],
|
|
2135
|
-
nextSteps: doc.frontmatter.nextSteps || [],
|
|
2136
|
-
decisions: doc.frontmatter.decisions,
|
|
2137
|
-
openQuestions: doc.frontmatter.openQuestions,
|
|
2138
|
-
feeling: doc.frontmatter.feeling
|
|
2139
|
-
};
|
|
2140
|
-
}
|
|
2141
|
-
// === Private helpers ===
|
|
2142
|
-
applyQmdConfig(meta) {
|
|
2143
|
-
const collection = meta?.qmdCollection || this.config.qmdCollection || this.config.name;
|
|
2144
|
-
const root = meta?.qmdRoot || this.config.qmdRoot || this.config.path;
|
|
2145
|
-
this.config.qmdCollection = collection;
|
|
2146
|
-
this.config.qmdRoot = root;
|
|
2147
|
-
this.search.setVaultPath(this.config.path);
|
|
2148
|
-
this.search.setCollection(collection);
|
|
2149
|
-
this.search.setCollectionRoot(root);
|
|
2150
|
-
}
|
|
2151
|
-
slugify(text) {
|
|
2152
|
-
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").trim();
|
|
2153
|
-
}
|
|
2154
|
-
async saveIndex() {
|
|
2155
|
-
const indexPath = path4.join(this.config.path, INDEX_FILE);
|
|
2156
|
-
const data = this.search.export();
|
|
2157
|
-
fs4.writeFileSync(indexPath, JSON.stringify(data, null, 2));
|
|
2158
|
-
const configPath = path4.join(this.config.path, CONFIG_FILE);
|
|
2159
|
-
if (fs4.existsSync(configPath)) {
|
|
2160
|
-
const meta = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
2161
|
-
meta.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2162
|
-
meta.documentCount = this.search.size;
|
|
2163
|
-
fs4.writeFileSync(configPath, JSON.stringify(meta, null, 2));
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
async createTemplates() {
|
|
2167
|
-
const templatesPath = path4.join(this.config.path, "templates");
|
|
2168
|
-
if (!fs4.existsSync(templatesPath)) {
|
|
2169
|
-
fs4.mkdirSync(templatesPath, { recursive: true });
|
|
2170
|
-
}
|
|
2171
|
-
const moduleDir = path4.dirname((0, import_url.fileURLToPath)(import_meta.url));
|
|
2172
|
-
const candidates = [
|
|
2173
|
-
path4.resolve(moduleDir, "../templates"),
|
|
2174
|
-
path4.resolve(moduleDir, "../../templates")
|
|
2175
|
-
];
|
|
2176
|
-
const builtinDir = candidates.find((dir) => fs4.existsSync(dir) && fs4.statSync(dir).isDirectory());
|
|
2177
|
-
if (!builtinDir) return;
|
|
2178
|
-
for (const entry of fs4.readdirSync(builtinDir, { withFileTypes: true })) {
|
|
2179
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2180
|
-
if (entry.name === "daily.md") continue;
|
|
2181
|
-
const sourcePath = path4.join(builtinDir, entry.name);
|
|
2182
|
-
const targetPath = path4.join(templatesPath, entry.name);
|
|
2183
|
-
if (!fs4.existsSync(targetPath)) {
|
|
2184
|
-
fs4.copyFileSync(sourcePath, targetPath);
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
async createWelcomeNote() {
|
|
2189
|
-
if (!this.config.categories.includes("inbox")) return;
|
|
2190
|
-
const inboxPath = path4.join(this.config.path, "inbox", "welcome.md");
|
|
2191
|
-
if (fs4.existsSync(inboxPath)) return;
|
|
2192
|
-
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2193
|
-
const content = `---
|
|
2194
|
-
title: "Welcome to ${this.config.name}"
|
|
2195
|
-
date: ${now}
|
|
2196
|
-
type: fact
|
|
2197
|
-
tags: [welcome, getting-started]
|
|
2198
|
-
---
|
|
2199
|
-
|
|
2200
|
-
# Welcome to ${this.config.name}
|
|
2201
|
-
|
|
2202
|
-
Your vault is ready. Here's what you can do:
|
|
2203
|
-
|
|
2204
|
-
## Quick Start
|
|
2205
|
-
|
|
2206
|
-
- **Capture a thought:** \`clawvault capture "your note here"\`
|
|
2207
|
-
- **Store structured memory:** \`clawvault store --category decisions --title "My Choice" --content "..."\`
|
|
2208
|
-
- **Search your vault:** \`clawvault search "query"\`
|
|
2209
|
-
- **See your knowledge graph:** \`clawvault graph\`
|
|
2210
|
-
- **Get context for a topic:** \`clawvault context "topic"\`
|
|
2211
|
-
|
|
2212
|
-
## Vault Structure
|
|
2213
|
-
|
|
2214
|
-
Your vault organizes memories by type \u2014 decisions, lessons, people, projects, and more.
|
|
2215
|
-
Each category is a folder. Each memory is a markdown file with frontmatter.
|
|
2216
|
-
|
|
2217
|
-
## Observational Memory
|
|
2218
|
-
|
|
2219
|
-
When connected to an AI agent (like OpenClaw), your vault can automatically observe
|
|
2220
|
-
conversations and extract important memories \u2014 decisions, lessons, commitments \u2014 without
|
|
2221
|
-
manual effort.
|
|
2222
|
-
|
|
2223
|
-
## Wiki-Links
|
|
2224
|
-
|
|
2225
|
-
Use \`[[double brackets]]\` to link between notes. Your memory graph tracks these
|
|
2226
|
-
connections, building a knowledge network that grows with you.
|
|
2227
|
-
|
|
2228
|
-
---
|
|
2229
|
-
|
|
2230
|
-
*Delete this file anytime. It's just here to say hello.*
|
|
2231
|
-
`;
|
|
2232
|
-
fs4.writeFileSync(inboxPath, content);
|
|
2233
|
-
}
|
|
2234
|
-
async syncMemoryGraphIndex(options = {}) {
|
|
2235
|
-
try {
|
|
2236
|
-
await buildOrUpdateMemoryGraphIndex(this.config.path, options);
|
|
2237
|
-
} catch {
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
generateReadme() {
|
|
2241
|
-
const coreCategories = this.config.categories.filter((c) => !["templates", "tasks", "backlog"].includes(c));
|
|
2242
|
-
const workCategories = this.config.categories.filter((c) => ["tasks", "backlog"].includes(c));
|
|
2243
|
-
return `# ${this.config.name}
|
|
2244
|
-
|
|
2245
|
-
An elephant never forgets.
|
|
2246
|
-
|
|
2247
|
-
## Structure
|
|
2248
|
-
|
|
2249
|
-
### Memory Categories
|
|
2250
|
-
${coreCategories.map((c) => `- \`${c}/\` \u2014 ${this.getCategoryDescription(c)}`).join("\n")}
|
|
2251
|
-
|
|
2252
|
-
### Work Tracking
|
|
2253
|
-
${workCategories.map((c) => `- \`${c}/\` \u2014 ${this.getCategoryDescription(c)}`).join("\n")}
|
|
2254
|
-
|
|
2255
|
-
### Observational Memory
|
|
2256
|
-
- \`ledger/raw/\` \u2014 Raw session transcripts (source of truth)
|
|
2257
|
-
- \`ledger/observations/\` \u2014 Compressed observations with importance scores
|
|
2258
|
-
- \`ledger/reflections/\` \u2014 Weekly reflection summaries
|
|
2259
|
-
|
|
2260
|
-
## Quick Reference
|
|
2261
|
-
|
|
2262
|
-
\`\`\`bash
|
|
2263
|
-
# Capture a thought
|
|
2264
|
-
clawvault capture "important insight about X"
|
|
2265
|
-
|
|
2266
|
-
# Store structured memory
|
|
2267
|
-
clawvault store --category decisions --title "Choice" --content "We chose X because..."
|
|
2268
|
-
|
|
2269
|
-
# Search
|
|
2270
|
-
clawvault search "query"
|
|
2271
|
-
clawvault vsearch "semantic query" # vector search
|
|
2272
|
-
|
|
2273
|
-
# Knowledge graph
|
|
2274
|
-
clawvault graph # vault stats
|
|
2275
|
-
clawvault context "topic" # graph-aware context retrieval
|
|
2276
|
-
|
|
2277
|
-
# Session lifecycle
|
|
2278
|
-
clawvault checkpoint --working-on "task"
|
|
2279
|
-
clawvault sleep "what I did" --next "what's next"
|
|
2280
|
-
clawvault wake # restore context on startup
|
|
2281
|
-
\`\`\`
|
|
2282
|
-
|
|
2283
|
-
---
|
|
2284
|
-
|
|
2285
|
-
*Managed by [ClawVault](https://clawvault.dev)*
|
|
2286
|
-
`;
|
|
2287
|
-
}
|
|
2288
|
-
getCategoryDescription(category) {
|
|
2289
|
-
const descriptions = {
|
|
2290
|
-
// Memory type categories (Benthic's taxonomy)
|
|
2291
|
-
facts: "Raw information, data points, things that are true",
|
|
2292
|
-
feelings: "Emotional states, reactions, energy levels",
|
|
2293
|
-
decisions: "Choices made with context and reasoning",
|
|
2294
|
-
rules: "Injectable operational constraints, guardrails, and runbooks",
|
|
2295
|
-
lessons: "What I learned, insights, patterns observed",
|
|
2296
|
-
commitments: "Promises, goals, obligations to fulfill",
|
|
2297
|
-
preferences: "Likes, dislikes, how I want things",
|
|
2298
|
-
people: "Relationships, one file per person",
|
|
2299
|
-
projects: "Active work, ventures, ongoing efforts",
|
|
2300
|
-
// System categories
|
|
2301
|
-
handoffs: "Session bridges \u2014 what I was doing, what comes next",
|
|
2302
|
-
transcripts: "Session summaries and logs",
|
|
2303
|
-
goals: "Long-term and short-term objectives",
|
|
2304
|
-
patterns: "Recurring behaviors (\u2192 lessons)",
|
|
2305
|
-
inbox: "Quick capture \u2192 process later",
|
|
2306
|
-
templates: "Templates for each document type",
|
|
2307
|
-
agents: "Other agents \u2014 capabilities, trust levels, coordination notes",
|
|
2308
|
-
research: "Deep dives, analysis, reference material",
|
|
2309
|
-
tasks: "Active work items with status and context",
|
|
2310
|
-
backlog: "Future work \u2014 ideas and tasks not yet started"
|
|
2311
|
-
};
|
|
2312
|
-
return descriptions[category] || category;
|
|
2313
|
-
}
|
|
2314
|
-
};
|
|
2315
|
-
|
|
2316
|
-
// src/lib/observation-reader.ts
|
|
2317
|
-
var fs5 = __toESM(require("fs"), 1);
|
|
2318
|
-
function readObservations(vaultPath, days = 7) {
|
|
2319
|
-
const normalizedDays = Number.isFinite(days) ? Math.max(0, Math.floor(days)) : 0;
|
|
2320
|
-
if (normalizedDays === 0) {
|
|
2321
|
-
return "";
|
|
2322
|
-
}
|
|
2323
|
-
const files = listObservationFiles(vaultPath, {
|
|
2324
|
-
includeLegacy: true,
|
|
2325
|
-
includeArchive: false,
|
|
2326
|
-
dedupeByDate: true
|
|
2327
|
-
}).sort((left, right) => right.date.localeCompare(left.date)).slice(0, normalizedDays);
|
|
2328
|
-
if (files.length === 0) {
|
|
2329
|
-
return "";
|
|
2330
|
-
}
|
|
2331
|
-
return files.map((entry) => fs5.readFileSync(entry.path, "utf-8").trim()).filter(Boolean).join("\n\n").trim();
|
|
2332
|
-
}
|
|
2333
|
-
function parseObservationLines(markdown) {
|
|
2334
|
-
return parseObservationMarkdown(markdown).map((record) => ({
|
|
2335
|
-
type: record.type,
|
|
2336
|
-
confidence: record.confidence,
|
|
2337
|
-
importance: record.importance,
|
|
2338
|
-
content: record.content,
|
|
2339
|
-
date: record.date,
|
|
2340
|
-
format: record.format,
|
|
2341
|
-
priority: record.priority
|
|
2342
|
-
}));
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
// src/lib/token-counter.ts
|
|
2346
|
-
function estimateTokens(text) {
|
|
2347
|
-
if (!text) {
|
|
2348
|
-
return 0;
|
|
2349
|
-
}
|
|
2350
|
-
return Math.ceil(text.length / 4);
|
|
2351
|
-
}
|
|
2352
|
-
function fitWithinBudget(items, budget) {
|
|
2353
|
-
if (!Number.isFinite(budget) || budget <= 0) {
|
|
2354
|
-
return [];
|
|
2355
|
-
}
|
|
2356
|
-
const sorted = items.map((item, index) => ({ ...item, index })).sort((a, b) => {
|
|
2357
|
-
if (a.priority !== b.priority) {
|
|
2358
|
-
return a.priority - b.priority;
|
|
2359
|
-
}
|
|
2360
|
-
return a.index - b.index;
|
|
2361
|
-
});
|
|
2362
|
-
let remaining = Math.floor(budget);
|
|
2363
|
-
const fitted = [];
|
|
2364
|
-
for (const item of sorted) {
|
|
2365
|
-
if (!item.text.trim()) {
|
|
2366
|
-
continue;
|
|
2367
|
-
}
|
|
2368
|
-
const cost = estimateTokens(item.text);
|
|
2369
|
-
if (cost <= remaining) {
|
|
2370
|
-
fitted.push({ text: item.text, source: item.source });
|
|
2371
|
-
remaining -= cost;
|
|
2372
|
-
}
|
|
2373
|
-
if (remaining <= 0) {
|
|
2374
|
-
break;
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
return fitted;
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
// src/lib/context-profile.ts
|
|
2381
|
-
var INCIDENT_PROMPT_RE = /\b(outage|incident|sev[1-4]|p[0-3]|broken|failure|urgent|rollback|hotfix|degraded)\b/i;
|
|
2382
|
-
var PLANNING_PROMPT_RE = /\b(plan|planning|design|architecture|roadmap|proposal|spec|migrate|migration|approach)\b/i;
|
|
2383
|
-
var HANDOFF_PROMPT_RE = /\b(resume|continue|handoff|pick up|where (did|was) i|last session)\b/i;
|
|
2384
|
-
function inferContextProfile(task) {
|
|
2385
|
-
const normalizedTask = task.trim();
|
|
2386
|
-
if (!normalizedTask) {
|
|
2387
|
-
return "default";
|
|
2388
|
-
}
|
|
2389
|
-
if (INCIDENT_PROMPT_RE.test(normalizedTask)) return "incident";
|
|
2390
|
-
if (HANDOFF_PROMPT_RE.test(normalizedTask)) return "handoff";
|
|
2391
|
-
if (PLANNING_PROMPT_RE.test(normalizedTask)) return "planning";
|
|
2392
|
-
return "default";
|
|
2393
|
-
}
|
|
2394
|
-
function normalizeContextProfileInput(profile) {
|
|
2395
|
-
if (profile === "planning" || profile === "incident" || profile === "handoff" || profile === "auto") {
|
|
2396
|
-
return profile;
|
|
2397
|
-
}
|
|
2398
|
-
return "default";
|
|
2399
|
-
}
|
|
2400
|
-
function resolveContextProfile(profile, task) {
|
|
2401
|
-
const normalized = normalizeContextProfileInput(profile);
|
|
2402
|
-
if (normalized === "auto") {
|
|
2403
|
-
return inferContextProfile(task);
|
|
2404
|
-
}
|
|
2405
|
-
return normalized;
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
// src/commands/context.ts
|
|
2409
|
-
var DEFAULT_LIMIT = 5;
|
|
2410
|
-
var MAX_SNIPPET_LENGTH = 320;
|
|
2411
|
-
var OBSERVATION_LOOKBACK_DAYS = 7;
|
|
2412
|
-
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
2413
|
-
"a",
|
|
2414
|
-
"an",
|
|
2415
|
-
"and",
|
|
2416
|
-
"are",
|
|
2417
|
-
"as",
|
|
2418
|
-
"at",
|
|
2419
|
-
"be",
|
|
2420
|
-
"by",
|
|
2421
|
-
"for",
|
|
2422
|
-
"from",
|
|
2423
|
-
"how",
|
|
2424
|
-
"in",
|
|
2425
|
-
"is",
|
|
2426
|
-
"it",
|
|
2427
|
-
"of",
|
|
2428
|
-
"on",
|
|
2429
|
-
"or",
|
|
2430
|
-
"that",
|
|
2431
|
-
"the",
|
|
2432
|
-
"this",
|
|
2433
|
-
"to",
|
|
2434
|
-
"was",
|
|
2435
|
-
"were",
|
|
2436
|
-
"what",
|
|
2437
|
-
"when",
|
|
2438
|
-
"where",
|
|
2439
|
-
"who",
|
|
2440
|
-
"why",
|
|
2441
|
-
"with",
|
|
2442
|
-
"you",
|
|
2443
|
-
"your"
|
|
2444
|
-
]);
|
|
2445
|
-
function formatRelativeAge(date, now = Date.now()) {
|
|
2446
|
-
const ageMs = Math.max(0, now - date.getTime());
|
|
2447
|
-
const days = Math.floor(ageMs / (24 * 60 * 60 * 1e3));
|
|
2448
|
-
if (days === 0) return "today";
|
|
2449
|
-
if (days < 7) return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
2450
|
-
const weeks = Math.floor(days / 7);
|
|
2451
|
-
if (weeks < 5) return `${weeks} week${weeks === 1 ? "" : "s"} ago`;
|
|
2452
|
-
const months = Math.floor(days / 30);
|
|
2453
|
-
if (months < 12) return `${months} month${months === 1 ? "" : "s"} ago`;
|
|
2454
|
-
const years = Math.floor(days / 365);
|
|
2455
|
-
return `${years} year${years === 1 ? "" : "s"} ago`;
|
|
2456
|
-
}
|
|
2457
|
-
function normalizeSnippet(result) {
|
|
2458
|
-
const source = (result.snippet || result.document.content || "").trim();
|
|
2459
|
-
if (!source) return "No snippet available.";
|
|
2460
|
-
return source.replace(/\s+/g, " ").slice(0, MAX_SNIPPET_LENGTH);
|
|
2461
|
-
}
|
|
2462
|
-
function formatContextMarkdown(task, entries) {
|
|
2463
|
-
let output = `## Relevant Context for: ${task}
|
|
2464
|
-
|
|
2465
|
-
`;
|
|
2466
|
-
if (entries.length === 0) {
|
|
2467
|
-
output += "_No relevant context found._\n";
|
|
2468
|
-
return output;
|
|
2469
|
-
}
|
|
2470
|
-
for (const entry of entries) {
|
|
2471
|
-
output += `### ${entry.title} (${entry.source}, score: ${entry.score.toFixed(2)}, ${entry.age})
|
|
2472
|
-
`;
|
|
2473
|
-
output += `${entry.snippet}
|
|
2474
|
-
|
|
2475
|
-
`;
|
|
2476
|
-
}
|
|
2477
|
-
return output.trimEnd();
|
|
2478
|
-
}
|
|
2479
|
-
var PROFILE_ORDERING = {
|
|
2480
|
-
default: {
|
|
2481
|
-
order: ["structural", "daily", "search", "graph", "potential", "contextual"],
|
|
2482
|
-
caps: {}
|
|
2483
|
-
},
|
|
2484
|
-
planning: {
|
|
2485
|
-
order: ["search", "graph", "structural", "potential", "daily", "contextual"],
|
|
2486
|
-
caps: { observation: 12, graph: 12 }
|
|
2487
|
-
},
|
|
2488
|
-
incident: {
|
|
2489
|
-
order: ["structural", "search", "potential", "daily", "graph", "contextual"],
|
|
2490
|
-
caps: { observation: 20, graph: 8 }
|
|
2491
|
-
},
|
|
2492
|
-
handoff: {
|
|
2493
|
-
order: ["daily", "structural", "potential", "search", "graph", "contextual"],
|
|
2494
|
-
caps: { "daily-note": 2, observation: 15 }
|
|
2495
|
-
}
|
|
2496
|
-
};
|
|
2497
|
-
function extractKeywords(text) {
|
|
2498
|
-
const raw = text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
2499
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2500
|
-
const keywords = [];
|
|
2501
|
-
for (const token of raw) {
|
|
2502
|
-
if (token.length < 2 || STOP_WORDS.has(token) || seen.has(token)) {
|
|
2503
|
-
continue;
|
|
2504
|
-
}
|
|
2505
|
-
seen.add(token);
|
|
2506
|
-
keywords.push(token);
|
|
2507
|
-
}
|
|
2508
|
-
return keywords;
|
|
2509
|
-
}
|
|
2510
|
-
function computeKeywordOverlapScore(queryKeywords, text) {
|
|
2511
|
-
if (queryKeywords.length === 0) {
|
|
2512
|
-
return 1;
|
|
2513
|
-
}
|
|
2514
|
-
const haystack = new Set(extractKeywords(text));
|
|
2515
|
-
let matches = 0;
|
|
2516
|
-
for (const keyword of queryKeywords) {
|
|
2517
|
-
if (haystack.has(keyword)) {
|
|
2518
|
-
matches += 1;
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
if (matches === 0) {
|
|
2522
|
-
return 0.1;
|
|
2523
|
-
}
|
|
2524
|
-
return matches / queryKeywords.length;
|
|
2525
|
-
}
|
|
2526
|
-
function estimateSnippet(source) {
|
|
2527
|
-
const normalized = source.replace(/\s+/g, " ").trim();
|
|
2528
|
-
if (!normalized) {
|
|
2529
|
-
return "No snippet available.";
|
|
2530
|
-
}
|
|
2531
|
-
return normalized.slice(0, MAX_SNIPPET_LENGTH);
|
|
2532
|
-
}
|
|
2533
|
-
function parseIsoDate(value) {
|
|
2534
|
-
if (typeof value === "string") {
|
|
2535
|
-
const trimmed = value.trim();
|
|
2536
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
2537
|
-
return trimmed;
|
|
2538
|
-
}
|
|
2539
|
-
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
|
|
2540
|
-
return trimmed.slice(0, 10);
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
if (value instanceof Date) {
|
|
2544
|
-
const time = value.getTime();
|
|
2545
|
-
if (!Number.isNaN(time)) {
|
|
2546
|
-
return value.toISOString().slice(0, 10);
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
return null;
|
|
2550
|
-
}
|
|
2551
|
-
function asDate(value, fallback = /* @__PURE__ */ new Date(0)) {
|
|
2552
|
-
if (!value) {
|
|
2553
|
-
return fallback;
|
|
2554
|
-
}
|
|
2555
|
-
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
2556
|
-
return Number.isNaN(parsed.getTime()) ? fallback : parsed;
|
|
2557
|
-
}
|
|
2558
|
-
function observationImportanceToRank(importance) {
|
|
2559
|
-
if (importance >= 0.8) return 1;
|
|
2560
|
-
if (importance >= 0.4) return 4;
|
|
2561
|
-
return 5;
|
|
2562
|
-
}
|
|
2563
|
-
function toLedgerObservationPath(date) {
|
|
2564
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
2565
|
-
return `ledger/observations/${date}.md`;
|
|
2566
|
-
}
|
|
2567
|
-
const [year, month, day] = date.split("-");
|
|
2568
|
-
return `ledger/observations/${year}/${month}/${day}.md`;
|
|
2569
|
-
}
|
|
2570
|
-
function isLikelyDailyNote(document) {
|
|
2571
|
-
const normalizedPath = document.path.split(path5.sep).join("/").toLowerCase();
|
|
2572
|
-
if (normalizedPath.includes("/daily/")) {
|
|
2573
|
-
return true;
|
|
2574
|
-
}
|
|
2575
|
-
const category = document.category.toLowerCase();
|
|
2576
|
-
if (category.includes("daily")) {
|
|
2577
|
-
return true;
|
|
2578
|
-
}
|
|
2579
|
-
const type = typeof document.frontmatter.type === "string" ? document.frontmatter.type.toLowerCase() : "";
|
|
2580
|
-
return type === "daily";
|
|
2581
|
-
}
|
|
2582
|
-
function findDailyDate(document, targetDates) {
|
|
2583
|
-
const frontmatterDate = parseIsoDate(document.frontmatter.date);
|
|
2584
|
-
const titleDate = parseIsoDate(document.title);
|
|
2585
|
-
const fileDate = parseIsoDate(path5.basename(document.path, ".md"));
|
|
2586
|
-
const candidates = [frontmatterDate, titleDate, fileDate].filter((value) => Boolean(value));
|
|
2587
|
-
for (const candidate of candidates) {
|
|
2588
|
-
if (!targetDates.has(candidate)) {
|
|
2589
|
-
continue;
|
|
2590
|
-
}
|
|
2591
|
-
if (isLikelyDailyNote(document) || titleDate === candidate || fileDate === candidate) {
|
|
2592
|
-
return candidate;
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
return null;
|
|
2596
|
-
}
|
|
2597
|
-
function getTargetDailyDates(now = /* @__PURE__ */ new Date()) {
|
|
2598
|
-
const today = now.toISOString().slice(0, 10);
|
|
2599
|
-
const yesterdayDate = new Date(now);
|
|
2600
|
-
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
|
|
2601
|
-
const yesterday = yesterdayDate.toISOString().slice(0, 10);
|
|
2602
|
-
return [today, yesterday];
|
|
2603
|
-
}
|
|
2604
|
-
function buildDailyContextItems(vaultPath, allDocuments) {
|
|
2605
|
-
const targetDates = getTargetDailyDates();
|
|
2606
|
-
const targetDateSet = new Set(targetDates);
|
|
2607
|
-
const byDate = /* @__PURE__ */ new Map();
|
|
2608
|
-
for (const document of allDocuments) {
|
|
2609
|
-
const dailyDate = findDailyDate(document, targetDateSet);
|
|
2610
|
-
if (!dailyDate) {
|
|
2611
|
-
continue;
|
|
2612
|
-
}
|
|
2613
|
-
const existing = byDate.get(dailyDate);
|
|
2614
|
-
if (!existing || document.modified.getTime() > existing.modified.getTime()) {
|
|
2615
|
-
byDate.set(dailyDate, document);
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
const items = [];
|
|
2619
|
-
for (const date of targetDates) {
|
|
2620
|
-
const document = byDate.get(date);
|
|
2621
|
-
if (!document) {
|
|
2622
|
-
continue;
|
|
2623
|
-
}
|
|
2624
|
-
const relativePath = path5.relative(vaultPath, document.path).split(path5.sep).join("/");
|
|
2625
|
-
const snippet = estimateSnippet(document.content);
|
|
2626
|
-
items.push({
|
|
2627
|
-
priority: 2,
|
|
2628
|
-
entry: {
|
|
2629
|
-
title: `Daily note ${date}`,
|
|
2630
|
-
path: relativePath,
|
|
2631
|
-
category: document.category,
|
|
2632
|
-
score: 0.9,
|
|
2633
|
-
snippet,
|
|
2634
|
-
modified: document.modified.toISOString(),
|
|
2635
|
-
age: formatRelativeAge(document.modified),
|
|
2636
|
-
source: "daily-note",
|
|
2637
|
-
signals: ["daily_recency"],
|
|
2638
|
-
rationale: "Pinned daily note context (today/yesterday)."
|
|
2639
|
-
}
|
|
2640
|
-
});
|
|
2641
|
-
}
|
|
2642
|
-
return items;
|
|
2643
|
-
}
|
|
2644
|
-
function buildObservationContextItems(vaultPath, queryKeywords) {
|
|
2645
|
-
const observationMarkdown = readObservations(vaultPath, OBSERVATION_LOOKBACK_DAYS);
|
|
2646
|
-
const parsed = parseObservationLines(observationMarkdown);
|
|
2647
|
-
const items = [];
|
|
2648
|
-
for (const [index, observation] of parsed.entries()) {
|
|
2649
|
-
const priority = observationImportanceToRank(observation.importance);
|
|
2650
|
-
const modifiedDate = asDate(observation.date, /* @__PURE__ */ new Date());
|
|
2651
|
-
const date = observation.date || modifiedDate.toISOString().slice(0, 10);
|
|
2652
|
-
const snippet = estimateSnippet(observation.content);
|
|
2653
|
-
items.push({
|
|
2654
|
-
priority,
|
|
2655
|
-
entry: {
|
|
2656
|
-
title: `[${observation.type}|i=${observation.importance.toFixed(2)}] observation (${date}) #${index + 1}`,
|
|
2657
|
-
path: toLedgerObservationPath(date),
|
|
2658
|
-
category: "observations",
|
|
2659
|
-
score: computeKeywordOverlapScore(queryKeywords, observation.content),
|
|
2660
|
-
snippet,
|
|
2661
|
-
modified: modifiedDate.toISOString(),
|
|
2662
|
-
age: formatRelativeAge(modifiedDate),
|
|
2663
|
-
source: "observation",
|
|
2664
|
-
signals: ["observation_importance", `type:${observation.type}`, "keyword_overlap"],
|
|
2665
|
-
rationale: `Observation type ${observation.type} with importance ${observation.importance.toFixed(2)} matched task keywords.`
|
|
2666
|
-
}
|
|
2667
|
-
});
|
|
2668
|
-
}
|
|
2669
|
-
return items;
|
|
2670
|
-
}
|
|
2671
|
-
function buildSearchContextItems(vault, results) {
|
|
2672
|
-
return results.map((result) => {
|
|
2673
|
-
const relativePath = path5.relative(vault.getPath(), result.document.path).split(path5.sep).join("/");
|
|
2674
|
-
const entry = {
|
|
2675
|
-
title: result.document.title,
|
|
2676
|
-
path: relativePath,
|
|
2677
|
-
category: result.document.category,
|
|
2678
|
-
score: result.score,
|
|
2679
|
-
snippet: normalizeSnippet(result),
|
|
2680
|
-
modified: result.document.modified.toISOString(),
|
|
2681
|
-
age: formatRelativeAge(result.document.modified),
|
|
2682
|
-
source: "search",
|
|
2683
|
-
signals: ["semantic_search"],
|
|
2684
|
-
rationale: "Selected by semantic retrieval."
|
|
2685
|
-
};
|
|
2686
|
-
return {
|
|
2687
|
-
priority: 3,
|
|
2688
|
-
entry
|
|
2689
|
-
};
|
|
2690
|
-
});
|
|
2691
|
-
}
|
|
2692
|
-
function toNoteNodeId2(vaultPath, absolutePath) {
|
|
2693
|
-
const relativePath = path5.relative(vaultPath, absolutePath).split(path5.sep).join("/");
|
|
2694
|
-
const noteKey = relativePath.toLowerCase().endsWith(".md") ? relativePath.slice(0, -3) : relativePath;
|
|
2695
|
-
return `note:${noteKey}`;
|
|
2696
|
-
}
|
|
2697
|
-
function buildGraphAdjacency(edges) {
|
|
2698
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
2699
|
-
for (const edge of edges) {
|
|
2700
|
-
const sourceBucket = adjacency.get(edge.source) ?? [];
|
|
2701
|
-
sourceBucket.push(edge);
|
|
2702
|
-
adjacency.set(edge.source, sourceBucket);
|
|
2703
|
-
const targetBucket = adjacency.get(edge.target) ?? [];
|
|
2704
|
-
targetBucket.push(edge);
|
|
2705
|
-
adjacency.set(edge.target, targetBucket);
|
|
2706
|
-
}
|
|
2707
|
-
return adjacency;
|
|
2708
|
-
}
|
|
2709
|
-
function edgeWeight(edge) {
|
|
2710
|
-
if (edge.type === "frontmatter_relation") return 0.95;
|
|
2711
|
-
if (edge.type === "wiki_link") return 0.8;
|
|
2712
|
-
return 0.6;
|
|
2713
|
-
}
|
|
2714
|
-
function buildGraphContextItems(params) {
|
|
2715
|
-
const { graph, vaultPath, documents, searchItems, limit, maxHops } = params;
|
|
2716
|
-
if (searchItems.length === 0 || graph.nodes.length === 0 || graph.edges.length === 0 || maxHops <= 0) {
|
|
2717
|
-
return [];
|
|
2718
|
-
}
|
|
2719
|
-
const graphNodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
2720
|
-
const adjacency = buildGraphAdjacency(graph.edges);
|
|
2721
|
-
const docByNodeId = /* @__PURE__ */ new Map();
|
|
2722
|
-
for (const document of documents) {
|
|
2723
|
-
docByNodeId.set(toNoteNodeId2(vaultPath, document.path), document);
|
|
2724
|
-
}
|
|
2725
|
-
const anchors = searchItems.map((item) => ({
|
|
2726
|
-
item,
|
|
2727
|
-
nodeId: toNoteNodeId2(vaultPath, path5.join(vaultPath, item.entry.path))
|
|
2728
|
-
})).filter((anchor) => graphNodeById.has(anchor.nodeId));
|
|
2729
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
2730
|
-
for (const anchor of anchors) {
|
|
2731
|
-
const visited = /* @__PURE__ */ new Set([anchor.nodeId]);
|
|
2732
|
-
const queue = [{ nodeId: anchor.nodeId, hop: 0 }];
|
|
2733
|
-
while (queue.length > 0) {
|
|
2734
|
-
const current = queue.shift();
|
|
2735
|
-
if (current.hop >= maxHops) {
|
|
2736
|
-
continue;
|
|
2737
|
-
}
|
|
2738
|
-
const connectedEdges = adjacency.get(current.nodeId) ?? [];
|
|
2739
|
-
for (const edge of connectedEdges) {
|
|
2740
|
-
const neighborId = edge.source === current.nodeId ? edge.target : edge.source;
|
|
2741
|
-
if (visited.has(neighborId)) {
|
|
2742
|
-
continue;
|
|
2743
|
-
}
|
|
2744
|
-
visited.add(neighborId);
|
|
2745
|
-
queue.push({ nodeId: neighborId, hop: current.hop + 1 });
|
|
2746
|
-
if (neighborId === anchor.nodeId) continue;
|
|
2747
|
-
const neighborNode = graphNodeById.get(neighborId);
|
|
2748
|
-
if (!neighborNode || neighborNode.type === "tag" || neighborNode.type === "unresolved" || neighborNode.missing === true) {
|
|
2749
|
-
continue;
|
|
2750
|
-
}
|
|
2751
|
-
const neighborDoc = docByNodeId.get(neighborId);
|
|
2752
|
-
if (!neighborDoc) {
|
|
2753
|
-
continue;
|
|
2754
|
-
}
|
|
2755
|
-
const neighborPath = path5.relative(vaultPath, neighborDoc.path).split(path5.sep).join("/");
|
|
2756
|
-
const modifiedAt = neighborDoc.modified;
|
|
2757
|
-
const snippet = estimateSnippet(neighborDoc.content);
|
|
2758
|
-
const hopPenalty = Math.pow(0.85, Math.max(0, current.hop));
|
|
2759
|
-
const score = Math.max(0.05, Math.min(1, anchor.item.entry.score * edgeWeight(edge) * hopPenalty));
|
|
2760
|
-
const key = neighborId;
|
|
2761
|
-
const existing = candidates.get(key);
|
|
2762
|
-
const candidate = {
|
|
2763
|
-
priority: 3,
|
|
2764
|
-
entry: {
|
|
2765
|
-
title: neighborDoc.title,
|
|
2766
|
-
path: neighborPath,
|
|
2767
|
-
category: neighborDoc.category,
|
|
2768
|
-
score,
|
|
2769
|
-
snippet,
|
|
2770
|
-
modified: modifiedAt.toISOString(),
|
|
2771
|
-
age: formatRelativeAge(modifiedAt),
|
|
2772
|
-
source: "graph",
|
|
2773
|
-
signals: ["graph_neighbor", `edge:${edge.type}`, `hop:${current.hop + 1}`],
|
|
2774
|
-
rationale: `Connected to "${anchor.item.entry.title}" within ${current.hop + 1} hop(s) via ${edge.type}${edge.label ? ` (${edge.label})` : ""}.`
|
|
2775
|
-
}
|
|
2776
|
-
};
|
|
2777
|
-
if (!existing || existing.entry.score < candidate.entry.score) {
|
|
2778
|
-
candidates.set(key, candidate);
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
return [...candidates.values()].sort((left, right) => right.entry.score - left.entry.score).slice(0, Math.max(limit, 1));
|
|
2784
|
-
}
|
|
2785
|
-
function dedupeContextItems(items) {
|
|
2786
|
-
const deduped = /* @__PURE__ */ new Map();
|
|
2787
|
-
for (const item of items) {
|
|
2788
|
-
const key = `${item.entry.path}|${item.entry.source}|${item.entry.title}`;
|
|
2789
|
-
const existing = deduped.get(key);
|
|
2790
|
-
if (!existing || existing.entry.score < item.entry.score) {
|
|
2791
|
-
deduped.set(key, item);
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
return [...deduped.values()];
|
|
2795
|
-
}
|
|
2796
|
-
function applySourceCaps(items, caps) {
|
|
2797
|
-
const counts = {};
|
|
2798
|
-
const capped = [];
|
|
2799
|
-
for (const item of items) {
|
|
2800
|
-
const source = item.entry.source;
|
|
2801
|
-
const limit = caps[source];
|
|
2802
|
-
if (limit !== void 0) {
|
|
2803
|
-
const current = counts[source] ?? 0;
|
|
2804
|
-
if (current >= limit) {
|
|
2805
|
-
continue;
|
|
2806
|
-
}
|
|
2807
|
-
counts[source] = current + 1;
|
|
2808
|
-
}
|
|
2809
|
-
capped.push(item);
|
|
2810
|
-
}
|
|
2811
|
-
return capped;
|
|
2812
|
-
}
|
|
2813
|
-
function renderEntryBlock(entry) {
|
|
2814
|
-
return `### ${entry.title} (${entry.source}, score: ${entry.score.toFixed(2)}, ${entry.age})
|
|
2815
|
-
${entry.snippet}
|
|
2816
|
-
|
|
2817
|
-
`;
|
|
2818
|
-
}
|
|
2819
|
-
function truncateToBudget(text, budget) {
|
|
2820
|
-
if (!Number.isFinite(budget) || budget <= 0) {
|
|
2821
|
-
return "";
|
|
2822
|
-
}
|
|
2823
|
-
const maxChars = Math.max(0, Math.floor(budget) * 4);
|
|
2824
|
-
if (text.length <= maxChars) {
|
|
2825
|
-
return text;
|
|
2826
|
-
}
|
|
2827
|
-
return text.slice(0, maxChars).trimEnd();
|
|
2828
|
-
}
|
|
2829
|
-
function applyTokenBudget(items, task, budget) {
|
|
2830
|
-
const fullContext = items.map((item) => item.entry);
|
|
2831
|
-
const fullMarkdown = formatContextMarkdown(task, fullContext);
|
|
2832
|
-
if (budget === void 0) {
|
|
2833
|
-
return { context: fullContext, markdown: fullMarkdown };
|
|
2834
|
-
}
|
|
2835
|
-
const normalizedBudget = Math.max(1, Math.floor(budget));
|
|
2836
|
-
if (estimateTokens(fullMarkdown) <= normalizedBudget) {
|
|
2837
|
-
return { context: fullContext, markdown: fullMarkdown };
|
|
2838
|
-
}
|
|
2839
|
-
const header = `## Relevant Context for: ${task}
|
|
2840
|
-
|
|
2841
|
-
`;
|
|
2842
|
-
const headerCost = estimateTokens(header);
|
|
2843
|
-
if (headerCost >= normalizedBudget) {
|
|
2844
|
-
return {
|
|
2845
|
-
context: [],
|
|
2846
|
-
markdown: truncateToBudget(header.trimEnd(), normalizedBudget)
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
const fitted = fitWithinBudget(
|
|
2850
|
-
items.map((item, index) => ({
|
|
2851
|
-
text: renderEntryBlock(item.entry),
|
|
2852
|
-
priority: item.priority,
|
|
2853
|
-
source: String(index)
|
|
2854
|
-
})),
|
|
2855
|
-
normalizedBudget - headerCost
|
|
2856
|
-
);
|
|
2857
|
-
const selectedEntries = fitted.map((item) => {
|
|
2858
|
-
const index = Number.parseInt(item.source, 10);
|
|
2859
|
-
return Number.isNaN(index) ? null : items[index]?.entry ?? null;
|
|
2860
|
-
}).filter((entry) => Boolean(entry));
|
|
2861
|
-
if (selectedEntries.length === 0 && items.length > 0) {
|
|
2862
|
-
const topEntry = items[0].entry;
|
|
2863
|
-
return {
|
|
2864
|
-
context: [topEntry],
|
|
2865
|
-
markdown: truncateToBudget(formatContextMarkdown(task, [topEntry]), normalizedBudget)
|
|
2866
|
-
};
|
|
2867
|
-
}
|
|
2868
|
-
const markdown = truncateToBudget(formatContextMarkdown(task, selectedEntries), normalizedBudget);
|
|
2869
|
-
return {
|
|
2870
|
-
context: selectedEntries,
|
|
2871
|
-
markdown
|
|
2872
|
-
};
|
|
2873
|
-
}
|
|
2874
|
-
async function buildContext(task, options) {
|
|
2875
|
-
const normalizedTask = task.trim();
|
|
2876
|
-
if (!normalizedTask) {
|
|
2877
|
-
throw new Error("Task description is required.");
|
|
2878
|
-
}
|
|
2879
|
-
const vault = new ClawVault(path5.resolve(options.vaultPath));
|
|
2880
|
-
await vault.load();
|
|
2881
|
-
const limit = Math.max(1, options.limit ?? DEFAULT_LIMIT);
|
|
2882
|
-
const recent = options.recent ?? true;
|
|
2883
|
-
const includeObservations = options.includeObservations ?? true;
|
|
2884
|
-
const maxHops = Math.max(1, Math.floor(options.maxHops ?? 2));
|
|
2885
|
-
const profile = resolveContextProfile(options.profile, normalizedTask);
|
|
2886
|
-
const queryKeywords = extractKeywords(normalizedTask);
|
|
2887
|
-
const allDocuments = await vault.list();
|
|
2888
|
-
const searchResults = await vault.vsearch(normalizedTask, {
|
|
2889
|
-
limit,
|
|
2890
|
-
temporalBoost: recent
|
|
2891
|
-
});
|
|
2892
|
-
const searchItems = buildSearchContextItems(vault, searchResults);
|
|
2893
|
-
const dailyItems = buildDailyContextItems(vault.getPath(), allDocuments);
|
|
2894
|
-
const observationItems = includeObservations ? buildObservationContextItems(vault.getPath(), queryKeywords) : [];
|
|
2895
|
-
const graph = await getMemoryGraph(vault.getPath());
|
|
2896
|
-
const graphItems = buildGraphContextItems({
|
|
2897
|
-
graph,
|
|
2898
|
-
vaultPath: vault.getPath(),
|
|
2899
|
-
documents: allDocuments,
|
|
2900
|
-
searchItems,
|
|
2901
|
-
limit,
|
|
2902
|
-
maxHops
|
|
2903
|
-
});
|
|
2904
|
-
const byScoreDesc = (left, right) => right.entry.score - left.entry.score;
|
|
2905
|
-
const structuralObservations = observationItems.filter((item) => item.priority === 1).sort(byScoreDesc);
|
|
2906
|
-
const potentialObservations = observationItems.filter((item) => item.priority === 4).sort(byScoreDesc);
|
|
2907
|
-
const contextualObservations = observationItems.filter((item) => item.priority === 5).sort(byScoreDesc);
|
|
2908
|
-
const sortedDailyItems = [...dailyItems].sort(byScoreDesc);
|
|
2909
|
-
const sortedSearchItems = [...searchItems].sort(byScoreDesc);
|
|
2910
|
-
const sortedGraphItems = [...graphItems].sort(byScoreDesc);
|
|
2911
|
-
const grouped = {
|
|
2912
|
-
structural: structuralObservations,
|
|
2913
|
-
daily: sortedDailyItems,
|
|
2914
|
-
search: sortedSearchItems,
|
|
2915
|
-
graph: sortedGraphItems,
|
|
2916
|
-
potential: potentialObservations,
|
|
2917
|
-
contextual: contextualObservations
|
|
2918
|
-
};
|
|
2919
|
-
const ordering = PROFILE_ORDERING[profile];
|
|
2920
|
-
const ordered = dedupeContextItems(
|
|
2921
|
-
applySourceCaps(
|
|
2922
|
-
ordering.order.flatMap((group) => grouped[group]),
|
|
2923
|
-
ordering.caps
|
|
2924
|
-
)
|
|
2925
|
-
);
|
|
2926
|
-
const { context, markdown } = applyTokenBudget(ordered, normalizedTask, options.budget);
|
|
2927
|
-
return {
|
|
2928
|
-
task: normalizedTask,
|
|
2929
|
-
profile,
|
|
2930
|
-
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2931
|
-
context,
|
|
2932
|
-
markdown
|
|
2933
|
-
};
|
|
2934
|
-
}
|
|
2935
|
-
async function contextCommand(task, options) {
|
|
2936
|
-
const result = await buildContext(task, options);
|
|
2937
|
-
const format = options.format ?? "markdown";
|
|
2938
|
-
if (format === "json") {
|
|
2939
|
-
const context = result.context.map((entry) => ({
|
|
2940
|
-
...entry,
|
|
2941
|
-
explain: {
|
|
2942
|
-
signals: entry.signals ?? [],
|
|
2943
|
-
rationale: entry.rationale ?? ""
|
|
2944
|
-
}
|
|
2945
|
-
}));
|
|
2946
|
-
console.log(JSON.stringify({
|
|
2947
|
-
task: result.task,
|
|
2948
|
-
profile: result.profile,
|
|
2949
|
-
generated: result.generated,
|
|
2950
|
-
count: context.length,
|
|
2951
|
-
context
|
|
2952
|
-
}, null, 2));
|
|
2953
|
-
return;
|
|
2954
|
-
}
|
|
2955
|
-
console.log(result.markdown);
|
|
2956
|
-
}
|
|
2957
|
-
function parsePositiveInteger(raw, label) {
|
|
2958
|
-
const parsed = Number.parseInt(raw, 10);
|
|
2959
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
2960
|
-
throw new Error(`Invalid ${label}: ${raw}`);
|
|
2961
|
-
}
|
|
2962
|
-
return parsed;
|
|
2963
|
-
}
|
|
2964
|
-
function registerContextCommand(program) {
|
|
2965
|
-
program.command("context <task>").description("Generate task-relevant context for prompt injection").option("-n, --limit <n>", "Max results", "5").option("--format <format>", "Output format (markdown|json)", "markdown").option("--recent", "Boost recent documents (enabled by default)", true).option("--include-observations", "Include observation memories in output", true).option("--budget <number>", "Optional token budget for assembled context").option("--profile <profile>", "Context profile (default|planning|incident|handoff|auto)", "default").option("--max-hops <n>", "Maximum graph expansion hops", "2").option("-v, --vault <path>", "Vault path").action(async (task, rawOptions) => {
|
|
2966
|
-
const format = rawOptions.format === "json" ? "json" : "markdown";
|
|
2967
|
-
const budget = rawOptions.budget ? parsePositiveInteger(rawOptions.budget, "budget") : void 0;
|
|
2968
|
-
const limit = parsePositiveInteger(rawOptions.limit, "limit");
|
|
2969
|
-
const maxHops = parsePositiveInteger(rawOptions.maxHops, "max-hops");
|
|
2970
|
-
const vaultPath = rawOptions.vault ?? process.env.CLAWVAULT_PATH ?? process.cwd();
|
|
2971
|
-
await contextCommand(task, {
|
|
2972
|
-
vaultPath,
|
|
2973
|
-
limit,
|
|
2974
|
-
format,
|
|
2975
|
-
recent: rawOptions.recent ?? true,
|
|
2976
|
-
includeObservations: rawOptions.includeObservations ?? true,
|
|
2977
|
-
budget,
|
|
2978
|
-
profile: normalizeContextProfileInput(rawOptions.profile),
|
|
2979
|
-
maxHops
|
|
2980
|
-
});
|
|
2981
|
-
});
|
|
2982
|
-
}
|
|
2983
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
2984
|
-
0 && (module.exports = {
|
|
2985
|
-
buildContext,
|
|
2986
|
-
contextCommand,
|
|
2987
|
-
formatContextMarkdown,
|
|
2988
|
-
registerContextCommand
|
|
2989
|
-
});
|