sphica 0.6.20 → 0.6.22

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "sphica",
4
- "version": "0.6.20",
4
+ "version": "0.6.22",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sphica",
3
- "version": "0.6.20",
3
+ "version": "0.6.22",
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
  "author": {
6
6
  "name": "iroha924",
@@ -45,7 +45,7 @@ This file is generated by `node scripts/third-party-notices.mjs`. Do not edit it
45
45
  | fast-deep-equal | 3.1.3 | MIT |
46
46
  | fast-string-truncated-width | 3.0.3 | MIT |
47
47
  | fast-string-width | 3.0.2 | MIT |
48
- | fast-uri | 3.1.7 | BSD-3-Clause |
48
+ | fast-uri | 3.1.8 | BSD-3-Clause |
49
49
  | fast-wrap-ansi | 0.2.2 | MIT |
50
50
  | finalhandler | 2.1.1 | MIT |
51
51
  | forwarded | 0.2.0 | MIT |
@@ -60,7 +60,7 @@ This file is generated by `node scripts/third-party-notices.mjs`. Do not edit it
60
60
  | http-errors | 2.0.1 | MIT |
61
61
  | iconv-lite | 0.7.3 | MIT |
62
62
  | inherits | 2.0.4 | ISC |
63
- | ip-address | 10.7.0 | MIT |
63
+ | ip-address | 10.7.1 | MIT |
64
64
  | ipaddr.js | 1.9.1 | MIT |
65
65
  | is-promise | 4.0.0 | MIT |
66
66
  | isexe | 2.0.0 | ISC |
@@ -1393,7 +1393,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1393
1393
  DEALINGS IN THE SOFTWARE.
1394
1394
  ```
1395
1395
 
1396
- ## fast-uri 3.1.7
1396
+ ## fast-uri 3.1.8
1397
1397
 
1398
1398
  SPDX: BSD-3-Clause
1399
1399
  Source: https://github.com/fastify/fast-uri
@@ -1835,7 +1835,7 @@ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1835
1835
  PERFORMANCE OF THIS SOFTWARE.
1836
1836
  ```
1837
1837
 
1838
- ## ip-address 10.7.0
1838
+ ## ip-address 10.7.1
1839
1839
 
1840
1840
  SPDX: MIT
1841
1841
  Source: https://github.com/beaugunderson/ip-address
@@ -0,0 +1,33 @@
1
+ -- Rows revision 8 cannot take and the migration must not change on its own. `sphica init` runs this first, inside the same transaction,
2
+ -- and stops with every row listed when there is any; nothing of revision 8 is applied.
3
+
4
+ create temp table sphica_migration_stop (rule text, item text);
5
+
6
+ -- Projects whose keys become one once normalized. Merging two projects that both hold rows would move sessions, whose ids derive from the
7
+ -- project id, so this revision merges only when at most one of them holds any row.
8
+ create temp table sphica_project_use as
9
+ select id, key,
10
+ case when lower(host) = 'github.com' then lower(key) else 'git:' || lower(host) || substr(key, length(host) + 5) end as normal,
11
+ (select count(*) from session where project_id = p.id) as sessions,
12
+ (select count(*) from source where project_id = p.id) as sources,
13
+ (select count(*) from artifact_link where project_id = p.id) as links,
14
+ (select count(*) from forget_batch where project_id = p.id) as forgets,
15
+ (select count(*) from source_forgotten where project_id = p.id) as forgotten,
16
+ (select count(*) from extraction_run where project_id = p.id) as runs,
17
+ (select count(*) from unit where project_id = p.id) as units,
18
+ (select count(*) from field_def where project_id = p.id) as fields,
19
+ (select count(*) from work where project_id = p.id) as works
20
+ from (select id, key, case when instr(substr(key, 5), '/') = 0 then substr(key, 5)
21
+ else substr(key, 5, instr(substr(key, 5), '/') - 1) end as host from project where key glob 'git:*') p;
22
+
23
+ insert into sphica_migration_stop
24
+ select 'projects with records whose keys become one once normalized', 'project ' || id || ' ' || key || ' (becomes ' || normal || '): '
25
+ || sessions || ' sessions, ' || sources || ' sources, ' || links || ' artifact links, ' || forgets || ' forget batches, '
26
+ || forgotten || ' forgotten sources, ' || runs || ' runs, ' || units || ' records, ' || fields || ' fields, ' || works || ' work items'
27
+ from sphica_project_use
28
+ where sessions + sources + links + forgets + forgotten + runs + units + fields + works > 0
29
+ and normal in (select normal from sphica_project_use
30
+ where sessions + sources + links + forgets + forgotten + runs + units + fields + works > 0 group by normal having count(*) > 1)
31
+ order by normal, id;
32
+
33
+ drop table temp.sphica_project_use;
@@ -0,0 +1,53 @@
1
+ -- Revision 7 → 8 of generation 2: project keys are normalized, and triggers refuse a key that is not. Projects whose keys become one
2
+ -- merge only when at most one holds rows: the one with rows, or the oldest, keeps its id and takes the key, and the empty others go.
3
+ -- Every statement that stays is the same as in the schema at revision 8.
4
+
5
+ create temp table sphica_migration_note (rule text, item text, action text);
6
+
7
+ create temp table sphica_project_normal as
8
+ select id, key, normal,
9
+ coalesce(nullif(substr(normal, 5 + instr(substr(normal, 5), '/')), ''), substr(normal, 5)) as name,
10
+ exists (select 1 from session where project_id = p.id) or exists (select 1 from source where project_id = p.id)
11
+ or exists (select 1 from artifact_link where project_id = p.id) or exists (select 1 from forget_batch where project_id = p.id)
12
+ or exists (select 1 from source_forgotten where project_id = p.id) or exists (select 1 from extraction_run where project_id = p.id)
13
+ or exists (select 1 from unit where project_id = p.id) or exists (select 1 from field_def where project_id = p.id)
14
+ or exists (select 1 from work where project_id = p.id) as used
15
+ from (select id, key,
16
+ case when lower(host) = 'github.com' then lower(key) else 'git:' || lower(host) || substr(key, length(host) + 5) end as normal
17
+ from (select id, key, case when instr(substr(key, 5), '/') = 0 then substr(key, 5)
18
+ else substr(key, 5, instr(substr(key, 5), '/') - 1) end as host from project where key glob 'git:*')) p;
19
+
20
+ create temp table sphica_project_keep as
21
+ select normal, coalesce(max(case when used then id end), min(id)) as id from sphica_project_normal group by normal;
22
+
23
+ -- Delete the empty others first: one of them may hold the normalized key already
24
+ insert into sphica_migration_note
25
+ select 'an empty project whose key another project takes once normalized', 'project ' || n.id || ' ' || n.key,
26
+ 'removed; project ' || k.id || ' takes ' || n.normal
27
+ from sphica_project_normal n join sphica_project_keep k on k.normal = n.normal where n.id <> k.id order by n.id;
28
+ delete from project where id in (select n.id from sphica_project_normal n join sphica_project_keep k on k.normal = n.normal where n.id <> k.id);
29
+
30
+ insert into sphica_migration_note
31
+ select 'a project key that was not normalized', 'project ' || n.id || ' ' || n.key, 'now ' || n.normal || ' (' || n.name || ')'
32
+ from sphica_project_normal n join sphica_project_keep k on k.id = n.id where n.key <> n.normal order by n.id;
33
+ update project set key = (select normal from sphica_project_normal n where n.id = project.id),
34
+ name = (select name from sphica_project_normal n where n.id = project.id)
35
+ where id in (select n.id from sphica_project_normal n join sphica_project_keep k on k.id = n.id where n.key <> n.normal);
36
+
37
+ drop table temp.sphica_project_keep;
38
+ drop table temp.sphica_project_normal;
39
+
40
+ create trigger project_key_normal_insert before insert on project when new.key glob 'git:*' begin
41
+ select raise(abort, 'the project key is not normalized')
42
+ from (select rest, case when instr(rest, '/') = 0 then rest else substr(rest, 1, instr(rest, '/') - 1) end as host
43
+ from (select substr(new.key, 5) as rest))
44
+ where new.key is not case when lower(host) = 'github.com' then lower(new.key) else 'git:' || lower(host) || substr(rest, length(host) + 1) end;
45
+ end;
46
+ create trigger project_key_normal_update before update of key on project when new.key glob 'git:*' begin
47
+ select raise(abort, 'the project key is not normalized')
48
+ from (select rest, case when instr(rest, '/') = 0 then rest else substr(rest, 1, instr(rest, '/') - 1) end as host
49
+ from (select substr(new.key, 5) as rest))
50
+ where new.key is not case when lower(host) = 'github.com' then lower(new.key) else 'git:' || lower(host) || substr(rest, length(host) + 1) end;
51
+ end;
52
+
53
+ pragma user_version = 8;
package/db/schema.sql CHANGED
@@ -32,6 +32,21 @@ create table project (
32
32
  check (strftime('%Y-%m-%dT%H:%M:%fZ', created_at) is created_at)
33
33
  ) strict;
34
34
 
35
+ -- A git key's host is lowercase, and on github.com the whole key is (owner and repository names are case-insensitive there), so one
36
+ -- repository has one key however its remote is written. lower() folds ASCII only, as normalizeKey does.
37
+ create trigger project_key_normal_insert before insert on project when new.key glob 'git:*' begin
38
+ select raise(abort, 'the project key is not normalized')
39
+ from (select rest, case when instr(rest, '/') = 0 then rest else substr(rest, 1, instr(rest, '/') - 1) end as host
40
+ from (select substr(new.key, 5) as rest))
41
+ where new.key is not case when lower(host) = 'github.com' then lower(new.key) else 'git:' || lower(host) || substr(rest, length(host) + 1) end;
42
+ end;
43
+ create trigger project_key_normal_update before update of key on project when new.key glob 'git:*' begin
44
+ select raise(abort, 'the project key is not normalized')
45
+ from (select rest, case when instr(rest, '/') = 0 then rest else substr(rest, 1, instr(rest, '/') - 1) end as host
46
+ from (select substr(new.key, 5) as rest))
47
+ where new.key is not case when lower(host) = 'github.com' then lower(new.key) else 'git:' || lower(host) || substr(rest, length(host) + 1) end;
48
+ end;
49
+
35
50
  -- Identities bound to the owner of this machine (the owner's GitHub account id), set by the owner through the CLI.
36
51
  -- An external source counts as the owner's words only when its author id matches; a login name alone never does.
37
52
  create table owner_identity (
@@ -1055,4 +1070,4 @@ create trigger capture_delivery_prune_insert instead of insert on capture_delive
1055
1070
  order by d.at, d.id limit 200);
1056
1071
  end;
1057
1072
 
1058
- pragma user_version = 7;
1073
+ pragma user_version = 8;
package/dist/capture.js CHANGED
@@ -11110,7 +11110,7 @@ var HOST_MARKS = new Set([".orphaned_at", ".in_use"]);
11110
11110
 
11111
11111
  // server/src/sqlite.ts
11112
11112
  var SCHEMA_GENERATION = 2;
11113
- var SCHEMA_REVISION = 7;
11113
+ var SCHEMA_REVISION = 8;
11114
11114
  var sphicaHome = () => process.env.SPHICA_HOME || path2.join(os.homedir(), ".sphica");
11115
11115
  var dbFile = () => process.env.SPHICA_DB || path2.join(sphicaHome(), "sphica.db");
11116
11116
  function requireRuntime() {
@@ -11470,7 +11470,7 @@ var leaves = (rel) => rel === ".." || rel.startsWith(`..${path3.sep}`) || path3.
11470
11470
  // server/src/project.ts
11471
11471
  var localFile = () => path4.join(sphicaHome(), "projects.json");
11472
11472
  var LOCAL_KEY = /^[a-z0-9][a-z0-9._-]*$/;
11473
- function normalizeRemote(url) {
11473
+ function legacyRemote(url) {
11474
11474
  const raw = String(url ?? "").trim();
11475
11475
  if (!raw)
11476
11476
  return null;
@@ -11487,6 +11487,15 @@ function normalizeRemote(url) {
11487
11487
  return null;
11488
11488
  }
11489
11489
  }
11490
+ var fold = (s) => s.replace(/[A-Z]+/g, (m) => m.toLowerCase());
11491
+ function normalizeKey(key) {
11492
+ if (!key.startsWith("git:"))
11493
+ return key;
11494
+ const rest = key.slice(4);
11495
+ const cut = rest.indexOf("/");
11496
+ const host = fold(cut < 0 ? rest : rest.slice(0, cut));
11497
+ return host === "github.com" ? fold(key) : `git:${host}${cut < 0 ? "" : rest.slice(cut)}`;
11498
+ }
11490
11499
  var git = (dir, ...args) => {
11491
11500
  try {
11492
11501
  return execFileSync("git", ["-C", dir, ...args], {
@@ -11521,14 +11530,17 @@ function identify(dir) {
11521
11530
  const given = path4.resolve(dir);
11522
11531
  const top = git(given, "rev-parse", "--show-toplevel");
11523
11532
  const root = top || given;
11524
- const remote = top ? normalizeRemote(git(root, "remote", "get-url", "origin")) : null;
11525
- if (remote)
11526
- return { key: `git:${remote}`, root, name: remote.split("/").slice(1).join("/") || remote };
11533
+ const legacy = top ? legacyRemote(git(root, "remote", "get-url", "origin")) : null;
11534
+ if (legacy) {
11535
+ const key = normalizeKey(`git:${legacy}`);
11536
+ const remote = key.slice(4);
11537
+ return { key, legacyKey: `git:${legacy}`, root, name: remote.split("/").slice(1).join("/") || remote };
11538
+ }
11527
11539
  const map = localMap();
11528
11540
  for (let d = root;; d = path4.dirname(d)) {
11529
11541
  const local = map[d];
11530
11542
  if (local && LOCAL_KEY.test(local))
11531
- return { key: `local:${local}`, root: d, name: local };
11543
+ return { key: `local:${local}`, legacyKey: `local:${local}`, root: d, name: local };
11532
11544
  if (top || path4.dirname(d) === d)
11533
11545
  return null;
11534
11546
  }
@@ -11620,6 +11632,8 @@ var HOLD_MAX = 1000;
11620
11632
  var baselineDir = () => path6.join(sphicaHome(), "worktree");
11621
11633
  function current(raw) {
11622
11634
  const r = raw;
11635
+ if (typeof r.project !== "string")
11636
+ throw new Error("queue record without a project key");
11623
11637
  if (r.v === 2)
11624
11638
  return raw;
11625
11639
  if (r.v !== 1)
@@ -11800,7 +11814,7 @@ function onHook(host, input) {
11800
11814
  v: 2,
11801
11815
  host,
11802
11816
  session: String(input.session_id),
11803
- project: place.key,
11817
+ project: place.legacyKey,
11804
11818
  branch: branchOf(place.root),
11805
11819
  turn,
11806
11820
  at
@@ -11933,17 +11947,33 @@ var FLUSH_BUDGET_MS = 30000;
11933
11947
  var TOOL_FLUSH_BUDGET_MS = 2000;
11934
11948
  var ROWS = 1000;
11935
11949
  var chunks = (xs, n = ROWS) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, (i + 1) * n));
11936
- async function write(db, batch, projects) {
11950
+ async function projectsOf(trx, keys) {
11951
+ if (!keys.length)
11952
+ return new Map;
11953
+ const all = await trx.selectFrom("project").select(["id", "key"]).execute();
11954
+ const exact = new Map(all.map((p) => [p.key, p.id]));
11955
+ const normal = Map.groupBy(all, (p) => normalizeKey(p.key));
11956
+ const found = new Map;
11957
+ for (const k of keys) {
11958
+ const same = normal.get(normalizeKey(k));
11959
+ const id = exact.get(k) ?? (same?.length === 1 ? same[0]?.id : undefined);
11960
+ if (id !== undefined)
11961
+ found.set(k, id);
11962
+ }
11963
+ return found;
11964
+ }
11965
+ async function write(db, batch) {
11937
11966
  return inTransaction(db, async (trx) => {
11967
+ const projects = await projectsOf(trx, batch.map((r) => r.project));
11938
11968
  const sessions = new Map;
11939
11969
  for (const r of batch) {
11940
11970
  const p = projects.get(r.project);
11941
- if (!p)
11971
+ if (p === undefined)
11942
11972
  continue;
11943
- const id = sessionId(p.id, r.host, r.session);
11973
+ const id = sessionId(p, r.host, r.session);
11944
11974
  const prev = sessions.get(id);
11945
11975
  if (!prev || Date.parse(r.at) < Date.parse(prev.at))
11946
- sessions.set(id, { project: p.id, host: r.host, session: r.session, branch: r.branch, at: r.at });
11976
+ sessions.set(id, { project: p, host: r.host, session: r.session, branch: r.branch, at: r.at });
11947
11977
  }
11948
11978
  for (const part of chunks([...sessions]))
11949
11979
  await trx.insertInto("capture_session").values(part.map(([id, v]) => ({
@@ -11956,9 +11986,9 @@ async function write(db, batch, projects) {
11956
11986
  }))).execute();
11957
11987
  const messages = batch.flatMap((m) => {
11958
11988
  const p = m.kind === "message" ? projects.get(m.project) : undefined;
11959
- if (m.kind !== "message" || !p)
11989
+ if (m.kind !== "message" || p === undefined)
11960
11990
  return [];
11961
- return [{ m, session: sessionId(p.id, m.host, m.session) }];
11991
+ return [{ m, session: sessionId(p, m.host, m.session) }];
11962
11992
  });
11963
11993
  const present = async () => {
11964
11994
  let n = 0;
@@ -11986,11 +12016,11 @@ async function write(db, batch, projects) {
11986
12016
  const after = await present();
11987
12017
  const edits = batch.flatMap((r) => {
11988
12018
  const p = r.kind === "edit" ? projects.get(r.project) : undefined;
11989
- if (r.kind !== "edit" || !p)
12019
+ if (r.kind !== "edit" || p === undefined)
11990
12020
  return [];
11991
12021
  return [
11992
12022
  {
11993
- session_id: sessionId(p.id, r.host, r.session),
12023
+ session_id: sessionId(p, r.host, r.session),
11994
12024
  turn_id: r.turn,
11995
12025
  tool_event_id: r.event,
11996
12026
  path: r.path,
@@ -12001,7 +12031,7 @@ async function write(db, batch, projects) {
12001
12031
  });
12002
12032
  for (const part of chunks(edits))
12003
12033
  await trx.insertInto("capture_edit").values(part).execute();
12004
- return after - before;
12034
+ return { sent: after - before, strayed: new Set(batch.filter((r) => !projects.has(r.project))) };
12005
12035
  });
12006
12036
  }
12007
12037
  var REJECTED = new Set([18, 19, 20, 25]);
@@ -12030,19 +12060,22 @@ async function sendBatch(db, names) {
12030
12060
  else
12031
12061
  fs5.rmSync(path6.join(from, name), { force: true });
12032
12062
  }
12033
- const projects = new Map((await db.selectFrom("project").select(["id", "key", "name"]).where("key", "in", [...new Set(records.map((x) => x.r.project))]).execute()).map((p) => [p.key, { id: p.id, name: p.name }]));
12034
- const known = records.filter((x) => projects.has(x.r.project));
12035
- const strayed = records.filter((x) => !projects.has(x.r.project));
12036
12063
  let sent = 0;
12064
+ let strayed = [];
12037
12065
  const bad = [];
12038
12066
  try {
12039
- sent = await write(db, known.map((x) => x.r), projects);
12067
+ const out = await write(db, records.map((x) => x.r));
12068
+ sent = out.sent;
12069
+ strayed = records.filter((x) => out.strayed.has(x.r));
12040
12070
  } catch (e) {
12041
12071
  if (!rejected(e))
12042
12072
  throw e;
12043
- for (const x of known) {
12073
+ for (const x of records) {
12044
12074
  try {
12045
- sent += await write(db, [x.r], projects);
12075
+ const out = await write(db, [x.r]);
12076
+ sent += out.sent;
12077
+ if (out.strayed.size)
12078
+ strayed.push(x);
12046
12079
  } catch (e2) {
12047
12080
  if (!rejected(e2))
12048
12081
  throw e2;
@@ -12163,6 +12196,8 @@ if (process.argv[1] && /capture\.(ts|js)$/.test(process.argv[1])) {
12163
12196
  main().catch(() => {});
12164
12197
  }
12165
12198
  export {
12199
+ HOLD_DAYS,
12200
+ HOLD_MAX,
12166
12201
  MAX_MESSAGE,
12167
12202
  TOOL_FLUSH_BUDGET_MS,
12168
12203
  answersOf,