@hraness/kb 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
defineWorkflow
|
|
4
|
+
} from "./index-3v2z4f0q.js";
|
|
5
|
+
import {
|
|
6
|
+
packUntrustedSearchContext
|
|
7
|
+
} from "./index-zzhgcwyt.js";
|
|
8
|
+
|
|
9
|
+
// src/workflows/decision-context.ts
|
|
10
|
+
var decisionContextWorkflow = defineWorkflow({
|
|
11
|
+
id: "decision-context",
|
|
12
|
+
nodes: [
|
|
13
|
+
{
|
|
14
|
+
id: "search",
|
|
15
|
+
resource: "qmd",
|
|
16
|
+
run: ({ input, kb }) => kb.search({
|
|
17
|
+
query: input.query,
|
|
18
|
+
filters: input.filters ?? [],
|
|
19
|
+
tags: input.tags ?? [],
|
|
20
|
+
graph: { related: input.related ?? [] },
|
|
21
|
+
history: false,
|
|
22
|
+
...input.resultLimit === undefined ? {} : { limit: input.resultLimit }
|
|
23
|
+
})
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "history",
|
|
27
|
+
resource: "git",
|
|
28
|
+
needs: ["search"],
|
|
29
|
+
run: async ({ kb, result }) => {
|
|
30
|
+
const search = result("search");
|
|
31
|
+
return await kb.history(search.results.slice(0, 5).map(({ id }) => id), {
|
|
32
|
+
commitsPerNote: 3,
|
|
33
|
+
cochangedPathsPerCommit: 8
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "pack",
|
|
39
|
+
needs: ["search", "history"],
|
|
40
|
+
run: ({ input, result }) => {
|
|
41
|
+
const search = result("search");
|
|
42
|
+
const enriched = {
|
|
43
|
+
...search,
|
|
44
|
+
history: result("history")
|
|
45
|
+
};
|
|
46
|
+
const packed = packUntrustedSearchContext(enriched, {
|
|
47
|
+
...input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes }
|
|
48
|
+
});
|
|
49
|
+
return { context: packed.content, truncated: packed.truncated };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
output: "pack"
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export { decisionContextWorkflow };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/clip/terminal.ts
|
|
3
|
+
var ESCAPE = 27;
|
|
4
|
+
var BELL = 7;
|
|
5
|
+
var STRING_TERMINATOR = 156;
|
|
6
|
+
var MAX_PENDING_LENGTH = 64 * 1024;
|
|
7
|
+
var MAX_PENDING_SEGMENTS = 4096;
|
|
8
|
+
var CONTEXT_FREE_TERMINAL_CONTROLS = new RegExp(String.raw`[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]`, "g");
|
|
9
|
+
var CONTEXT_SENSITIVE_TERMINAL_CONTROLS = new RegExp(String.raw`[\u0009\u000d\u001b\u0090\u0098\u009b\u009d-\u009f\u2028\u2029]`);
|
|
10
|
+
function chunkBuilder() {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let pending = [];
|
|
13
|
+
let pendingLength = 0;
|
|
14
|
+
const flush = () => {
|
|
15
|
+
if (pending.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
chunks.push(pending.join(""));
|
|
18
|
+
pending = [];
|
|
19
|
+
pendingLength = 0;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
append: (value) => {
|
|
23
|
+
if (value === "")
|
|
24
|
+
return;
|
|
25
|
+
if (value.length >= MAX_PENDING_LENGTH) {
|
|
26
|
+
flush();
|
|
27
|
+
chunks.push(value);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
pending.push(value);
|
|
31
|
+
pendingLength += value.length;
|
|
32
|
+
if (pendingLength >= MAX_PENDING_LENGTH || pending.length >= MAX_PENDING_SEGMENTS)
|
|
33
|
+
flush();
|
|
34
|
+
},
|
|
35
|
+
finish: () => {
|
|
36
|
+
flush();
|
|
37
|
+
if (chunks.length === 0)
|
|
38
|
+
return "";
|
|
39
|
+
if (chunks.length === 1)
|
|
40
|
+
return chunks[0] ?? "";
|
|
41
|
+
return chunks.join("");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isEscapeStringIntroducer(code) {
|
|
46
|
+
return code === 80 || code === 88 || code === 93 || code === 94 || code === 95;
|
|
47
|
+
}
|
|
48
|
+
function isC1StringIntroducer(code) {
|
|
49
|
+
return code === 144 || code === 152 || code === 157 || code === 158 || code === 159;
|
|
50
|
+
}
|
|
51
|
+
function controlStringEnd(value, start) {
|
|
52
|
+
for (let cursor = start;cursor < value.length; cursor += 1) {
|
|
53
|
+
const code = value.charCodeAt(cursor);
|
|
54
|
+
if (code === BELL || code === STRING_TERMINATOR)
|
|
55
|
+
return cursor + 1;
|
|
56
|
+
if (code === ESCAPE && value.charCodeAt(cursor + 1) === 92)
|
|
57
|
+
return cursor + 2;
|
|
58
|
+
}
|
|
59
|
+
return value.length;
|
|
60
|
+
}
|
|
61
|
+
function controlSequenceEnd(value, start) {
|
|
62
|
+
for (let cursor = start;cursor < value.length; cursor += 1) {
|
|
63
|
+
const code = value.charCodeAt(cursor);
|
|
64
|
+
if (code >= 64 && code <= 126)
|
|
65
|
+
return cursor + 1;
|
|
66
|
+
}
|
|
67
|
+
return value.length;
|
|
68
|
+
}
|
|
69
|
+
function escapeSequenceEnd(value, start) {
|
|
70
|
+
const introducer = value.charCodeAt(start + 1);
|
|
71
|
+
if (Number.isNaN(introducer))
|
|
72
|
+
return start + 1;
|
|
73
|
+
if (introducer === 91)
|
|
74
|
+
return controlSequenceEnd(value, start + 2);
|
|
75
|
+
if (isEscapeStringIntroducer(introducer)) {
|
|
76
|
+
return controlStringEnd(value, start + 2);
|
|
77
|
+
}
|
|
78
|
+
let cursor = start + 1;
|
|
79
|
+
while (cursor < value.length) {
|
|
80
|
+
const code = value.charCodeAt(cursor);
|
|
81
|
+
if (code >= 32 && code <= 47) {
|
|
82
|
+
cursor += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
return code >= 48 && code <= 126 ? cursor + 1 : start + 1;
|
|
86
|
+
}
|
|
87
|
+
return value.length;
|
|
88
|
+
}
|
|
89
|
+
function isBidiFormatControl(code) {
|
|
90
|
+
return code === 1564 || code === 8206 || code === 8207 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297;
|
|
91
|
+
}
|
|
92
|
+
function hasUnsafeTerminalCharacters(value) {
|
|
93
|
+
for (const character of value) {
|
|
94
|
+
const code = character.codePointAt(0) ?? 0;
|
|
95
|
+
if (code <= 9 || code >= 11 && code <= 31 || code >= 127 && code <= 159 || isBidiFormatControl(code))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function sanitizeTerminalText(value) {
|
|
101
|
+
if (!CONTEXT_SENSITIVE_TERMINAL_CONTROLS.test(value)) {
|
|
102
|
+
return value.replace(CONTEXT_FREE_TERMINAL_CONTROLS, "");
|
|
103
|
+
}
|
|
104
|
+
let builder = null;
|
|
105
|
+
let unchangedStart = 0;
|
|
106
|
+
const replace = (start, end, replacement = "") => {
|
|
107
|
+
builder ??= chunkBuilder();
|
|
108
|
+
builder.append(value.slice(unchangedStart, start));
|
|
109
|
+
builder.append(replacement);
|
|
110
|
+
unchangedStart = end;
|
|
111
|
+
};
|
|
112
|
+
for (let cursor = 0;cursor < value.length; ) {
|
|
113
|
+
const code = value.charCodeAt(cursor);
|
|
114
|
+
if (code === ESCAPE) {
|
|
115
|
+
const end = escapeSequenceEnd(value, cursor);
|
|
116
|
+
replace(cursor, end);
|
|
117
|
+
cursor = end;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (code === 155) {
|
|
121
|
+
const end = controlSequenceEnd(value, cursor + 1);
|
|
122
|
+
replace(cursor, end);
|
|
123
|
+
cursor = end;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (isC1StringIntroducer(code)) {
|
|
127
|
+
const end = controlStringEnd(value, cursor + 1);
|
|
128
|
+
replace(cursor, end);
|
|
129
|
+
cursor = end;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (code === 13) {
|
|
133
|
+
const end = value.charCodeAt(cursor + 1) === 10 ? cursor + 2 : cursor + 1;
|
|
134
|
+
replace(cursor, end, `
|
|
135
|
+
`);
|
|
136
|
+
cursor = end;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (code === 10) {
|
|
140
|
+
cursor += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (code === 8232 || code === 8233) {
|
|
144
|
+
replace(cursor, cursor + 1, `
|
|
145
|
+
`);
|
|
146
|
+
cursor += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (code === 9) {
|
|
150
|
+
replace(cursor, cursor + 1, " ");
|
|
151
|
+
cursor += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (code <= 31 || code >= 127 && code <= 159 || isBidiFormatControl(code)) {
|
|
155
|
+
replace(cursor, cursor + 1);
|
|
156
|
+
cursor += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
cursor += 1;
|
|
160
|
+
}
|
|
161
|
+
const completedBuilder = builder;
|
|
162
|
+
if (completedBuilder === null)
|
|
163
|
+
return value;
|
|
164
|
+
completedBuilder.append(value.slice(unchangedStart));
|
|
165
|
+
return completedBuilder.finish();
|
|
166
|
+
}
|
|
167
|
+
function sanitizeTerminalLine(value) {
|
|
168
|
+
return sanitizeTerminalText(value).replace(/\n/g, " ");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { hasUnsafeTerminalCharacters, sanitizeTerminalText, sanitizeTerminalLine };
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/note-lock.ts
|
|
3
|
+
import { createHash, randomUUID } from "crypto";
|
|
4
|
+
import { constants } from "fs";
|
|
5
|
+
import {
|
|
6
|
+
link,
|
|
7
|
+
lstat,
|
|
8
|
+
mkdir,
|
|
9
|
+
open,
|
|
10
|
+
realpath,
|
|
11
|
+
rename,
|
|
12
|
+
unlink
|
|
13
|
+
} from "fs/promises";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
16
|
+
var LOCK_SCHEMA_VERSION = 1;
|
|
17
|
+
var MAX_LOCK_BYTES = 4 * 1024;
|
|
18
|
+
var DEFAULT_STALE_AFTER_MS = 5 * 60 * 1000;
|
|
19
|
+
var DEFAULT_HEARTBEAT_MS = 5000;
|
|
20
|
+
var DEFAULT_POLL_INTERVAL_MS = 20;
|
|
21
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 30000;
|
|
22
|
+
var MAX_RECLAIM_ATTEMPTS = 8;
|
|
23
|
+
|
|
24
|
+
class NoteLockBusyError extends Error {
|
|
25
|
+
lockPath;
|
|
26
|
+
constructor(lockPath) {
|
|
27
|
+
super("this note is already being edited");
|
|
28
|
+
this.name = "NoteLockBusyError";
|
|
29
|
+
this.lockPath = lockPath;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class NoteLockLostError extends Error {
|
|
34
|
+
lockPath;
|
|
35
|
+
constructor(lockPath) {
|
|
36
|
+
super("the note lock is no longer owned by this process");
|
|
37
|
+
this.name = "NoteLockLostError";
|
|
38
|
+
this.lockPath = lockPath;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isErrno(error, code) {
|
|
42
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
43
|
+
}
|
|
44
|
+
function sha256(value) {
|
|
45
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function within(root, candidate) {
|
|
48
|
+
const fromRoot = relative(root, candidate);
|
|
49
|
+
return fromRoot === "" || fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`);
|
|
50
|
+
}
|
|
51
|
+
function defaultCacheHome() {
|
|
52
|
+
const configured = process.env.XDG_CACHE_HOME;
|
|
53
|
+
return configured !== undefined && isAbsolute(configured) ? configured : join(homedir(), ".cache");
|
|
54
|
+
}
|
|
55
|
+
function defaultDependencies() {
|
|
56
|
+
return {
|
|
57
|
+
pid: process.pid,
|
|
58
|
+
now: () => new Date,
|
|
59
|
+
monotonicNow: () => performance.now(),
|
|
60
|
+
token: () => randomUUID(),
|
|
61
|
+
isProcessAlive: (pid) => {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(pid, 0);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return !isErrno(error, "ESRCH");
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
sleep: async (milliseconds) => {
|
|
70
|
+
await new Promise((resolveSleep) => {
|
|
71
|
+
setTimeout(resolveSleep, milliseconds);
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
staleAfterMs: DEFAULT_STALE_AFTER_MS,
|
|
75
|
+
heartbeatMs: DEFAULT_HEARTBEAT_MS,
|
|
76
|
+
pollIntervalMs: DEFAULT_POLL_INTERVAL_MS
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function resolvedDependencies(overrides) {
|
|
80
|
+
const dependencies = { ...defaultDependencies(), ...overrides };
|
|
81
|
+
if (!Number.isSafeInteger(dependencies.pid) || dependencies.pid <= 0) {
|
|
82
|
+
throw new TypeError("a note lock requires a positive process ID");
|
|
83
|
+
}
|
|
84
|
+
for (const [label, value] of [
|
|
85
|
+
["staleAfterMs", dependencies.staleAfterMs],
|
|
86
|
+
["heartbeatMs", dependencies.heartbeatMs],
|
|
87
|
+
["pollIntervalMs", dependencies.pollIntervalMs]
|
|
88
|
+
]) {
|
|
89
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
90
|
+
throw new TypeError(`${label} must be a positive finite duration`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return dependencies;
|
|
94
|
+
}
|
|
95
|
+
async function lockPathFor(vaultRootInput, canonicalNoteId, cacheHomeInput) {
|
|
96
|
+
if (canonicalNoteId === "" || canonicalNoteId.includes("\x00") || canonicalNoteId.includes(`
|
|
97
|
+
`) || canonicalNoteId.includes("\r")) {
|
|
98
|
+
throw new TypeError("a note lock requires a non-empty single-line note ID");
|
|
99
|
+
}
|
|
100
|
+
const vaultRoot = await realpath(resolve(vaultRootInput));
|
|
101
|
+
const cacheHome = resolve(cacheHomeInput ?? defaultCacheHome());
|
|
102
|
+
const requestedDirectory = join(cacheHome, "hraness-kb", "note-locks", sha256(vaultRoot));
|
|
103
|
+
await mkdir(requestedDirectory, { recursive: true, mode: 448 });
|
|
104
|
+
const requestedMetadata = await lstat(requestedDirectory);
|
|
105
|
+
if (!requestedMetadata.isDirectory() || requestedMetadata.isSymbolicLink()) {
|
|
106
|
+
throw new Error("the note lock root must be a real directory");
|
|
107
|
+
}
|
|
108
|
+
const lockDirectory = await realpath(requestedDirectory);
|
|
109
|
+
if (within(vaultRoot, lockDirectory)) {
|
|
110
|
+
throw new Error("the note lock root must remain outside the vault");
|
|
111
|
+
}
|
|
112
|
+
return join(lockDirectory, `${sha256(canonicalNoteId)}.lock`);
|
|
113
|
+
}
|
|
114
|
+
function parseOwner(value) {
|
|
115
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
116
|
+
return null;
|
|
117
|
+
const record = value;
|
|
118
|
+
const version = record["version"];
|
|
119
|
+
const pid = record["pid"];
|
|
120
|
+
const token = record["token"];
|
|
121
|
+
const acquiredAt = record["acquiredAt"];
|
|
122
|
+
if (version !== LOCK_SCHEMA_VERSION || typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0 || typeof token !== "string" || !/^[0-9a-z-]{16,128}$/iu.test(token) || typeof acquiredAt !== "string" || Number.isNaN(Date.parse(acquiredAt))) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return { version, pid, token, acquiredAt };
|
|
126
|
+
}
|
|
127
|
+
async function observeLock(path) {
|
|
128
|
+
let metadata;
|
|
129
|
+
try {
|
|
130
|
+
metadata = await lstat(path, { bigint: true });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (isErrno(error, "ENOENT"))
|
|
133
|
+
return null;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n || metadata.size > BigInt(MAX_LOCK_BYTES)) {
|
|
137
|
+
return { kind: "unsafe" };
|
|
138
|
+
}
|
|
139
|
+
let handle;
|
|
140
|
+
try {
|
|
141
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return isErrno(error, "ENOENT") ? null : { kind: "unsafe" };
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const opened = await handle.stat({ bigint: true });
|
|
147
|
+
if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== metadata.dev || opened.ino !== metadata.ino || opened.size !== metadata.size || opened.size > BigInt(MAX_LOCK_BYTES)) {
|
|
148
|
+
return { kind: "unsafe" };
|
|
149
|
+
}
|
|
150
|
+
const bytes = new Uint8Array(Number(opened.size));
|
|
151
|
+
let offset = 0;
|
|
152
|
+
while (offset < bytes.byteLength) {
|
|
153
|
+
const result = await handle.read(bytes, offset, bytes.byteLength - offset, offset);
|
|
154
|
+
if (result.bytesRead === 0)
|
|
155
|
+
return { kind: "unsafe" };
|
|
156
|
+
offset += result.bytesRead;
|
|
157
|
+
}
|
|
158
|
+
const overflow = new Uint8Array(1);
|
|
159
|
+
if ((await handle.read(overflow, 0, 1, Number(opened.size))).bytesRead !== 0) {
|
|
160
|
+
return { kind: "unsafe" };
|
|
161
|
+
}
|
|
162
|
+
const finished = await handle.stat({ bigint: true });
|
|
163
|
+
let finalPath;
|
|
164
|
+
try {
|
|
165
|
+
finalPath = await lstat(path, { bigint: true });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (isErrno(error, "ENOENT"))
|
|
168
|
+
return null;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
if (!finalPath.isFile() || finalPath.isSymbolicLink() || finalPath.nlink !== 1n || finalPath.dev !== opened.dev || finalPath.ino !== opened.ino || finalPath.size !== opened.size || finished.size !== opened.size || finished.mtimeNs !== opened.mtimeNs || finished.ctimeNs !== opened.ctimeNs) {
|
|
172
|
+
return { kind: "unsafe" };
|
|
173
|
+
}
|
|
174
|
+
let value;
|
|
175
|
+
try {
|
|
176
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
177
|
+
} catch {
|
|
178
|
+
value = null;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
kind: "regular",
|
|
182
|
+
device: opened.dev,
|
|
183
|
+
inode: opened.ino,
|
|
184
|
+
modifiedAtMs: Number(opened.mtimeMs),
|
|
185
|
+
owner: parseOwner(value)
|
|
186
|
+
};
|
|
187
|
+
} finally {
|
|
188
|
+
await handle.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function sameIdentity(left, right) {
|
|
192
|
+
return left.device === right.device && left.inode === right.inode;
|
|
193
|
+
}
|
|
194
|
+
async function restoreUnexpectedLock(tombstone, lockPath) {
|
|
195
|
+
try {
|
|
196
|
+
await link(tombstone, lockPath);
|
|
197
|
+
} catch {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await unlink(tombstone);
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
204
|
+
async function reclaimObservedLock(lockPath, observed, token, dependencies) {
|
|
205
|
+
const tombstone = `${lockPath}.stale-${token}`;
|
|
206
|
+
try {
|
|
207
|
+
await rename(lockPath, tombstone);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (isErrno(error, "ENOENT") || isErrno(error, "EEXIST"))
|
|
210
|
+
return false;
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
await dependencies.afterTombstoneMove?.(tombstone, lockPath);
|
|
214
|
+
const moved = await observeLock(tombstone);
|
|
215
|
+
if (moved === null || moved.kind !== "regular" || !sameIdentity(observed, moved)) {
|
|
216
|
+
await restoreUnexpectedLock(tombstone, lockPath);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
await unlink(tombstone);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
async function releaseOwnedLock(lockPath, owner, dependencies) {
|
|
223
|
+
const tombstone = `${lockPath}.release-${owner.token}`;
|
|
224
|
+
try {
|
|
225
|
+
await rename(lockPath, tombstone);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (isErrno(error, "ENOENT"))
|
|
228
|
+
return;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
await dependencies.afterTombstoneMove?.(tombstone, lockPath);
|
|
232
|
+
const moved = await observeLock(tombstone);
|
|
233
|
+
if (moved?.kind !== "regular" || moved.owner?.token !== owner.token) {
|
|
234
|
+
await restoreUnexpectedLock(tombstone, lockPath);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
await unlink(tombstone);
|
|
238
|
+
}
|
|
239
|
+
async function tryAcquire(lockPath, dependencies) {
|
|
240
|
+
for (let attempt = 0;attempt < MAX_RECLAIM_ATTEMPTS; attempt += 1) {
|
|
241
|
+
const owner = {
|
|
242
|
+
version: LOCK_SCHEMA_VERSION,
|
|
243
|
+
pid: dependencies.pid,
|
|
244
|
+
token: dependencies.token(),
|
|
245
|
+
acquiredAt: dependencies.now().toISOString()
|
|
246
|
+
};
|
|
247
|
+
let handle;
|
|
248
|
+
try {
|
|
249
|
+
handle = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 384);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (!isErrno(error, "EEXIST"))
|
|
252
|
+
throw error;
|
|
253
|
+
const observed = await observeLock(lockPath);
|
|
254
|
+
if (observed === null)
|
|
255
|
+
continue;
|
|
256
|
+
if (observed.kind === "unsafe")
|
|
257
|
+
throw new NoteLockBusyError(lockPath);
|
|
258
|
+
const ageMs = Math.max(0, dependencies.now().getTime() - observed.modifiedAtMs);
|
|
259
|
+
const ownerAlive = observed.owner !== null && dependencies.isProcessAlive(observed.owner.pid);
|
|
260
|
+
if (ownerAlive || observed.owner === null && ageMs <= dependencies.staleAfterMs) {
|
|
261
|
+
throw new NoteLockBusyError(lockPath);
|
|
262
|
+
}
|
|
263
|
+
if (await reclaimObservedLock(lockPath, observed, owner.token, dependencies)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
await handle.writeFile(`${JSON.stringify(owner)}
|
|
270
|
+
`, { encoding: "utf8" });
|
|
271
|
+
await handle.sync();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
await handle.close().catch(() => {
|
|
274
|
+
return;
|
|
275
|
+
});
|
|
276
|
+
const created = await observeLock(lockPath);
|
|
277
|
+
if (created?.kind === "regular") {
|
|
278
|
+
await reclaimObservedLock(lockPath, created, owner.token, dependencies).catch(() => {
|
|
279
|
+
return;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
let leaseOperationTail = Promise.resolve();
|
|
285
|
+
const runLeaseOperation = (operation) => {
|
|
286
|
+
const result = leaseOperationTail.then(operation);
|
|
287
|
+
leaseOperationTail = result.then(() => {
|
|
288
|
+
return;
|
|
289
|
+
}, () => {
|
|
290
|
+
return;
|
|
291
|
+
});
|
|
292
|
+
return result;
|
|
293
|
+
};
|
|
294
|
+
let heartbeatQueued = false;
|
|
295
|
+
let released = false;
|
|
296
|
+
let releasePromise;
|
|
297
|
+
const timer = setInterval(() => {
|
|
298
|
+
if (heartbeatQueued || releasePromise !== undefined)
|
|
299
|
+
return;
|
|
300
|
+
heartbeatQueued = true;
|
|
301
|
+
runLeaseOperation(async () => {
|
|
302
|
+
if (released)
|
|
303
|
+
return;
|
|
304
|
+
const now = dependencies.now();
|
|
305
|
+
await handle.utimes(now, now);
|
|
306
|
+
}).catch(() => {
|
|
307
|
+
return;
|
|
308
|
+
}).finally(() => {
|
|
309
|
+
heartbeatQueued = false;
|
|
310
|
+
});
|
|
311
|
+
}, dependencies.heartbeatMs);
|
|
312
|
+
timer.unref();
|
|
313
|
+
return {
|
|
314
|
+
path: lockPath,
|
|
315
|
+
assertOwned: () => runLeaseOperation(async () => {
|
|
316
|
+
if (released)
|
|
317
|
+
throw new NoteLockLostError(lockPath);
|
|
318
|
+
const observed = await observeLock(lockPath);
|
|
319
|
+
if (observed?.kind !== "regular" || observed.owner?.token !== owner.token) {
|
|
320
|
+
throw new NoteLockLostError(lockPath);
|
|
321
|
+
}
|
|
322
|
+
}),
|
|
323
|
+
release: async () => {
|
|
324
|
+
if (releasePromise !== undefined)
|
|
325
|
+
return releasePromise;
|
|
326
|
+
clearInterval(timer);
|
|
327
|
+
releasePromise = runLeaseOperation(async () => {
|
|
328
|
+
released = true;
|
|
329
|
+
await handle.close();
|
|
330
|
+
await releaseOwnedLock(lockPath, owner, dependencies);
|
|
331
|
+
});
|
|
332
|
+
return releasePromise;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
throw new NoteLockBusyError(lockPath);
|
|
337
|
+
}
|
|
338
|
+
async function acquireNoteLock(vaultRoot, canonicalNoteId, options = {}) {
|
|
339
|
+
const lockPath = await lockPathFor(vaultRoot, canonicalNoteId, options.cacheHome);
|
|
340
|
+
return acquireFileLease(lockPath, options);
|
|
341
|
+
}
|
|
342
|
+
async function acquireFileLease(lockPathInput, options = {}) {
|
|
343
|
+
if (typeof lockPathInput !== "string" || lockPathInput.trim() === "") {
|
|
344
|
+
throw new TypeError("a file lease requires a non-empty path");
|
|
345
|
+
}
|
|
346
|
+
const lockPath = resolve(lockPathInput);
|
|
347
|
+
const requestedDirectory = dirname(lockPath);
|
|
348
|
+
const directory = await realpath(requestedDirectory);
|
|
349
|
+
const metadata = await lstat(directory);
|
|
350
|
+
if (directory !== requestedDirectory || !metadata.isDirectory() || metadata.isSymbolicLink() || dirname(lockPath) !== directory) {
|
|
351
|
+
throw new Error("the file lease parent must be one canonical real directory");
|
|
352
|
+
}
|
|
353
|
+
const dependencies = resolvedDependencies(options.dependencies);
|
|
354
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
355
|
+
if (!Number.isFinite(waitTimeoutMs) || waitTimeoutMs < 0) {
|
|
356
|
+
throw new TypeError("waitTimeoutMs must be a non-negative finite duration");
|
|
357
|
+
}
|
|
358
|
+
const deadline = dependencies.monotonicNow() + waitTimeoutMs;
|
|
359
|
+
for (;; ) {
|
|
360
|
+
try {
|
|
361
|
+
return await tryAcquire(lockPath, dependencies);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (!(error instanceof NoteLockBusyError))
|
|
364
|
+
throw error;
|
|
365
|
+
const remaining = deadline - dependencies.monotonicNow();
|
|
366
|
+
if (remaining <= 0)
|
|
367
|
+
throw error;
|
|
368
|
+
await dependencies.sleep(Math.min(dependencies.pollIntervalMs, remaining));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export { acquireNoteLock, acquireFileLease };
|