sphica 0.5.6 → 0.6.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +4 -3
- package/db/migrations/0002.sql +145 -0
- package/db/schema.sql +54 -9
- package/dist/capture.js +61 -19
- package/dist/cli.js +95 -22
- package/dist/deliver.js +300 -258
- package/dist/mcp-record.js +450 -53
- package/dist/mcp.js +7 -8
- package/package.json +1 -1
- package/skills/forget/SKILL.md +53 -0
- package/skills/forget/agents/openai.yaml +2 -0
- package/skills/glean/SKILL.md +2 -2
- package/skills/harvest/SKILL.md +1 -1
- package/skills/trace/SKILL.md +1 -1
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
|
}
|
|
@@ -14179,7 +14187,7 @@ import os from "node:os";
|
|
|
14179
14187
|
import path2 from "node:path";
|
|
14180
14188
|
import { constants as C, DatabaseSync } from "node:sqlite";
|
|
14181
14189
|
var SCHEMA_GENERATION = 2;
|
|
14182
|
-
var SCHEMA_REVISION =
|
|
14190
|
+
var SCHEMA_REVISION = 2;
|
|
14183
14191
|
var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
|
|
14184
14192
|
var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
|
|
14185
14193
|
function requireRuntime() {
|
|
@@ -14203,7 +14211,7 @@ function prepare(raw, check) {
|
|
|
14203
14211
|
const got = raw.prepare("pragma user_version").get()?.user_version;
|
|
14204
14212
|
if (got === SCHEMA_REVISION)
|
|
14205
14213
|
return;
|
|
14206
|
-
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "
|
|
14214
|
+
throw new Error(`The database schema is revision ${got}, but this Sphica expects revision ${SCHEMA_REVISION}. ` + ((got ?? 0) < SCHEMA_REVISION ? "Update the sphica CLI (`npm i -g sphica`), then run `sphica init` to migrate it (records are kept)." : "Update sphica."));
|
|
14207
14215
|
}
|
|
14208
14216
|
function generationOf(raw) {
|
|
14209
14217
|
const has = raw.prepare("select 1 from sqlite_schema where type = 'table' and name = 'sphica_generation'").get();
|
|
@@ -14375,22 +14383,54 @@ function captureAuthorizer(own, action, p1, p2, triggerOrView) {
|
|
|
14375
14383
|
return C2.SQLITE_DENY;
|
|
14376
14384
|
}
|
|
14377
14385
|
var writes = (action) => action === C2.SQLITE_INSERT || action === C2.SQLITE_UPDATE || action === C2.SQLITE_DELETE;
|
|
14386
|
+
var FORGET_ONLY = new Set(["forget_batch", "source_forgotten"]);
|
|
14387
|
+
var KEPT_BY_INGEST = new Set(["source", "unit_evidence", "unit_adoption"]);
|
|
14378
14388
|
function ingestAuthorizer(action, p1, p2) {
|
|
14379
14389
|
if (DDL().has(action))
|
|
14380
14390
|
return C2.SQLITE_DENY;
|
|
14381
|
-
if (writes(action) && p1 === "owner_identity")
|
|
14391
|
+
if (writes(action) && (p1 === "owner_identity" || FORGET_ONLY.has(p1 ?? "")))
|
|
14392
|
+
return C2.SQLITE_DENY;
|
|
14393
|
+
if (action === C2.SQLITE_DELETE && KEPT_BY_INGEST.has(p1 ?? ""))
|
|
14382
14394
|
return C2.SQLITE_DENY;
|
|
14383
14395
|
if (action === C2.SQLITE_PRAGMA)
|
|
14384
14396
|
return readsDataVersion(p1, p2) ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14385
14397
|
return C2.SQLITE_OK;
|
|
14386
14398
|
}
|
|
14399
|
+
var FORGET_WRITES = {
|
|
14400
|
+
[C2.SQLITE_INSERT]: new Set(["forget_batch", "source_forgotten", "unit_state", "source_fts"]),
|
|
14401
|
+
[C2.SQLITE_DELETE]: new Set([
|
|
14402
|
+
"source",
|
|
14403
|
+
"external_reference",
|
|
14404
|
+
"unit_evidence",
|
|
14405
|
+
"unit_adoption",
|
|
14406
|
+
"source_processing",
|
|
14407
|
+
"source_fts"
|
|
14408
|
+
])
|
|
14409
|
+
};
|
|
14410
|
+
var FORGET_UPDATES = {
|
|
14411
|
+
unit: new Set(["lifecycle", "revision"]),
|
|
14412
|
+
unit_state: new Set(["source_id"])
|
|
14413
|
+
};
|
|
14414
|
+
function forgetAuthorizer(action, p1, p2) {
|
|
14415
|
+
if (SHADOW.test(p1 ?? "") && writes(action))
|
|
14416
|
+
return C2.SQLITE_OK;
|
|
14417
|
+
if (action === C2.SQLITE_UPDATE)
|
|
14418
|
+
return FORGET_UPDATES[p1 ?? ""]?.has(p2 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14419
|
+
if (action === C2.SQLITE_INSERT || action === C2.SQLITE_DELETE)
|
|
14420
|
+
return FORGET_WRITES[action]?.has(p1 ?? "") ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14421
|
+
if (action === C2.SQLITE_PRAGMA)
|
|
14422
|
+
return readsDataVersion(p1, p2) || p1 === "secure_delete" && ["on", "1", "true"].includes((p2 ?? "").toLowerCase()) || p1 === "wal_checkpoint" ? C2.SQLITE_OK : C2.SQLITE_DENY;
|
|
14423
|
+
if (action === C2.SQLITE_READ || action === C2.SQLITE_SELECT || action === C2.SQLITE_FUNCTION || action === C2.SQLITE_TRANSACTION || action === C2.SQLITE_SAVEPOINT)
|
|
14424
|
+
return C2.SQLITE_OK;
|
|
14425
|
+
return C2.SQLITE_DENY;
|
|
14426
|
+
}
|
|
14387
14427
|
function connectWriter(role, file = dbFile(), create = false) {
|
|
14388
14428
|
requireRuntime();
|
|
14389
14429
|
if (!create)
|
|
14390
14430
|
requireFile(file);
|
|
14391
14431
|
const raw = new DatabaseSync2(file);
|
|
14392
14432
|
try {
|
|
14393
|
-
prepare(raw, create ? "none" : role === "ingest" ? "revision" : "generation");
|
|
14433
|
+
prepare(raw, create ? "none" : role === "ingest" || role === "forget" ? "revision" : "generation");
|
|
14394
14434
|
raw.function("sphica_terms", { deterministic: true }, (text) => terms(String(text ?? "")).join(" "));
|
|
14395
14435
|
} catch (e) {
|
|
14396
14436
|
raw.close();
|
|
@@ -14398,6 +14438,8 @@ function connectWriter(role, file = dbFile(), create = false) {
|
|
|
14398
14438
|
}
|
|
14399
14439
|
if (role === "ingest")
|
|
14400
14440
|
raw.setAuthorizer(ingestAuthorizer);
|
|
14441
|
+
else if (role === "forget")
|
|
14442
|
+
raw.setAuthorizer(forgetAuthorizer);
|
|
14401
14443
|
else if (role === "capture") {
|
|
14402
14444
|
let own = false;
|
|
14403
14445
|
raw.setAuthorizer((action, p1, p2, _db, triggerOrView) => captureAuthorizer(own, action, p1, p2, triggerOrView));
|
|
@@ -14423,6 +14465,7 @@ function openWriter(role, file = dbFile()) {
|
|
|
14423
14465
|
// server/src/admin.ts
|
|
14424
14466
|
var say = (text) => console.log(indent(text));
|
|
14425
14467
|
var SCHEMA = () => path3.join(dbDir(), "schema.sql");
|
|
14468
|
+
var MIGRATION = (revision) => path3.join(dbDir(), "migrations", `${String(revision).padStart(4, "0")}.sql`);
|
|
14426
14469
|
var versionOf = (raw) => raw.prepare("pragma user_version").get().user_version;
|
|
14427
14470
|
function immediate(raw, fn) {
|
|
14428
14471
|
raw.exec("begin immediate");
|
|
@@ -14443,6 +14486,32 @@ function withOwner(file, fn, create = false) {
|
|
|
14443
14486
|
raw.close();
|
|
14444
14487
|
}
|
|
14445
14488
|
}
|
|
14489
|
+
function migrate(file = dbFile()) {
|
|
14490
|
+
return withOwner(file, (raw) => {
|
|
14491
|
+
const from = versionOf(raw);
|
|
14492
|
+
for (let r2 = from + 1;r2 <= SCHEMA_REVISION; r2++) {
|
|
14493
|
+
const script = MIGRATION(r2);
|
|
14494
|
+
if (!fs3.existsSync(script))
|
|
14495
|
+
throw new Error(`No migration from revision ${r2 - 1}. Move the database aside, then run \`sphica init\`.`);
|
|
14496
|
+
raw.exec("pragma foreign_keys = off");
|
|
14497
|
+
try {
|
|
14498
|
+
immediate(raw, () => {
|
|
14499
|
+
if (versionOf(raw) !== r2 - 1)
|
|
14500
|
+
return;
|
|
14501
|
+
raw.exec(fs3.readFileSync(script, "utf8"));
|
|
14502
|
+
const broken = raw.prepare("pragma foreign_key_check").all().length;
|
|
14503
|
+
if (broken)
|
|
14504
|
+
throw new Error(`Migration to revision ${r2} left ${plural(broken, "broken reference")}; nothing was changed`);
|
|
14505
|
+
if (versionOf(raw) !== r2)
|
|
14506
|
+
throw new Error(`${path3.basename(script)} did not set revision ${r2}`);
|
|
14507
|
+
});
|
|
14508
|
+
} finally {
|
|
14509
|
+
raw.exec("pragma foreign_keys = on");
|
|
14510
|
+
}
|
|
14511
|
+
}
|
|
14512
|
+
return from;
|
|
14513
|
+
});
|
|
14514
|
+
}
|
|
14446
14515
|
function dbInit(file = dbFile()) {
|
|
14447
14516
|
if (fs3.existsSync(file)) {
|
|
14448
14517
|
const look = new DatabaseSync3(file, { readOnly: true });
|
|
@@ -14457,8 +14526,12 @@ function dbInit(file = dbFile()) {
|
|
|
14457
14526
|
const got = withOwner(file, versionOf);
|
|
14458
14527
|
if (got === SCHEMA_REVISION)
|
|
14459
14528
|
say(`Already exists: ${file} (revision ${got})`);
|
|
14460
|
-
else
|
|
14461
|
-
say(`Already exists: ${file} (revision ${got}; this
|
|
14529
|
+
else if (got > SCHEMA_REVISION)
|
|
14530
|
+
say(`Already exists: ${file} (revision ${got}, made by a newer Sphica; this one expects ${SCHEMA_REVISION}. Update sphica.)`);
|
|
14531
|
+
else {
|
|
14532
|
+
migrate(file);
|
|
14533
|
+
say(`Migrated: ${file} (revision ${got} → ${SCHEMA_REVISION})`);
|
|
14534
|
+
}
|
|
14462
14535
|
return;
|
|
14463
14536
|
}
|
|
14464
14537
|
fs3.mkdirSync(path3.dirname(file), { recursive: true, mode: 448 });
|
|
@@ -15728,7 +15801,7 @@ async function doctor(cwd) {
|
|
|
15728
15801
|
const x = inspect(file);
|
|
15729
15802
|
usable2 = x.revision === SCHEMA_REVISION;
|
|
15730
15803
|
say2("ok", "DB", `${file} (${(x.bytes / 1024 / 1024).toFixed(1)} MB)`);
|
|
15731
|
-
say2(usable2 ? "ok" : "fail", "Schema version", usable2 ? `revision ${x.revision}` : `revision ${x.revision}, this Sphica expects ${SCHEMA_REVISION} (${x.revision < SCHEMA_REVISION ? "made by an older Sphica:
|
|
15804
|
+
say2(usable2 ? "ok" : "fail", "Schema version", usable2 ? `revision ${x.revision}` : `revision ${x.revision}, this Sphica expects ${SCHEMA_REVISION} (${x.revision < SCHEMA_REVISION ? "made by an older Sphica: run sphica init to migrate it" : "update sphica"})`);
|
|
15732
15805
|
const broken = Object.entries(x.fts).filter(([, v]) => v !== null);
|
|
15733
15806
|
say2(broken.length ? "fail" : "ok", "Full-text index", broken.length ? `broken: ${broken.map(([k, v]) => `${k} (${plain(v ?? "")})`).join(" / ")}. Rebuild it with sphica doctor --reindex` : "healthy");
|
|
15734
15807
|
} catch (e) {
|