sphica 0.5.6 → 0.5.7
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/capture.js +23 -15
- package/dist/cli.js +23 -15
- package/dist/deliver.js +262 -254
- package/dist/mcp-record.js +177 -38
- package/dist/mcp.js +5 -6
- package/package.json +1 -1
- package/skills/glean/SKILL.md +2 -2
- package/skills/harvest/SKILL.md +1 -1
- package/skills/trace/SKILL.md +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "sphica",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.7",
|
|
5
5
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "iroha924",
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ The agent searches with Sphica's `search` and opens full records with `read`. `s
|
|
|
118
118
|
- **What.** Your prompts, the agent's final reply for each turn, and the paths of changed files. Background-task notifications and messages from other agents are skipped when Sphica recognizes their format. Replies in the middle of a turn are not kept, nor are files created and deleted within one turn without the edit tools.
|
|
119
119
|
- **What was shown.** Each automatic delivery is logged by which records it showed, not their text.
|
|
120
120
|
- **Unregistered repositories.** Sessions in a repository you have not registered stay in the queue and are written after you register it. Held sessions are dropped after 30 days, and when more than 1,000 are waiting the oldest go first.
|
|
121
|
-
- **Secrets.**
|
|
121
|
+
- **Secrets.** In your prompts, pull request text, and the file lines `/sphica:glean` cites, only secrets with a recognizable shape are masked (excerpts saved before 0.5.7 stay as they were):
|
|
122
122
|
- keys with known prefixes
|
|
123
123
|
- `KEY=…` and `"password": …` assignments
|
|
124
124
|
- credentials in URLs
|
package/dist/capture.js
CHANGED
|
@@ -11108,24 +11108,32 @@ var MYSQL_PASSWORD = /(\s-p)(?:'[^'\n]*'|"[^"\n]*"|(?=[^\s-])\S+)/;
|
|
|
11108
11108
|
var URL_CREDENTIALS = /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|https?):\/\/[^:\s/@]*:)[^\s/]*@([^@\s/?#]+)/g;
|
|
11109
11109
|
var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
|
|
11110
11110
|
var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
11111
|
-
function
|
|
11111
|
+
function keyBlocks(text) {
|
|
11112
11112
|
const ends = [...text.matchAll(KEY_END)].map((m) => [m.index, m.index + m[0].length]);
|
|
11113
|
-
|
|
11114
|
-
return text;
|
|
11115
|
-
let out = "";
|
|
11113
|
+
const out = [];
|
|
11116
11114
|
let last = 0;
|
|
11117
11115
|
let e = 0;
|
|
11118
|
-
|
|
11119
|
-
const
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
e
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11116
|
+
if (ends.length > 0)
|
|
11117
|
+
for (const m of text.matchAll(KEY_BEGIN)) {
|
|
11118
|
+
const after = m.index + m[0].length;
|
|
11119
|
+
if (m.index < last)
|
|
11120
|
+
continue;
|
|
11121
|
+
while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
|
|
11122
|
+
e++;
|
|
11123
|
+
const end = ends[e];
|
|
11124
|
+
if (!end)
|
|
11125
|
+
break;
|
|
11126
|
+
out.push([m.index, end[1]]);
|
|
11127
|
+
last = end[1];
|
|
11128
|
+
}
|
|
11129
|
+
return out;
|
|
11130
|
+
}
|
|
11131
|
+
function maskPrivateKeys(text) {
|
|
11132
|
+
let out = "";
|
|
11133
|
+
let last = 0;
|
|
11134
|
+
for (const [a, b] of keyBlocks(text)) {
|
|
11135
|
+
out += `${text.slice(last, a)}[redacted: private key]`;
|
|
11136
|
+
last = b;
|
|
11129
11137
|
}
|
|
11130
11138
|
return out + text.slice(last);
|
|
11131
11139
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3193,24 +3193,32 @@ var MYSQL_PASSWORD = /(\s-p)(?:'[^'\n]*'|"[^"\n]*"|(?=[^\s-])\S+)/;
|
|
|
3193
3193
|
var URL_CREDENTIALS = /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|https?):\/\/[^:\s/@]*:)[^\s/]*@([^@\s/?#]+)/g;
|
|
3194
3194
|
var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
|
|
3195
3195
|
var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
3196
|
-
function
|
|
3196
|
+
function keyBlocks(text) {
|
|
3197
3197
|
const ends = [...text.matchAll(KEY_END)].map((m) => [m.index, m.index + m[0].length]);
|
|
3198
|
-
|
|
3199
|
-
return text;
|
|
3200
|
-
let out = "";
|
|
3198
|
+
const out = [];
|
|
3201
3199
|
let last = 0;
|
|
3202
3200
|
let e = 0;
|
|
3203
|
-
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
e
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3201
|
+
if (ends.length > 0)
|
|
3202
|
+
for (const m of text.matchAll(KEY_BEGIN)) {
|
|
3203
|
+
const after = m.index + m[0].length;
|
|
3204
|
+
if (m.index < last)
|
|
3205
|
+
continue;
|
|
3206
|
+
while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
|
|
3207
|
+
e++;
|
|
3208
|
+
const end = ends[e];
|
|
3209
|
+
if (!end)
|
|
3210
|
+
break;
|
|
3211
|
+
out.push([m.index, end[1]]);
|
|
3212
|
+
last = end[1];
|
|
3213
|
+
}
|
|
3214
|
+
return out;
|
|
3215
|
+
}
|
|
3216
|
+
function maskPrivateKeys(text) {
|
|
3217
|
+
let out = "";
|
|
3218
|
+
let last = 0;
|
|
3219
|
+
for (const [a2, b] of keyBlocks(text)) {
|
|
3220
|
+
out += `${text.slice(last, a2)}[redacted: private key]`;
|
|
3221
|
+
last = b;
|
|
3214
3222
|
}
|
|
3215
3223
|
return out + text.slice(last);
|
|
3216
3224
|
}
|
package/dist/deliver.js
CHANGED
|
@@ -21,6 +21,255 @@ import path7 from "node:path";
|
|
|
21
21
|
|
|
22
22
|
// server/src/anchors.ts
|
|
23
23
|
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
// server/src/text.ts
|
|
26
|
+
import crypto from "node:crypto";
|
|
27
|
+
var segmenter = new Intl.Segmenter("ja", { granularity: "word" });
|
|
28
|
+
var HIRAGANA_ONLY = /^[\p{Script=Hiragana}ー]+$/u;
|
|
29
|
+
var STOP = new Set(["the", "a", "an", "of", "to", "in", "is", "and", "or", "for", "on", "it", "be"]);
|
|
30
|
+
var IDENT = /#\d+|[a-z0-9][a-z0-9_./#-]*[a-z0-9]/g;
|
|
31
|
+
var MAX_TERM = 100;
|
|
32
|
+
var OKURIGANA = /^(\p{Script=Han}+)[\p{Script=Hiragana}ー]+$/u;
|
|
33
|
+
function singular(w) {
|
|
34
|
+
if (!/^[a-z]{4,}$/.test(w))
|
|
35
|
+
return w;
|
|
36
|
+
if (w.endsWith("ies"))
|
|
37
|
+
return `${w.slice(0, -3)}y`;
|
|
38
|
+
if (w.endsWith("ie"))
|
|
39
|
+
return `${w.slice(0, -2)}y`;
|
|
40
|
+
if (/(?:ss|us|x|z|ch|sh)es$/.test(w))
|
|
41
|
+
return w.slice(0, -2);
|
|
42
|
+
if (/(?:ch|sh|us)e$/.test(w))
|
|
43
|
+
return w.slice(0, -1);
|
|
44
|
+
return w.endsWith("s") && !/(?:ss|us|is)$/.test(w) ? w.slice(0, -1) : w;
|
|
45
|
+
}
|
|
46
|
+
function terms(text) {
|
|
47
|
+
const norm = text.normalize("NFKC").toLowerCase();
|
|
48
|
+
const out = [];
|
|
49
|
+
const keep = (w) => {
|
|
50
|
+
if (w.length > MAX_TERM || STOP.has(w) || HIRAGANA_ONLY.test(w))
|
|
51
|
+
return;
|
|
52
|
+
out.push(OKURIGANA.exec(w)?.[1] ?? singular(w));
|
|
53
|
+
};
|
|
54
|
+
for (const s of segmenter.segment(norm))
|
|
55
|
+
if (s.isWordLike)
|
|
56
|
+
keep(s.segment.trim());
|
|
57
|
+
for (const m of norm.matchAll(IDENT))
|
|
58
|
+
if (m[0].length >= 3)
|
|
59
|
+
keep(m[0]);
|
|
60
|
+
return out.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
var QUESTION = new Set([
|
|
63
|
+
...[
|
|
64
|
+
"why",
|
|
65
|
+
"what",
|
|
66
|
+
"which",
|
|
67
|
+
"when",
|
|
68
|
+
"where",
|
|
69
|
+
"who",
|
|
70
|
+
"how",
|
|
71
|
+
"do",
|
|
72
|
+
"does",
|
|
73
|
+
"did",
|
|
74
|
+
"we",
|
|
75
|
+
"our",
|
|
76
|
+
"us",
|
|
77
|
+
"you",
|
|
78
|
+
"i",
|
|
79
|
+
"are",
|
|
80
|
+
"was",
|
|
81
|
+
"were"
|
|
82
|
+
],
|
|
83
|
+
...[
|
|
84
|
+
"will",
|
|
85
|
+
"would",
|
|
86
|
+
"should",
|
|
87
|
+
"can",
|
|
88
|
+
"could",
|
|
89
|
+
"there",
|
|
90
|
+
"this",
|
|
91
|
+
"that",
|
|
92
|
+
"with",
|
|
93
|
+
"from",
|
|
94
|
+
"any",
|
|
95
|
+
"ever",
|
|
96
|
+
"long",
|
|
97
|
+
"use",
|
|
98
|
+
"used",
|
|
99
|
+
"using"
|
|
100
|
+
],
|
|
101
|
+
...["add", "get", "make", "have", "has", "not", "no", "yes", "reason"],
|
|
102
|
+
...["理由", "仕組", "何", "方", "場合", "今", "件"]
|
|
103
|
+
]);
|
|
104
|
+
var sha256 = (s) => crypto.createHash("sha256").update(s).digest();
|
|
105
|
+
function uuidFrom(...parts) {
|
|
106
|
+
const b = crypto.createHash("sha256").update(parts.join("\x00")).digest().subarray(0, 16);
|
|
107
|
+
b[6] = (b[6] ?? 0) & 15 | 128;
|
|
108
|
+
b[8] = (b[8] ?? 0) & 63 | 128;
|
|
109
|
+
const h = b.toString("hex");
|
|
110
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
111
|
+
}
|
|
112
|
+
var bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
113
|
+
function head(s, n) {
|
|
114
|
+
if (bytes(s) <= n)
|
|
115
|
+
return s;
|
|
116
|
+
let out = "";
|
|
117
|
+
let used = 0;
|
|
118
|
+
for (const ch of s) {
|
|
119
|
+
const b = bytes(ch);
|
|
120
|
+
if (used + b > n)
|
|
121
|
+
break;
|
|
122
|
+
out += ch;
|
|
123
|
+
used += b;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function tail(s, n) {
|
|
128
|
+
if (bytes(s) <= n)
|
|
129
|
+
return s;
|
|
130
|
+
const chars = [...s];
|
|
131
|
+
let used = 0;
|
|
132
|
+
let i = chars.length;
|
|
133
|
+
while (i > 0) {
|
|
134
|
+
const b = bytes(chars[i - 1] ?? "");
|
|
135
|
+
if (used + b > n)
|
|
136
|
+
break;
|
|
137
|
+
used += b;
|
|
138
|
+
i--;
|
|
139
|
+
}
|
|
140
|
+
return chars.slice(i).join("");
|
|
141
|
+
}
|
|
142
|
+
var clean = (s) => s.replaceAll("\x00", "");
|
|
143
|
+
var visible = (s) => s.replace(/(?!\p{Join_Control}|\p{Variation_Selector})\p{Default_Ignorable_Code_Point}/gu, "");
|
|
144
|
+
var SECRETS = [
|
|
145
|
+
[/\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{20,}/g, "API key"],
|
|
146
|
+
[/\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g, "API key"],
|
|
147
|
+
[/\bwhsec_[A-Za-z0-9+/=]{16,}/g, "webhook signing secret"],
|
|
148
|
+
[/\bpa-[A-Za-z0-9_-]{20,}/g, "API key"],
|
|
149
|
+
[/\bAIza[0-9A-Za-z_-]{35}/g, "API key"],
|
|
150
|
+
[/\bnpg_[A-Za-z0-9]{12,}/g, "database password"],
|
|
151
|
+
[/\bnapi_[A-Za-z0-9]{30,}/g, "API key"],
|
|
152
|
+
[/\bnpm_[A-Za-z0-9]{36}\b/g, "npm token"],
|
|
153
|
+
[/\bglpat-[A-Za-z0-9_-]{20,}/g, "GitLab token"],
|
|
154
|
+
[/\bgh[pousr]_[A-Za-z0-9]{30,}/g, "GitHub token"],
|
|
155
|
+
[/\bgithub_pat_[A-Za-z0-9_]{40,}/g, "GitHub token"],
|
|
156
|
+
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, "Slack token"],
|
|
157
|
+
[/https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/]+/g, "Slack webhook"],
|
|
158
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "AWS key"],
|
|
159
|
+
[/(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "JWT"],
|
|
160
|
+
[/\b(?:Bearer|BEARER)\s+(?=[A-Za-z0-9._~+/=-]{0,512}\d)[A-Za-z0-9._~+/=-]{16,}/g, "auth header value"]
|
|
161
|
+
];
|
|
162
|
+
var AUTH_HEADER = /(\bAuthorization["']?\s*[:=]\s*(?:["']\s*)?(?:Bearer|Basic|Token|Digest)\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
163
|
+
var HEADER_BEARER = /(:[ \t]*(?:["'][ \t]*)?bearer[ \t]+)[A-Za-z0-9._~+/=-]{16,}/gi;
|
|
164
|
+
var ENV_ASSIGN = /\b((?:[A-Z][A-Z0-9_]*_)?(?:API|SECRET|MASTER|ENCRYPTION|PRIVATE|ACCESS|SIGNING|AUTH)?KEY|[A-Z][A-Z0-9_]*_(?:PASS|PWD)|(?:[A-Z][A-Z0-9_]*?)?(?:TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?))(\s*=\s*)(?:"(?!\$)[^"\n]+"|'(?!\$)[^'\n]+'|(?![$"'])[^\s"']+)/g;
|
|
165
|
+
var FIELD_NAME = /(?:(?:api|account|access|private|secret)[-_]?key|secret|token|passw(?:or)?d)["']?\s*(?::=|=>|[:=])\s*/gi;
|
|
166
|
+
var MAX_QUOTED = 4096;
|
|
167
|
+
var BARE_HEAD = /[^\s"',;)]{1,256}/y;
|
|
168
|
+
var BARE_REST = /[^\s"',;)]*/y;
|
|
169
|
+
var NEXT_PARAM = /&[A-Za-z_][\w.-]*=/;
|
|
170
|
+
var bareAt = (re, text, at) => {
|
|
171
|
+
re.lastIndex = at;
|
|
172
|
+
const v = re.exec(text)?.[0] ?? "";
|
|
173
|
+
const cut = v.search(NEXT_PARAM);
|
|
174
|
+
return cut < 0 ? v : v.slice(0, cut);
|
|
175
|
+
};
|
|
176
|
+
function secretValue(quoted, v) {
|
|
177
|
+
if (v.length < 8 || /^\$(?:\{|[A-Za-z_])/.test(v))
|
|
178
|
+
return false;
|
|
179
|
+
if (!quoted)
|
|
180
|
+
return !/^[#$]/.test(v) && /\d/.test(v) && /[A-Za-z]/.test(v) && !/[()]/.test(v);
|
|
181
|
+
if (/^#[0-9a-f]{3,8}$/i.test(v) || !/[A-Za-z0-9]/.test(v))
|
|
182
|
+
return false;
|
|
183
|
+
return !/\s/.test(v) || v.split(/\s+/).some((w) => /\d/.test(w) && /[A-Za-z]/.test(w));
|
|
184
|
+
}
|
|
185
|
+
function maskFields(text) {
|
|
186
|
+
let out = "";
|
|
187
|
+
let last = 0;
|
|
188
|
+
FIELD_NAME.lastIndex = 0;
|
|
189
|
+
for (let m = FIELD_NAME.exec(text);m; m = FIELD_NAME.exec(text)) {
|
|
190
|
+
const at = m.index + m[0].length;
|
|
191
|
+
const q = text[at];
|
|
192
|
+
let quote = "";
|
|
193
|
+
let value;
|
|
194
|
+
if (q === '"' || q === "'") {
|
|
195
|
+
const close = text.indexOf(q, at + 1);
|
|
196
|
+
if (close < 0 || close - at - 1 > MAX_QUOTED)
|
|
197
|
+
continue;
|
|
198
|
+
value = text.slice(at + 1, close);
|
|
199
|
+
if (value.includes(`
|
|
200
|
+
`))
|
|
201
|
+
continue;
|
|
202
|
+
quote = q;
|
|
203
|
+
} else {
|
|
204
|
+
value = bareAt(BARE_HEAD, text, at);
|
|
205
|
+
}
|
|
206
|
+
if (!secretValue(quote !== "", value))
|
|
207
|
+
continue;
|
|
208
|
+
if (!quote && value.length === 256)
|
|
209
|
+
value += bareAt(BARE_REST, text, at + 256);
|
|
210
|
+
out += `${text.slice(last, at)}${quote}[redacted]`;
|
|
211
|
+
last = at + quote.length + value.length;
|
|
212
|
+
FIELD_NAME.lastIndex = last;
|
|
213
|
+
}
|
|
214
|
+
return out + text.slice(last);
|
|
215
|
+
}
|
|
216
|
+
var MYSQL_COMMAND = /\bmysql(?:dump|admin)?\b(?:'[^'\n]*'|"[^"\n]*"|[^\n;&|\\'"]|\\\r?\n|\\(?!\r?\n))*/g;
|
|
217
|
+
var MYSQL_PASSWORD = /(\s-p)(?:'[^'\n]*'|"[^"\n]*"|(?=[^\s-])\S+)/;
|
|
218
|
+
var URL_CREDENTIALS = /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|https?):\/\/[^:\s/@]*:)[^\s/]*@([^@\s/?#]+)/g;
|
|
219
|
+
var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
|
|
220
|
+
var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
221
|
+
function keyBlocks(text) {
|
|
222
|
+
const ends = [...text.matchAll(KEY_END)].map((m) => [m.index, m.index + m[0].length]);
|
|
223
|
+
const out = [];
|
|
224
|
+
let last = 0;
|
|
225
|
+
let e = 0;
|
|
226
|
+
if (ends.length > 0)
|
|
227
|
+
for (const m of text.matchAll(KEY_BEGIN)) {
|
|
228
|
+
const after = m.index + m[0].length;
|
|
229
|
+
if (m.index < last)
|
|
230
|
+
continue;
|
|
231
|
+
while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
|
|
232
|
+
e++;
|
|
233
|
+
const end = ends[e];
|
|
234
|
+
if (!end)
|
|
235
|
+
break;
|
|
236
|
+
out.push([m.index, end[1]]);
|
|
237
|
+
last = end[1];
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
function maskPrivateKeys(text) {
|
|
242
|
+
let out = "";
|
|
243
|
+
let last = 0;
|
|
244
|
+
for (const [a, b] of keyBlocks(text)) {
|
|
245
|
+
out += `${text.slice(last, a)}[redacted: private key]`;
|
|
246
|
+
last = b;
|
|
247
|
+
}
|
|
248
|
+
return out + text.slice(last);
|
|
249
|
+
}
|
|
250
|
+
function mask(text) {
|
|
251
|
+
let out = maskFields(maskPrivateKeys(text).replace(URL_CREDENTIALS, "$1[redacted]@$2").replace(AUTH_HEADER, "$1[redacted]").replace(HEADER_BEARER, "$1[redacted]").replace(ENV_ASSIGN, "$1$2[redacted]")).replace(MYSQL_COMMAND, (command) => command.replace(MYSQL_PASSWORD, "$1[redacted]"));
|
|
252
|
+
for (const [re, what] of SECRETS)
|
|
253
|
+
out = out.replace(re, `[redacted: ${what}]`);
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
var reason = (e) => explain(e, 0) || "unknown failure";
|
|
257
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
258
|
+
function explain(e, depth) {
|
|
259
|
+
if (!(e instanceof Error)) {
|
|
260
|
+
try {
|
|
261
|
+
return String(e);
|
|
262
|
+
} catch {
|
|
263
|
+
return "";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const own = e.message || (e.name === "Error" || e.name === "AggregateError" ? "" : e.name);
|
|
267
|
+
const parts = depth >= 3 ? [] : [...e instanceof AggregateError ? e.errors : [], ...e.cause === undefined ? [] : [e.cause]];
|
|
268
|
+
const inner = parts.map((x) => explain(x, depth + 1)).filter(Boolean).join(" / ");
|
|
269
|
+
return own && inner ? `${own} (${inner})` : own || inner;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// server/src/anchors.ts
|
|
24
273
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
25
274
|
var leaves = (rel) => rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
|
|
26
275
|
|
|
@@ -4154,8 +4403,8 @@ class Deferred {
|
|
|
4154
4403
|
this.#resolve?.(value);
|
|
4155
4404
|
this.#resolve = this.#reject = undefined;
|
|
4156
4405
|
};
|
|
4157
|
-
reject = (
|
|
4158
|
-
this.#reject?.(
|
|
4406
|
+
reject = (reason2) => {
|
|
4407
|
+
this.#reject?.(reason2);
|
|
4159
4408
|
this.#reject = this.#resolve = undefined;
|
|
4160
4409
|
};
|
|
4161
4410
|
}
|
|
@@ -4207,9 +4456,9 @@ function assertNotAborted(signal, timing, beforeThrow) {
|
|
|
4207
4456
|
throwReasonWithTiming(signal.reason, timing);
|
|
4208
4457
|
}
|
|
4209
4458
|
}
|
|
4210
|
-
function throwReasonWithTiming(
|
|
4211
|
-
decorateWithTiming(
|
|
4212
|
-
throw
|
|
4459
|
+
function throwReasonWithTiming(reason2, timing) {
|
|
4460
|
+
decorateWithTiming(reason2, timing);
|
|
4461
|
+
throw reason2;
|
|
4213
4462
|
}
|
|
4214
4463
|
var ABORTED = {};
|
|
4215
4464
|
async function waitOrAbort(promise, signal, name, onAbort) {
|
|
@@ -4234,11 +4483,11 @@ async function waitOrAbort(promise, signal, name, onAbort) {
|
|
|
4234
4483
|
}
|
|
4235
4484
|
}
|
|
4236
4485
|
function printBackgroundFail(name) {
|
|
4237
|
-
return (
|
|
4486
|
+
return (reason2) => console.error(`\`${name}\` failed in the background after abortion: ${getMessage(reason2)}`);
|
|
4238
4487
|
}
|
|
4239
|
-
function decorateWithTiming(
|
|
4240
|
-
if (
|
|
4241
|
-
Object.defineProperty(
|
|
4488
|
+
function decorateWithTiming(reason2, timing) {
|
|
4489
|
+
if (reason2 !== null && typeof reason2 === "object" && !Object.isFrozen(reason2)) {
|
|
4490
|
+
Object.defineProperty(reason2, "__kysely_timing__", {
|
|
4242
4491
|
configurable: true,
|
|
4243
4492
|
enumerable: false,
|
|
4244
4493
|
value: timing,
|
|
@@ -8163,9 +8412,9 @@ class RuntimeDriver {
|
|
|
8163
8412
|
}
|
|
8164
8413
|
this.#initPromise ??= this.#driver.init(options).then(() => {
|
|
8165
8414
|
this.#initDone = true;
|
|
8166
|
-
}).catch((
|
|
8415
|
+
}).catch((reason2) => {
|
|
8167
8416
|
this.#initPromise = undefined;
|
|
8168
|
-
throw
|
|
8417
|
+
throw reason2;
|
|
8169
8418
|
});
|
|
8170
8419
|
await waitOrAbort(this.#initPromise, options?.signal, "init");
|
|
8171
8420
|
}
|
|
@@ -8226,9 +8475,9 @@ class RuntimeDriver {
|
|
|
8226
8475
|
return;
|
|
8227
8476
|
}
|
|
8228
8477
|
await waitOrAbort(this.#initPromise, options?.signal, "destroy:initPromise");
|
|
8229
|
-
this.#destroyPromise ??= this.#driver.destroy(options).catch((
|
|
8478
|
+
this.#destroyPromise ??= this.#driver.destroy(options).catch((reason2) => {
|
|
8230
8479
|
this.#destroyPromise = undefined;
|
|
8231
|
-
throw
|
|
8480
|
+
throw reason2;
|
|
8232
8481
|
});
|
|
8233
8482
|
await waitOrAbort(this.#destroyPromise, options?.signal, "destroy");
|
|
8234
8483
|
}
|
|
@@ -10959,247 +11208,6 @@ function sqliteCode(e) {
|
|
|
10959
11208
|
|
|
10960
11209
|
// server/src/db-write.ts
|
|
10961
11210
|
import { constants as C2, DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
10962
|
-
|
|
10963
|
-
// server/src/text.ts
|
|
10964
|
-
import crypto from "node:crypto";
|
|
10965
|
-
var segmenter = new Intl.Segmenter("ja", { granularity: "word" });
|
|
10966
|
-
var HIRAGANA_ONLY = /^[\p{Script=Hiragana}ー]+$/u;
|
|
10967
|
-
var STOP = new Set(["the", "a", "an", "of", "to", "in", "is", "and", "or", "for", "on", "it", "be"]);
|
|
10968
|
-
var IDENT = /#\d+|[a-z0-9][a-z0-9_./#-]*[a-z0-9]/g;
|
|
10969
|
-
var MAX_TERM = 100;
|
|
10970
|
-
var OKURIGANA = /^(\p{Script=Han}+)[\p{Script=Hiragana}ー]+$/u;
|
|
10971
|
-
function singular(w) {
|
|
10972
|
-
if (!/^[a-z]{4,}$/.test(w))
|
|
10973
|
-
return w;
|
|
10974
|
-
if (w.endsWith("ies"))
|
|
10975
|
-
return `${w.slice(0, -3)}y`;
|
|
10976
|
-
if (w.endsWith("ie"))
|
|
10977
|
-
return `${w.slice(0, -2)}y`;
|
|
10978
|
-
if (/(?:ss|us|x|z|ch|sh)es$/.test(w))
|
|
10979
|
-
return w.slice(0, -2);
|
|
10980
|
-
if (/(?:ch|sh|us)e$/.test(w))
|
|
10981
|
-
return w.slice(0, -1);
|
|
10982
|
-
return w.endsWith("s") && !/(?:ss|us|is)$/.test(w) ? w.slice(0, -1) : w;
|
|
10983
|
-
}
|
|
10984
|
-
function terms(text) {
|
|
10985
|
-
const norm = text.normalize("NFKC").toLowerCase();
|
|
10986
|
-
const out = [];
|
|
10987
|
-
const keep = (w) => {
|
|
10988
|
-
if (w.length > MAX_TERM || STOP.has(w) || HIRAGANA_ONLY.test(w))
|
|
10989
|
-
return;
|
|
10990
|
-
out.push(OKURIGANA.exec(w)?.[1] ?? singular(w));
|
|
10991
|
-
};
|
|
10992
|
-
for (const s of segmenter.segment(norm))
|
|
10993
|
-
if (s.isWordLike)
|
|
10994
|
-
keep(s.segment.trim());
|
|
10995
|
-
for (const m of norm.matchAll(IDENT))
|
|
10996
|
-
if (m[0].length >= 3)
|
|
10997
|
-
keep(m[0]);
|
|
10998
|
-
return out.filter(Boolean);
|
|
10999
|
-
}
|
|
11000
|
-
var QUESTION = new Set([
|
|
11001
|
-
...[
|
|
11002
|
-
"why",
|
|
11003
|
-
"what",
|
|
11004
|
-
"which",
|
|
11005
|
-
"when",
|
|
11006
|
-
"where",
|
|
11007
|
-
"who",
|
|
11008
|
-
"how",
|
|
11009
|
-
"do",
|
|
11010
|
-
"does",
|
|
11011
|
-
"did",
|
|
11012
|
-
"we",
|
|
11013
|
-
"our",
|
|
11014
|
-
"us",
|
|
11015
|
-
"you",
|
|
11016
|
-
"i",
|
|
11017
|
-
"are",
|
|
11018
|
-
"was",
|
|
11019
|
-
"were"
|
|
11020
|
-
],
|
|
11021
|
-
...[
|
|
11022
|
-
"will",
|
|
11023
|
-
"would",
|
|
11024
|
-
"should",
|
|
11025
|
-
"can",
|
|
11026
|
-
"could",
|
|
11027
|
-
"there",
|
|
11028
|
-
"this",
|
|
11029
|
-
"that",
|
|
11030
|
-
"with",
|
|
11031
|
-
"from",
|
|
11032
|
-
"any",
|
|
11033
|
-
"ever",
|
|
11034
|
-
"long",
|
|
11035
|
-
"use",
|
|
11036
|
-
"used",
|
|
11037
|
-
"using"
|
|
11038
|
-
],
|
|
11039
|
-
...["add", "get", "make", "have", "has", "not", "no", "yes", "reason"],
|
|
11040
|
-
...["理由", "仕組", "何", "方", "場合", "今", "件"]
|
|
11041
|
-
]);
|
|
11042
|
-
var sha256 = (s) => crypto.createHash("sha256").update(s).digest();
|
|
11043
|
-
function uuidFrom(...parts) {
|
|
11044
|
-
const b = crypto.createHash("sha256").update(parts.join("\x00")).digest().subarray(0, 16);
|
|
11045
|
-
b[6] = (b[6] ?? 0) & 15 | 128;
|
|
11046
|
-
b[8] = (b[8] ?? 0) & 63 | 128;
|
|
11047
|
-
const h = b.toString("hex");
|
|
11048
|
-
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
11049
|
-
}
|
|
11050
|
-
var bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
11051
|
-
function head(s, n) {
|
|
11052
|
-
if (bytes(s) <= n)
|
|
11053
|
-
return s;
|
|
11054
|
-
let out = "";
|
|
11055
|
-
let used = 0;
|
|
11056
|
-
for (const ch of s) {
|
|
11057
|
-
const b = bytes(ch);
|
|
11058
|
-
if (used + b > n)
|
|
11059
|
-
break;
|
|
11060
|
-
out += ch;
|
|
11061
|
-
used += b;
|
|
11062
|
-
}
|
|
11063
|
-
return out;
|
|
11064
|
-
}
|
|
11065
|
-
function tail(s, n) {
|
|
11066
|
-
if (bytes(s) <= n)
|
|
11067
|
-
return s;
|
|
11068
|
-
const chars = [...s];
|
|
11069
|
-
let used = 0;
|
|
11070
|
-
let i = chars.length;
|
|
11071
|
-
while (i > 0) {
|
|
11072
|
-
const b = bytes(chars[i - 1] ?? "");
|
|
11073
|
-
if (used + b > n)
|
|
11074
|
-
break;
|
|
11075
|
-
used += b;
|
|
11076
|
-
i--;
|
|
11077
|
-
}
|
|
11078
|
-
return chars.slice(i).join("");
|
|
11079
|
-
}
|
|
11080
|
-
var clean = (s) => s.replaceAll("\x00", "");
|
|
11081
|
-
var visible = (s) => s.replace(/(?!\p{Join_Control}|\p{Variation_Selector})\p{Default_Ignorable_Code_Point}/gu, "");
|
|
11082
|
-
var SECRETS = [
|
|
11083
|
-
[/\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{20,}/g, "API key"],
|
|
11084
|
-
[/\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,}/g, "API key"],
|
|
11085
|
-
[/\bwhsec_[A-Za-z0-9+/=]{16,}/g, "webhook signing secret"],
|
|
11086
|
-
[/\bpa-[A-Za-z0-9_-]{20,}/g, "API key"],
|
|
11087
|
-
[/\bAIza[0-9A-Za-z_-]{35}/g, "API key"],
|
|
11088
|
-
[/\bnpg_[A-Za-z0-9]{12,}/g, "database password"],
|
|
11089
|
-
[/\bnapi_[A-Za-z0-9]{30,}/g, "API key"],
|
|
11090
|
-
[/\bnpm_[A-Za-z0-9]{36}\b/g, "npm token"],
|
|
11091
|
-
[/\bglpat-[A-Za-z0-9_-]{20,}/g, "GitLab token"],
|
|
11092
|
-
[/\bgh[pousr]_[A-Za-z0-9]{30,}/g, "GitHub token"],
|
|
11093
|
-
[/\bgithub_pat_[A-Za-z0-9_]{40,}/g, "GitHub token"],
|
|
11094
|
-
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, "Slack token"],
|
|
11095
|
-
[/https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/]+/g, "Slack webhook"],
|
|
11096
|
-
[/\bAKIA[0-9A-Z]{16}\b/g, "AWS key"],
|
|
11097
|
-
[/(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "JWT"],
|
|
11098
|
-
[/\b(?:Bearer|BEARER)\s+(?=[A-Za-z0-9._~+/=-]{0,512}\d)[A-Za-z0-9._~+/=-]{16,}/g, "auth header value"]
|
|
11099
|
-
];
|
|
11100
|
-
var AUTH_HEADER = /(\bAuthorization["']?\s*[:=]\s*(?:["']\s*)?(?:Bearer|Basic|Token|Digest)\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
11101
|
-
var HEADER_BEARER = /(:[ \t]*(?:["'][ \t]*)?bearer[ \t]+)[A-Za-z0-9._~+/=-]{16,}/gi;
|
|
11102
|
-
var ENV_ASSIGN = /\b((?:[A-Z][A-Z0-9_]*_)?(?:API|SECRET|MASTER|ENCRYPTION|PRIVATE|ACCESS|SIGNING|AUTH)?KEY|[A-Z][A-Z0-9_]*_(?:PASS|PWD)|(?:[A-Z][A-Z0-9_]*?)?(?:TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?))(\s*=\s*)(?:"(?!\$)[^"\n]+"|'(?!\$)[^'\n]+'|(?![$"'])[^\s"']+)/g;
|
|
11103
|
-
var FIELD_NAME = /(?:(?:api|account|access|private|secret)[-_]?key|secret|token|passw(?:or)?d)["']?\s*(?::=|=>|[:=])\s*/gi;
|
|
11104
|
-
var MAX_QUOTED = 4096;
|
|
11105
|
-
var BARE_HEAD = /[^\s"',;)]{1,256}/y;
|
|
11106
|
-
var BARE_REST = /[^\s"',;)]*/y;
|
|
11107
|
-
var NEXT_PARAM = /&[A-Za-z_][\w.-]*=/;
|
|
11108
|
-
var bareAt = (re, text, at) => {
|
|
11109
|
-
re.lastIndex = at;
|
|
11110
|
-
const v = re.exec(text)?.[0] ?? "";
|
|
11111
|
-
const cut = v.search(NEXT_PARAM);
|
|
11112
|
-
return cut < 0 ? v : v.slice(0, cut);
|
|
11113
|
-
};
|
|
11114
|
-
function secretValue(quoted, v) {
|
|
11115
|
-
if (v.length < 8 || /^\$(?:\{|[A-Za-z_])/.test(v))
|
|
11116
|
-
return false;
|
|
11117
|
-
if (!quoted)
|
|
11118
|
-
return !/^[#$]/.test(v) && /\d/.test(v) && /[A-Za-z]/.test(v) && !/[()]/.test(v);
|
|
11119
|
-
if (/^#[0-9a-f]{3,8}$/i.test(v) || !/[A-Za-z0-9]/.test(v))
|
|
11120
|
-
return false;
|
|
11121
|
-
return !/\s/.test(v) || v.split(/\s+/).some((w) => /\d/.test(w) && /[A-Za-z]/.test(w));
|
|
11122
|
-
}
|
|
11123
|
-
function maskFields(text) {
|
|
11124
|
-
let out = "";
|
|
11125
|
-
let last = 0;
|
|
11126
|
-
FIELD_NAME.lastIndex = 0;
|
|
11127
|
-
for (let m = FIELD_NAME.exec(text);m; m = FIELD_NAME.exec(text)) {
|
|
11128
|
-
const at = m.index + m[0].length;
|
|
11129
|
-
const q = text[at];
|
|
11130
|
-
let quote = "";
|
|
11131
|
-
let value;
|
|
11132
|
-
if (q === '"' || q === "'") {
|
|
11133
|
-
const close = text.indexOf(q, at + 1);
|
|
11134
|
-
if (close < 0 || close - at - 1 > MAX_QUOTED)
|
|
11135
|
-
continue;
|
|
11136
|
-
value = text.slice(at + 1, close);
|
|
11137
|
-
if (value.includes(`
|
|
11138
|
-
`))
|
|
11139
|
-
continue;
|
|
11140
|
-
quote = q;
|
|
11141
|
-
} else {
|
|
11142
|
-
value = bareAt(BARE_HEAD, text, at);
|
|
11143
|
-
}
|
|
11144
|
-
if (!secretValue(quote !== "", value))
|
|
11145
|
-
continue;
|
|
11146
|
-
if (!quote && value.length === 256)
|
|
11147
|
-
value += bareAt(BARE_REST, text, at + 256);
|
|
11148
|
-
out += `${text.slice(last, at)}${quote}[redacted]`;
|
|
11149
|
-
last = at + quote.length + value.length;
|
|
11150
|
-
FIELD_NAME.lastIndex = last;
|
|
11151
|
-
}
|
|
11152
|
-
return out + text.slice(last);
|
|
11153
|
-
}
|
|
11154
|
-
var MYSQL_COMMAND = /\bmysql(?:dump|admin)?\b(?:'[^'\n]*'|"[^"\n]*"|[^\n;&|\\'"]|\\\r?\n|\\(?!\r?\n))*/g;
|
|
11155
|
-
var MYSQL_PASSWORD = /(\s-p)(?:'[^'\n]*'|"[^"\n]*"|(?=[^\s-])\S+)/;
|
|
11156
|
-
var URL_CREDENTIALS = /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|https?):\/\/[^:\s/@]*:)[^\s/]*@([^@\s/?#]+)/g;
|
|
11157
|
-
var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
|
|
11158
|
-
var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
11159
|
-
function maskPrivateKeys(text) {
|
|
11160
|
-
const ends = [...text.matchAll(KEY_END)].map((m) => [m.index, m.index + m[0].length]);
|
|
11161
|
-
if (ends.length === 0)
|
|
11162
|
-
return text;
|
|
11163
|
-
let out = "";
|
|
11164
|
-
let last = 0;
|
|
11165
|
-
let e = 0;
|
|
11166
|
-
for (const m of text.matchAll(KEY_BEGIN)) {
|
|
11167
|
-
const after = m.index + m[0].length;
|
|
11168
|
-
if (m.index < last)
|
|
11169
|
-
continue;
|
|
11170
|
-
while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
|
|
11171
|
-
e++;
|
|
11172
|
-
const end = ends[e];
|
|
11173
|
-
if (!end)
|
|
11174
|
-
break;
|
|
11175
|
-
out += `${text.slice(last, m.index)}[redacted: private key]`;
|
|
11176
|
-
last = end[1];
|
|
11177
|
-
}
|
|
11178
|
-
return out + text.slice(last);
|
|
11179
|
-
}
|
|
11180
|
-
function mask(text) {
|
|
11181
|
-
let out = maskFields(maskPrivateKeys(text).replace(URL_CREDENTIALS, "$1[redacted]@$2").replace(AUTH_HEADER, "$1[redacted]").replace(HEADER_BEARER, "$1[redacted]").replace(ENV_ASSIGN, "$1$2[redacted]")).replace(MYSQL_COMMAND, (command) => command.replace(MYSQL_PASSWORD, "$1[redacted]"));
|
|
11182
|
-
for (const [re, what] of SECRETS)
|
|
11183
|
-
out = out.replace(re, `[redacted: ${what}]`);
|
|
11184
|
-
return out;
|
|
11185
|
-
}
|
|
11186
|
-
var reason = (e) => explain(e, 0) || "unknown failure";
|
|
11187
|
-
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
11188
|
-
function explain(e, depth) {
|
|
11189
|
-
if (!(e instanceof Error)) {
|
|
11190
|
-
try {
|
|
11191
|
-
return String(e);
|
|
11192
|
-
} catch {
|
|
11193
|
-
return "";
|
|
11194
|
-
}
|
|
11195
|
-
}
|
|
11196
|
-
const own = e.message || (e.name === "Error" || e.name === "AggregateError" ? "" : e.name);
|
|
11197
|
-
const parts = depth >= 3 ? [] : [...e instanceof AggregateError ? e.errors : [], ...e.cause === undefined ? [] : [e.cause]];
|
|
11198
|
-
const inner = parts.map((x) => explain(x, depth + 1)).filter(Boolean).join(" / ");
|
|
11199
|
-
return own && inner ? `${own} (${inner})` : own || inner;
|
|
11200
|
-
}
|
|
11201
|
-
|
|
11202
|
-
// server/src/db-write.ts
|
|
11203
11211
|
var ddl = null;
|
|
11204
11212
|
var DDL = () => ddl ??= new Set([
|
|
11205
11213
|
"SQLITE_ALTER_TABLE",
|
package/dist/mcp-record.js
CHANGED
|
@@ -45822,27 +45822,80 @@ var MYSQL_PASSWORD = /(\s-p)(?:'[^'\n]*'|"[^"\n]*"|(?=[^\s-])\S+)/;
|
|
|
45822
45822
|
var URL_CREDENTIALS = /\b((?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|rediss?|amqps?|https?):\/\/[^:\s/@]*:)[^\s/]*@([^@\s/?#]+)/g;
|
|
45823
45823
|
var KEY_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/g;
|
|
45824
45824
|
var KEY_END = /-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
45825
|
-
function
|
|
45825
|
+
function keyBlocks(text) {
|
|
45826
45826
|
const ends = [...text.matchAll(KEY_END)].map((m) => [m.index, m.index + m[0].length]);
|
|
45827
|
-
|
|
45828
|
-
return text;
|
|
45829
|
-
let out = "";
|
|
45827
|
+
const out = [];
|
|
45830
45828
|
let last = 0;
|
|
45831
45829
|
let e = 0;
|
|
45832
|
-
|
|
45833
|
-
const
|
|
45834
|
-
|
|
45835
|
-
|
|
45836
|
-
|
|
45837
|
-
e
|
|
45838
|
-
|
|
45839
|
-
|
|
45840
|
-
|
|
45841
|
-
|
|
45842
|
-
|
|
45830
|
+
if (ends.length > 0)
|
|
45831
|
+
for (const m of text.matchAll(KEY_BEGIN)) {
|
|
45832
|
+
const after = m.index + m[0].length;
|
|
45833
|
+
if (m.index < last)
|
|
45834
|
+
continue;
|
|
45835
|
+
while (e < ends.length && (ends[e]?.[0] ?? 0) < after)
|
|
45836
|
+
e++;
|
|
45837
|
+
const end = ends[e];
|
|
45838
|
+
if (!end)
|
|
45839
|
+
break;
|
|
45840
|
+
out.push([m.index, end[1]]);
|
|
45841
|
+
last = end[1];
|
|
45842
|
+
}
|
|
45843
|
+
return out;
|
|
45844
|
+
}
|
|
45845
|
+
function maskPrivateKeys(text) {
|
|
45846
|
+
let out = "";
|
|
45847
|
+
let last = 0;
|
|
45848
|
+
for (const [a, b] of keyBlocks(text)) {
|
|
45849
|
+
out += `${text.slice(last, a)}[redacted: private key]`;
|
|
45850
|
+
last = b;
|
|
45843
45851
|
}
|
|
45844
45852
|
return out + text.slice(last);
|
|
45845
45853
|
}
|
|
45854
|
+
function privateKeyRanges(text) {
|
|
45855
|
+
return byteRanges(text, keyBlocks(text));
|
|
45856
|
+
}
|
|
45857
|
+
function byteRanges(text, ranges) {
|
|
45858
|
+
const out = [];
|
|
45859
|
+
let at = 0;
|
|
45860
|
+
let pos = 0;
|
|
45861
|
+
for (const [a, b] of ranges) {
|
|
45862
|
+
pos += bytes(text.slice(at, a));
|
|
45863
|
+
const end = pos + bytes(text.slice(a, b));
|
|
45864
|
+
out.push([pos, end]);
|
|
45865
|
+
at = b;
|
|
45866
|
+
pos = end;
|
|
45867
|
+
}
|
|
45868
|
+
return out;
|
|
45869
|
+
}
|
|
45870
|
+
var PLACEHOLDER = /\[redacted(?:: [^\]\n]{1,40})?\]/g;
|
|
45871
|
+
function placeholderRanges(masked) {
|
|
45872
|
+
return [...masked.matchAll(PLACEHOLDER)].map((x) => [x.index, x.index + x[0].length]);
|
|
45873
|
+
}
|
|
45874
|
+
function starts(hay, needle) {
|
|
45875
|
+
const out = [];
|
|
45876
|
+
for (let at = hay.indexOf(needle);at >= 0; at = hay.indexOf(needle, at + 1))
|
|
45877
|
+
out.push(at);
|
|
45878
|
+
return out;
|
|
45879
|
+
}
|
|
45880
|
+
function quoteSpan(raw, masked, quote) {
|
|
45881
|
+
const q = Buffer.from(quote, "utf8");
|
|
45882
|
+
if (q.length === 0)
|
|
45883
|
+
return null;
|
|
45884
|
+
const m = Buffer.from(masked, "utf8");
|
|
45885
|
+
if (raw === masked) {
|
|
45886
|
+
const at = m.indexOf(q);
|
|
45887
|
+
return at < 0 ? null : [at, at + q.length];
|
|
45888
|
+
}
|
|
45889
|
+
const holes = byteRanges(masked, placeholderRanges(masked));
|
|
45890
|
+
let h = 0;
|
|
45891
|
+
const kept = starts(m, q).filter((at) => {
|
|
45892
|
+
while ((holes[h]?.[1] ?? Number.POSITIVE_INFINITY) <= at)
|
|
45893
|
+
h++;
|
|
45894
|
+
return !((holes[h]?.[0] ?? Number.POSITIVE_INFINITY) < at + q.length);
|
|
45895
|
+
});
|
|
45896
|
+
const first = kept[0];
|
|
45897
|
+
return first !== undefined && kept.length === starts(Buffer.from(raw, "utf8"), q).length ? [first, first + q.length] : null;
|
|
45898
|
+
}
|
|
45846
45899
|
function mask(text) {
|
|
45847
45900
|
let out = maskFields(maskPrivateKeys(text).replace(URL_CREDENTIALS, "$1[redacted]@$2").replace(AUTH_HEADER, "$1[redacted]").replace(HEADER_BEARER, "$1[redacted]").replace(ENV_ASSIGN, "$1$2[redacted]")).replace(MYSQL_COMMAND, (command) => command.replace(MYSQL_PASSWORD, "$1[redacted]"));
|
|
45848
45901
|
for (const [re, what] of SECRETS)
|
|
@@ -46053,14 +46106,61 @@ function readText(root, rel) {
|
|
|
46053
46106
|
function findSymbol(text, symbol2) {
|
|
46054
46107
|
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`);
|
|
46055
46108
|
const lines = text.split(/\r?\n/);
|
|
46056
|
-
|
|
46057
|
-
|
|
46109
|
+
return { lines, i: lines.findIndex((l) => re.test(l)) };
|
|
46110
|
+
}
|
|
46111
|
+
function swallowed(root, rel, symbol2) {
|
|
46112
|
+
if (mask(symbol2) !== symbol2)
|
|
46113
|
+
return true;
|
|
46114
|
+
const text = root ? readText(root, rel) : null;
|
|
46115
|
+
if (text === undefined)
|
|
46116
|
+
return true;
|
|
46117
|
+
if (text === null)
|
|
46118
|
+
return false;
|
|
46119
|
+
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`, "g");
|
|
46120
|
+
let raw = 0;
|
|
46121
|
+
for (let m = re.exec(text);m; m = re.exec(text))
|
|
46122
|
+
raw++;
|
|
46123
|
+
if (raw === 0)
|
|
46124
|
+
return false;
|
|
46125
|
+
const masked = mask(text);
|
|
46126
|
+
const holes = placeholderRanges(masked);
|
|
46127
|
+
let h = 0;
|
|
46128
|
+
let kept = 0;
|
|
46129
|
+
re.lastIndex = 0;
|
|
46130
|
+
for (let m = re.exec(masked);m; m = re.exec(masked)) {
|
|
46131
|
+
while ((holes[h]?.[1] ?? Number.POSITIVE_INFINITY) < m.index)
|
|
46132
|
+
h++;
|
|
46133
|
+
if (!((holes[h]?.[0] ?? Number.POSITIVE_INFINITY) <= m.index + symbol2.length))
|
|
46134
|
+
kept++;
|
|
46135
|
+
}
|
|
46136
|
+
return kept !== raw;
|
|
46137
|
+
}
|
|
46138
|
+
function masksSymbol(root, rel, symbol2) {
|
|
46139
|
+
const trimmed = symbol2.trim();
|
|
46140
|
+
return swallowed(root, rel, symbol2) || trimmed !== symbol2 && trimmed !== "" && swallowed(root, rel, trimmed);
|
|
46058
46141
|
}
|
|
46059
46142
|
function locate(root, rel, symbol2) {
|
|
46060
46143
|
if (!root)
|
|
46061
46144
|
return null;
|
|
46062
46145
|
const text = readText(root, rel);
|
|
46063
|
-
|
|
46146
|
+
if (typeof text !== "string")
|
|
46147
|
+
return null;
|
|
46148
|
+
const { lines, i } = findSymbol(text, symbol2);
|
|
46149
|
+
if (i < 0)
|
|
46150
|
+
return null;
|
|
46151
|
+
const line = lines[i] ?? "";
|
|
46152
|
+
const from = bytes(lines.slice(0, i).join(`
|
|
46153
|
+
`)) + (i > 0 ? 1 : 0);
|
|
46154
|
+
if (privateKeyRanges(lines.join(`
|
|
46155
|
+
`)).some(([a, b]) => a < from + bytes(line) && b > from))
|
|
46156
|
+
return { line: i + 1, excerpt: "[redacted: private key]" };
|
|
46157
|
+
const before = lines.slice(0, i).map((l) => `${l}
|
|
46158
|
+
`).join("");
|
|
46159
|
+
const after = lines.slice(i + 1).map((l) => `
|
|
46160
|
+
${l}`).join("");
|
|
46161
|
+
const own2 = mask(line);
|
|
46162
|
+
const cut = mask(before + line) !== mask(before) + own2 || mask(line + after) !== own2 + mask(after);
|
|
46163
|
+
return { line: i + 1, excerpt: cut ? "[redacted]" : own2.trim().slice(0, 200) };
|
|
46064
46164
|
}
|
|
46065
46165
|
|
|
46066
46166
|
// server/src/project.ts
|
|
@@ -47212,19 +47312,36 @@ async function checkRecord(db, target, raw) {
|
|
|
47212
47312
|
adoption.push({ source: s.id, start: span[0], end: span[1], route });
|
|
47213
47313
|
}
|
|
47214
47314
|
const anchors = [];
|
|
47315
|
+
const fallbacks = new Set;
|
|
47215
47316
|
for (const a of u.anchors) {
|
|
47216
47317
|
const p = repoPath(a.path);
|
|
47217
47318
|
if (!p) {
|
|
47218
47319
|
problems.push(`${key}: anchor path ${JSON.stringify(head(a.path, 80))} is not inside the repository; left out`);
|
|
47219
47320
|
continue;
|
|
47220
47321
|
}
|
|
47322
|
+
const symbol2 = a.symbol && masksSymbol(target.root, p, a.symbol) ? undefined : a.symbol;
|
|
47323
|
+
if (a.symbol && !symbol2)
|
|
47324
|
+
problems.push(`${key}: anchor symbol in ${p} is text Sphica masks; the anchor keeps only its path`);
|
|
47221
47325
|
let commit = a.commit;
|
|
47222
47326
|
if (commit && !(target.root && commitHolds(target.root, commit, p))) {
|
|
47223
47327
|
problems.push(`${key}: commit ${commit.slice(0, 12)} does not hold ${p} in the repository${target.root ? "" : " (no working tree to check)"}; the anchor keeps no commit`);
|
|
47224
47328
|
commit = undefined;
|
|
47225
47329
|
}
|
|
47226
47330
|
const observation = a.role === "evidence" && !commit && target.sessionId ? (await db.selectFrom("edit_observation").select("id").where("session_id", "=", target.sessionId).where("path", "=", p).orderBy("id", "desc").executeTakeFirst())?.id ?? null : null;
|
|
47227
|
-
|
|
47331
|
+
const planned = { ...a, symbol: symbol2, commit, path: p, observation };
|
|
47332
|
+
if (a.symbol && !symbol2)
|
|
47333
|
+
fallbacks.add(planned);
|
|
47334
|
+
anchors.push(planned);
|
|
47335
|
+
}
|
|
47336
|
+
const place = (x) => `${x.path}\x00${x.role}\x00${x.commit}\x00${x.lines ? [x.lines[0], Math.max(...x.lines)] : ""}`;
|
|
47337
|
+
const covered = new Set(anchors.filter((x) => !x.symbol && !fallbacks.has(x)).map(place));
|
|
47338
|
+
for (const x of [...fallbacks]) {
|
|
47339
|
+
if (!covered.has(place(x))) {
|
|
47340
|
+
covered.add(place(x));
|
|
47341
|
+
continue;
|
|
47342
|
+
}
|
|
47343
|
+
problems.push(`${key}: another path-only anchor on ${x.path} already covers it; left out`);
|
|
47344
|
+
anchors.splice(anchors.indexOf(x), 1);
|
|
47228
47345
|
}
|
|
47229
47346
|
const aliases = [...new Set(u.aliases.map((a) => a.trim()))];
|
|
47230
47347
|
const bad = aliases.filter((a) => !a || a.length > 40);
|
|
@@ -47346,12 +47463,13 @@ ${checked.errors.map((e) => ` ${e}`).join(`
|
|
|
47346
47463
|
}).onConflict((oc) => oc.doNothing()).execute();
|
|
47347
47464
|
}
|
|
47348
47465
|
for (const a of p.anchors) {
|
|
47349
|
-
const
|
|
47466
|
+
const symbol2 = a.symbol && !masksSymbol(target.root, a.path, a.symbol) ? a.symbol : undefined;
|
|
47467
|
+
const at = a.lines ? null : symbol2 ? locate(target.root, a.path, symbol2) : null;
|
|
47350
47468
|
const lines = a.lines ?? (at ? [at.line, at.line] : null);
|
|
47351
47469
|
await trx.insertInto("unit_anchor").values({
|
|
47352
47470
|
unit_id: id,
|
|
47353
47471
|
path: a.path,
|
|
47354
|
-
symbol:
|
|
47472
|
+
symbol: symbol2 ?? null,
|
|
47355
47473
|
commit_sha: a.commit ?? null,
|
|
47356
47474
|
line_start: lines?.[0] ?? null,
|
|
47357
47475
|
line_end: lines ? Math.max(lines[0], lines[1]) : null,
|
|
@@ -47549,15 +47667,24 @@ function readExcerpt(root, file2) {
|
|
|
47549
47667
|
} catch {
|
|
47550
47668
|
throw new Error(`${p} is not UTF-8 text`);
|
|
47551
47669
|
}
|
|
47552
|
-
const
|
|
47670
|
+
const starts2 = [0];
|
|
47553
47671
|
for (let i = 0;i < buf.length; i++)
|
|
47554
47672
|
if (buf[i] === 10 && i + 1 < buf.length)
|
|
47555
|
-
|
|
47673
|
+
starts2.push(i + 1);
|
|
47556
47674
|
const [a, b] = file2.lines;
|
|
47557
|
-
if (a > b || b >
|
|
47558
|
-
throw new Error(`${p} has ${
|
|
47559
|
-
const
|
|
47560
|
-
|
|
47675
|
+
if (a > b || b > starts2.length)
|
|
47676
|
+
throw new Error(`${p} has ${starts2.length} lines; lines ${a}-${b} are not in it`);
|
|
47677
|
+
const from = starts2[a - 1] ?? 0;
|
|
47678
|
+
const to = b < starts2.length ? starts2[b] ?? buf.length : buf.length;
|
|
47679
|
+
if (privateKeyRanges(buf.toString("utf8")).some(([s, e]) => s < to && e > from && (s < from || e > to)))
|
|
47680
|
+
throw new Error(`${p} lines ${a}-${b} are inside a private key; cite lines outside it`);
|
|
47681
|
+
const raw = buf.subarray(from, to).toString("utf8");
|
|
47682
|
+
const text2 = mask(raw);
|
|
47683
|
+
const before = buf.subarray(0, from).toString("utf8");
|
|
47684
|
+
const after = buf.subarray(to).toString("utf8");
|
|
47685
|
+
if (mask(before + raw) !== mask(before) + text2 || mask(raw + after) !== text2 + mask(after))
|
|
47686
|
+
throw new Error(`${p} lines ${a}-${b} cut through text Sphica masks; cite more lines around it`);
|
|
47687
|
+
return { path: p, commit, blob, size, lines: [a, b], raw, text: text2 };
|
|
47561
47688
|
}
|
|
47562
47689
|
var locate3 = (body, q) => {
|
|
47563
47690
|
const at = Buffer.from(body, "utf8").indexOf(Buffer.from(q, "utf8"));
|
|
@@ -47621,8 +47748,10 @@ async function checkGlean(db, target, raw) {
|
|
|
47621
47748
|
if (!target.root)
|
|
47622
47749
|
throw new Error("the repository is not known");
|
|
47623
47750
|
excerpt = readExcerpt(target.root, op.file);
|
|
47624
|
-
if (!locate3(excerpt.
|
|
47751
|
+
if (!locate3(excerpt.raw, op.quote))
|
|
47625
47752
|
errors3.push(`${what}: quote not found in ${excerpt.path} lines ${op.file.lines.join("-")}`);
|
|
47753
|
+
else if (!quoteSpan(excerpt.raw, excerpt.text, op.quote))
|
|
47754
|
+
errors3.push(`${what}: the quote also appears in text Sphica masks; quote a longer or different part`);
|
|
47626
47755
|
} catch (e) {
|
|
47627
47756
|
errors3.push(`${what}: ${e.message}`);
|
|
47628
47757
|
}
|
|
@@ -47643,6 +47772,10 @@ async function checkGlean(db, target, raw) {
|
|
|
47643
47772
|
}
|
|
47644
47773
|
if (op.op === "anchor" && !repoPath(op.path))
|
|
47645
47774
|
errors3.push(`${what}: the path is not inside the repository`);
|
|
47775
|
+
const pinned = op.op === "anchor" ? op : op.op === "replace_anchor" ? op.to : null;
|
|
47776
|
+
const pinnedPath = pinned && repoPath(pinned.path);
|
|
47777
|
+
if (pinned?.symbol && pinnedPath && masksSymbol(target.root, pinnedPath, pinned.symbol))
|
|
47778
|
+
errors3.push(`${what}: the symbol is text Sphica masks; anchor a name, not a key`);
|
|
47646
47779
|
if (op.op === "anchor" && op.commit) {
|
|
47647
47780
|
const rel2 = repoPath(op.path);
|
|
47648
47781
|
if (rel2 && !(target.root && commitHolds(target.root, op.commit, rel2)))
|
|
@@ -47732,24 +47865,25 @@ async function checkGlean(db, target, raw) {
|
|
|
47732
47865
|
}
|
|
47733
47866
|
async function excerptSource(trx, projectId2, x) {
|
|
47734
47867
|
const external2 = `file:${x.path}@${x.blob}#L${x.lines[0]}-${x.lines[1]}`;
|
|
47735
|
-
const
|
|
47736
|
-
|
|
47868
|
+
const hash2 = sha256(x.text);
|
|
47869
|
+
const found = await trx.selectFrom("source").select(["id", "revision", "content_hash"]).where("project_id", "=", projectId2).where("kind", "=", "file_excerpt").where("external_id", "=", external2).orderBy("revision", "desc").executeTakeFirst();
|
|
47870
|
+
if (found?.content_hash.equals(hash2))
|
|
47737
47871
|
return found.id;
|
|
47738
47872
|
const now = iso(Date.now());
|
|
47739
|
-
const partial2 = Buffer.byteLength(x.text, "utf8") !== x.size;
|
|
47740
47873
|
const row = await trx.insertInto("source").values({
|
|
47741
47874
|
project_id: projectId2,
|
|
47742
47875
|
kind: "file_excerpt",
|
|
47743
47876
|
artifact: `file:${x.path}`,
|
|
47744
47877
|
external_id: external2,
|
|
47745
|
-
revision: 1,
|
|
47878
|
+
revision: (found?.revision ?? 0) + 1,
|
|
47746
47879
|
author_kind: "person",
|
|
47747
47880
|
created_at: now,
|
|
47748
47881
|
captured_at: now,
|
|
47749
47882
|
text: x.text,
|
|
47750
|
-
truncated:
|
|
47883
|
+
truncated: bytes(x.raw) !== x.size ? 1 : 0,
|
|
47884
|
+
redacted: x.text !== x.raw ? 1 : 0,
|
|
47751
47885
|
original_bytes: x.size,
|
|
47752
|
-
content_hash:
|
|
47886
|
+
content_hash: hash2,
|
|
47753
47887
|
path: x.path,
|
|
47754
47888
|
line_start: x.lines[0],
|
|
47755
47889
|
line_end: x.lines[1],
|
|
@@ -47799,7 +47933,10 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
47799
47933
|
touched.set(p.unitId, op.unit);
|
|
47800
47934
|
if (op.op === "add_evidence") {
|
|
47801
47935
|
const s = p.excerpt ? { id: await excerptSource(trx, target.projectId, p.excerpt), text: p.excerpt.text } : await trx.selectFrom("source").select(["id", "text"]).where("id", "=", Number(op.source?.slice(1))).executeTakeFirstOrThrow();
|
|
47802
|
-
const at = locate3(s.text, op.quote) ?? [
|
|
47936
|
+
const at = (p.excerpt ? quoteSpan(p.excerpt.raw, s.text, op.quote) : locate3(s.text, op.quote)) ?? [
|
|
47937
|
+
0,
|
|
47938
|
+
0
|
|
47939
|
+
];
|
|
47803
47940
|
await trx.insertInto("unit_evidence").values({
|
|
47804
47941
|
unit_id: p.unitId,
|
|
47805
47942
|
source_id: s.id,
|
|
@@ -47826,11 +47963,13 @@ ${c.errors.map((e) => ` ${e}`).join(`
|
|
|
47826
47963
|
changed2.push(`${op.unit}: adopted`);
|
|
47827
47964
|
} else if (op.op === "anchor" || op.op === "replace_anchor") {
|
|
47828
47965
|
const to = op.op === "anchor" ? op : op.to;
|
|
47829
|
-
const
|
|
47966
|
+
const rel = repoPath(to.path) ?? to.path;
|
|
47967
|
+
const symbol2 = to.symbol && !masksSymbol(target.root, rel, to.symbol) ? to.symbol : null;
|
|
47968
|
+
const at = symbol2 ? locate(target.root, rel, symbol2) : null;
|
|
47830
47969
|
const added = await trx.insertInto("unit_anchor").values({
|
|
47831
47970
|
unit_id: p.unitId,
|
|
47832
|
-
path:
|
|
47833
|
-
symbol:
|
|
47971
|
+
path: rel,
|
|
47972
|
+
symbol: symbol2,
|
|
47834
47973
|
commit_sha: op.op === "anchor" ? op.commit ?? null : null,
|
|
47835
47974
|
line_start: at?.line ?? null,
|
|
47836
47975
|
line_end: at?.line ?? null,
|
package/dist/mcp.js
CHANGED
|
@@ -45821,8 +45821,7 @@ function readText(root, rel) {
|
|
|
45821
45821
|
function findSymbol(text, symbol2) {
|
|
45822
45822
|
const re = new RegExp(`(?<![\\w$])${literal3(symbol2)}(?![\\w$])`);
|
|
45823
45823
|
const lines = text.split(/\r?\n/);
|
|
45824
|
-
|
|
45825
|
-
return i < 0 ? null : { line: i + 1, excerpt: (lines[i] ?? "").trim().slice(0, 200) };
|
|
45824
|
+
return { lines, i: lines.findIndex((l) => re.test(l)) };
|
|
45826
45825
|
}
|
|
45827
45826
|
function checkAnchor(root, a) {
|
|
45828
45827
|
if (!root)
|
|
@@ -45834,12 +45833,12 @@ function checkAnchor(root, a) {
|
|
|
45834
45833
|
return { state: "unknown", line: null };
|
|
45835
45834
|
if (!a.symbol)
|
|
45836
45835
|
return { state: "located", line: a.line_start };
|
|
45837
|
-
const
|
|
45838
|
-
if (
|
|
45836
|
+
const { i } = findSymbol(text, a.symbol);
|
|
45837
|
+
if (i < 0)
|
|
45839
45838
|
return { state: "missing", line: null };
|
|
45840
45839
|
return {
|
|
45841
|
-
state: a.line_start === null || a.line_start ===
|
|
45842
|
-
line:
|
|
45840
|
+
state: a.line_start === null || a.line_start === i + 1 ? "located" : "moved",
|
|
45841
|
+
line: i + 1
|
|
45843
45842
|
};
|
|
45844
45843
|
}
|
|
45845
45844
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphica",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/skills/glean/SKILL.md
CHANGED
|
@@ -67,9 +67,9 @@ read tools `search` and `read`. Pass the repository root as `cwd` to every tool.
|
|
|
67
67
|
|
|
68
68
|
| Op | What it does |
|
|
69
69
|
|---|---|
|
|
70
|
-
| `add_evidence` | Cites a `source` ref or a committed `file` (path, commit, lines). `role` as in trace. When the owner reports what someone else said, add `reported_speaker`: it stays the owner's report, never that person's statement or an adoption |
|
|
70
|
+
| `add_evidence` | Cites a `source` ref or a committed `file` (path, commit, lines). `role` as in trace. When the owner reports what someone else said, add `reported_speaker`: it stays the owner's report, never that person's statement or an adoption. A file excerpt is stored with keys masked, so quote words around a key, never the key, and cite whole lines around it: a range that cuts through a private key, or leaves a key's name outside, is refused |
|
|
71
71
|
| `adopt` | The owner's (or a maintainer's) words that settle a decision or constraint. "Kimura said it was agreed" is not adoption; the owner saying "let's make it final" is |
|
|
72
|
-
| `anchor` / `replace_anchor` | Adds a code location, or replaces one whose code moved (`from` and `to`, citing the owner's words); the old one is kept as history. A replacement carries no commit, so an implementation whose proof was the replaced anchor goes back to candidate: add an `anchor` op with `commit` in the same batch to keep it active |
|
|
72
|
+
| `anchor` / `replace_anchor` | Adds a code location, or replaces one whose code moved (`from` and `to`, citing the owner's words); the old one is kept as history. A replacement carries no commit, so an implementation whose proof was the replaced anchor goes back to candidate: add an `anchor` op with `commit` in the same batch to keep it active. A `symbol` that is a key or a value Sphica masks is refused: anchor a name, or the path alone |
|
|
73
73
|
| `retract_evidence` / `retract_adoption` | Marks a link mistaken, citing the owner's words (`reason_source`, `reason_quote`). When the record cites the same source more than once, add `quote` to say which one. It is kept as history, and the record is judged again |
|
|
74
74
|
| `resolve_conflict` | Ends an unresolved conflict between `unit` and `with`, citing the owner's words (`reason_source`, `reason_quote`). Until then neither record is shown on its own |
|
|
75
75
|
| `withdraw` | Withdraws a record the owner says no longer holds, citing the owner's words |
|
package/skills/harvest/SKILL.md
CHANGED
|
@@ -53,7 +53,7 @@ and only by saying so: "we rejected yarn", "let's keep SQLite". check refuses th
|
|
|
53
53
|
## What to record
|
|
54
54
|
|
|
55
55
|
- Options someone proposed and a maintainer declined, with the reason given: a `decision` whose rejected option carries its `why` and evidence
|
|
56
|
-
- What the pull request implemented: an `implementation` citing the commit message or the body, with an `evidence` anchor when a path and symbol are named
|
|
56
|
+
- What the pull request implemented: an `implementation` citing the commit message or the body, with an `evidence` anchor when a path and symbol are named (a `symbol` that is a key or a value Sphica masks is dropped, keeping the path)
|
|
57
57
|
- The problem the closed issue describes, when it states a rule ("exports must never include private notes"): a `constraint` citing the issue body
|
|
58
58
|
- Review findings that led to a change (`finding`), paths tried and abandoned (`dead_end`), questions left open (`question`)
|
|
59
59
|
|
package/skills/trace/SKILL.md
CHANGED
|
@@ -80,7 +80,7 @@ The `"..."` stands for the other language's words: in this example, `"データ
|
|
|
80
80
|
| `evidence` | Required. `source` is a ref from context, `quote` is copied **exactly** from that message (a phrase is enough). `role`: `states`, `proposes`, `rejects`, `explains`, `implements`. When the owner reports what someone else said, add `reported_speaker` |
|
|
81
81
|
| `options` | Options compared, with `outcome` `chosen` / `rejected` / `deferred` / `proposed` and the `why` given. Evidence is optional per option |
|
|
82
82
|
| `adoption` | Decisions and constraints only: the owner's words that settle it. **Only owner messages adopt.** The AI proposing something and the owner not objecting is not adoption; leave it out and the record stays a candidate |
|
|
83
|
-
| `anchors` | Only where the record has a code location: `path` relative to the repository root, `symbol` when there is one, `role` `applies_to` (where it applies) or `evidence` (code that shows it was done; add `commit` when known). When an adopted decision or constraint governs how one existing code location behaves (keeping it as it is included), give it `applies_to` there, even if this work did not change it: delivery shows it when that file is read or edited. Confirm the path in the repository; do not infer one from a broad topic, and leave it unanchored when several places are plausible. `no_code_surface` may say why there is none |
|
|
83
|
+
| `anchors` | Only where the record has a code location: `path` relative to the repository root, `symbol` when there is one, `role` `applies_to` (where it applies) or `evidence` (code that shows it was done; add `commit` when known). When an adopted decision or constraint governs how one existing code location behaves (keeping it as it is included), give it `applies_to` there, even if this work did not change it: delivery shows it when that file is read or edited. Confirm the path in the repository; do not infer one from a broad topic, and leave it unanchored when several places are plausible. `no_code_surface` may say why there is none. A `symbol` must be a name in the code, never a key or a value Sphica masks: such a symbol is dropped and the anchor keeps only its path (save reports it) |
|
|
84
84
|
| `aliases` | 8 to 12 short search words in **both Japanese and English** a later reader might type: synonyms, the other language's words, abbreviations. Search only; never evidence. Not broad words that match everything (`code`, `fix`, `update`) |
|
|
85
85
|
| `supersedes` | The key of a live record this one replaces (context lists them). The old one is marked superseded, never deleted |
|
|
86
86
|
| `conflicts` | Keys of live records this one contradicts without replacing them. Both are held back from automatic injection until resolved |
|