@sammorrowdrums/mcpi-session-backend-sqlite-node 0.85.0-bootstrap.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +22 -0
  4. package/dist/index.d.ts +6 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +94 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/sqlite/branch-cache.d.ts +6 -0
  9. package/dist/sqlite/branch-cache.d.ts.map +1 -0
  10. package/dist/sqlite/branch-cache.js +70 -0
  11. package/dist/sqlite/branch-cache.js.map +1 -0
  12. package/dist/sqlite/index.d.ts +6 -0
  13. package/dist/sqlite/index.d.ts.map +1 -0
  14. package/dist/sqlite/index.js +5 -0
  15. package/dist/sqlite/index.js.map +1 -0
  16. package/dist/sqlite/migrations/001_initial.sql +122 -0
  17. package/dist/sqlite/migrations.d.ts +9 -0
  18. package/dist/sqlite/migrations.d.ts.map +1 -0
  19. package/dist/sqlite/migrations.js +39 -0
  20. package/dist/sqlite/migrations.js.map +1 -0
  21. package/dist/sqlite/repo.d.ts +41 -0
  22. package/dist/sqlite/repo.d.ts.map +1 -0
  23. package/dist/sqlite/repo.js +750 -0
  24. package/dist/sqlite/repo.js.map +1 -0
  25. package/dist/sqlite/search-backend.d.ts +14 -0
  26. package/dist/sqlite/search-backend.d.ts.map +1 -0
  27. package/dist/sqlite/search-backend.js +153 -0
  28. package/dist/sqlite/search-backend.js.map +1 -0
  29. package/dist/sqlite/sql.d.ts +19 -0
  30. package/dist/sqlite/sql.d.ts.map +1 -0
  31. package/dist/sqlite/sql.js +58 -0
  32. package/dist/sqlite/sql.js.map +1 -0
  33. package/dist/sqlite/storage/branch-entries.d.ts +41 -0
  34. package/dist/sqlite/storage/branch-entries.d.ts.map +1 -0
  35. package/dist/sqlite/storage/branch-entries.js +112 -0
  36. package/dist/sqlite/storage/branch-entries.js.map +1 -0
  37. package/dist/sqlite/storage/branch-tips.d.ts +7 -0
  38. package/dist/sqlite/storage/branch-tips.d.ts.map +1 -0
  39. package/dist/sqlite/storage/branch-tips.js +22 -0
  40. package/dist/sqlite/storage/branch-tips.js.map +1 -0
  41. package/dist/sqlite/storage/entries.d.ts +34 -0
  42. package/dist/sqlite/storage/entries.d.ts.map +1 -0
  43. package/dist/sqlite/storage/entries.js +37 -0
  44. package/dist/sqlite/storage/entries.js.map +1 -0
  45. package/dist/sqlite/storage/facts.d.ts +20 -0
  46. package/dist/sqlite/storage/facts.d.ts.map +1 -0
  47. package/dist/sqlite/storage/facts.js +38 -0
  48. package/dist/sqlite/storage/facts.js.map +1 -0
  49. package/dist/sqlite/storage/lanes.d.ts +35 -0
  50. package/dist/sqlite/storage/lanes.d.ts.map +1 -0
  51. package/dist/sqlite/storage/lanes.js +95 -0
  52. package/dist/sqlite/storage/lanes.js.map +1 -0
  53. package/dist/sqlite/storage/records.d.ts +38 -0
  54. package/dist/sqlite/storage/records.d.ts.map +1 -0
  55. package/dist/sqlite/storage/records.js +49 -0
  56. package/dist/sqlite/storage/records.js.map +1 -0
  57. package/dist/sqlite/storage/session-sequences.d.ts +7 -0
  58. package/dist/sqlite/storage/session-sequences.d.ts.map +1 -0
  59. package/dist/sqlite/storage/session-sequences.js +22 -0
  60. package/dist/sqlite/storage/session-sequences.js.map +1 -0
  61. package/dist/sqlite/storage/session-stats.d.ts +17 -0
  62. package/dist/sqlite/storage/session-stats.d.ts.map +1 -0
  63. package/dist/sqlite/storage/session-stats.js +40 -0
  64. package/dist/sqlite/storage/session-stats.js.map +1 -0
  65. package/dist/sqlite/storage/sessions.d.ts +26 -0
  66. package/dist/sqlite/storage/sessions.d.ts.map +1 -0
  67. package/dist/sqlite/storage/sessions.js +99 -0
  68. package/dist/sqlite/storage/sessions.js.map +1 -0
  69. package/dist/sqlite/storage/writer-leases.d.ts +15 -0
  70. package/dist/sqlite/storage/writer-leases.d.ts.map +1 -0
  71. package/dist/sqlite/storage/writer-leases.js +31 -0
  72. package/dist/sqlite/storage/writer-leases.js.map +1 -0
  73. package/dist/sqlite/types.d.ts +45 -0
  74. package/dist/sqlite/types.d.ts.map +1 -0
  75. package/dist/sqlite/types.js +2 -0
  76. package/dist/sqlite/types.js.map +1 -0
  77. package/package.json +53 -0
@@ -0,0 +1,49 @@
1
+ import { SessionError } from "@sammorrowdrums/mcpi-agent-core";
2
+ import { joinSqlFragments, sql } from "../sql.js";
3
+ export function appendRecordRow(db, sessionId, record) {
4
+ sql `INSERT INTO records
5
+ (session_id, seq, id, lane, run_id, type, op_kind, timestamp, payload)
6
+ VALUES (${sessionId}, ${record.seq}, ${record.id}, ${record.lane}, ${record.runId ?? null}, ${record.type}, ${record.opKind ?? null}, ${record.timestamp}, ${record.payload})`.run(db);
7
+ }
8
+ export function idExistsInRecords(db, sessionId, id) {
9
+ return !!sql `SELECT 1 AS found FROM records WHERE session_id = ${sessionId} AND id = ${id} LIMIT 1`.get(db);
10
+ }
11
+ export function deleteRecordRows(db, sessionId) {
12
+ sql `DELETE FROM records WHERE session_id = ${sessionId}`.run(db);
13
+ }
14
+ export function readRecordRows(db, sessionId, query = {}) {
15
+ const predicates = [sql `session_id = ${sessionId}`];
16
+ if (query.lane !== undefined)
17
+ predicates.push(sql `lane = ${query.lane}`);
18
+ if (query.type !== undefined)
19
+ predicates.push(sql `type = ${query.type}`);
20
+ if (query.runId !== undefined)
21
+ predicates.push(sql `run_id = ${query.runId}`);
22
+ if (query.operationKind !== undefined)
23
+ predicates.push(sql `op_kind = ${query.operationKind}`);
24
+ if (query.afterSeq !== undefined)
25
+ predicates.push(sql `seq > ${query.afterSeq}`);
26
+ const direction = query.order === "oldestFirst" ? sql `ASC` : sql `DESC`;
27
+ const limit = query.limit === undefined ? sql `` : sql ` LIMIT ${query.limit}`;
28
+ return sql `SELECT session_id, seq, id, lane, run_id, type, op_kind, timestamp, payload
29
+ FROM records
30
+ WHERE ${joinSqlFragments(predicates, " AND ")}
31
+ ORDER BY seq ${direction}${limit}`.all(db);
32
+ }
33
+ export function readOpenOperationRows(db, sessionId, lane, _options = {}) {
34
+ const laneRow = sql `SELECT open_operation_id FROM lanes WHERE session_id = ${sessionId} AND lane = ${lane}`.get(db);
35
+ if (!laneRow?.open_operation_id)
36
+ return [];
37
+ const record = sql `SELECT session_id, seq, id, lane, run_id, type, op_kind, timestamp, payload
38
+ FROM records
39
+ WHERE session_id = ${sessionId}
40
+ AND id = ${laneRow.open_operation_id}`.get(db);
41
+ if (!record) {
42
+ throw new SessionError("storage", `Lane ${lane} points at missing open operation ${laneRow.open_operation_id}`);
43
+ }
44
+ if (record.lane !== lane || record.type !== "operation_started") {
45
+ throw new SessionError("storage", `Lane ${lane} points at invalid open operation ${laneRow.open_operation_id}`);
46
+ }
47
+ return [record];
48
+ }
49
+ //# sourceMappingURL=records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"records.js","sourceRoot":"","sources":["../../../src/sqlite/storage/records.ts"],"names":[],"mappings":"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","sourcesContent":["import { SessionError } from \"@sammorrowdrums/mcpi-agent-core\";\nimport { joinSqlFragments, sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport interface RecordRow {\n\tsession_id: string;\n\tseq: number;\n\tid: string;\n\tlane: string;\n\trun_id: string | null;\n\ttype: string;\n\top_kind: string | null;\n\ttimestamp: number;\n\tpayload: string;\n}\n\nexport interface NewRecordRow {\n\tseq: number;\n\tid: string;\n\tlane: string;\n\trunId?: string;\n\ttype: string;\n\topKind?: string;\n\ttimestamp: number;\n\tpayload: string;\n}\n\nexport function appendRecordRow(db: SqliteDatabase, sessionId: string, record: NewRecordRow) {\n\tsql`INSERT INTO records\n\t\t\t(session_id, seq, id, lane, run_id, type, op_kind, timestamp, payload)\n\t\t\tVALUES (${sessionId}, ${record.seq}, ${record.id}, ${record.lane}, ${record.runId ?? null}, ${record.type}, ${record.opKind ?? null}, ${record.timestamp}, ${record.payload})`.run(\n\t\tdb,\n\t);\n}\n\nexport function idExistsInRecords(db: SqliteDatabase, sessionId: string, id: string) {\n\treturn !!sql`SELECT 1 AS found FROM records WHERE session_id = ${sessionId} AND id = ${id} LIMIT 1`.get<{\n\t\tfound: number;\n\t}>(db);\n}\n\nexport function deleteRecordRows(db: SqliteDatabase, sessionId: string) {\n\tsql`DELETE FROM records WHERE session_id = ${sessionId}`.run(db);\n}\n\nexport function readRecordRows(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tquery: {\n\t\tlane?: string;\n\t\ttype?: string;\n\t\trunId?: string;\n\t\toperationKind?: string;\n\t\tafterSeq?: number;\n\t\torder?: \"newestFirst\" | \"oldestFirst\";\n\t\tlimit?: number;\n\t} = {},\n) {\n\tconst predicates = [sql`session_id = ${sessionId}`];\n\tif (query.lane !== undefined) predicates.push(sql`lane = ${query.lane}`);\n\tif (query.type !== undefined) predicates.push(sql`type = ${query.type}`);\n\tif (query.runId !== undefined) predicates.push(sql`run_id = ${query.runId}`);\n\tif (query.operationKind !== undefined) predicates.push(sql`op_kind = ${query.operationKind}`);\n\tif (query.afterSeq !== undefined) predicates.push(sql`seq > ${query.afterSeq}`);\n\tconst direction = query.order === \"oldestFirst\" ? sql`ASC` : sql`DESC`;\n\tconst limit = query.limit === undefined ? sql`` : sql` LIMIT ${query.limit}`;\n\treturn sql`SELECT session_id, seq, id, lane, run_id, type, op_kind, timestamp, payload\n\t\tFROM records\n\t\tWHERE ${joinSqlFragments(predicates, \" AND \")}\n\t\tORDER BY seq ${direction}${limit}`.all<RecordRow>(db);\n}\n\nexport function readOpenOperationRows(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tlane: string,\n\t_options: { limit?: number } = {},\n): RecordRow[] {\n\tconst laneRow = sql`SELECT open_operation_id FROM lanes WHERE session_id = ${sessionId} AND lane = ${lane}`.get<{\n\t\topen_operation_id: string | null;\n\t}>(db);\n\tif (!laneRow?.open_operation_id) return [];\n\n\tconst record = sql`SELECT session_id, seq, id, lane, run_id, type, op_kind, timestamp, payload\n\t\tFROM records\n\t\tWHERE session_id = ${sessionId}\n\t\t\tAND id = ${laneRow.open_operation_id}`.get<RecordRow>(db);\n\tif (!record) {\n\t\tthrow new SessionError(\"storage\", `Lane ${lane} points at missing open operation ${laneRow.open_operation_id}`);\n\t}\n\tif (record.lane !== lane || record.type !== \"operation_started\") {\n\t\tthrow new SessionError(\"storage\", `Lane ${lane} points at invalid open operation ${laneRow.open_operation_id}`);\n\t}\n\treturn [record];\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import type { SqliteDatabase } from "../types.ts";
2
+ export declare function createSequence(db: SqliteDatabase, sessionId: string, nextSeq?: number): void;
3
+ export declare function getNextSequence(db: SqliteDatabase, sessionId: string): number;
4
+ export declare function setNextSequence(db: SqliteDatabase, sessionId: string, nextSeq: number): void;
5
+ export declare function advanceSequence(db: SqliteDatabase, sessionId: string, seq: number): void;
6
+ export declare function deleteSequence(db: SqliteDatabase, sessionId: string): void;
7
+ //# sourceMappingURL=session-sequences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-sequences.d.ts","sourceRoot":"","sources":["../../../src/sqlite/storage/session-sequences.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,SAAI,QAEhF;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,UAQpE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAErF;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,QAEjF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,QAEnE","sourcesContent":["import { SessionError } from \"@sammorrowdrums/mcpi-agent-core\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport function createSequence(db: SqliteDatabase, sessionId: string, nextSeq = 1) {\n\tsql`INSERT INTO session_sequences (session_id, next_seq) VALUES (${sessionId}, ${nextSeq})`.run(db);\n}\n\nexport function getNextSequence(db: SqliteDatabase, sessionId: string) {\n\tconst sequenceRow = sql`SELECT next_seq FROM session_sequences WHERE session_id = ${sessionId}`.get<{\n\t\tnext_seq: number;\n\t}>(db);\n\tif (!sequenceRow) {\n\t\tthrow new SessionError(\"storage\", `Missing sequence row for session ${sessionId}`);\n\t}\n\treturn sequenceRow.next_seq;\n}\n\nexport function setNextSequence(db: SqliteDatabase, sessionId: string, nextSeq: number) {\n\tsql`UPDATE session_sequences SET next_seq = ${nextSeq} WHERE session_id = ${sessionId}`.run(db);\n}\n\nexport function advanceSequence(db: SqliteDatabase, sessionId: string, seq: number) {\n\tsetNextSequence(db, sessionId, seq + 1);\n}\n\nexport function deleteSequence(db: SqliteDatabase, sessionId: string) {\n\tsql`DELETE FROM session_sequences WHERE session_id = ${sessionId}`.run(db);\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import { SessionError } from "@sammorrowdrums/mcpi-agent-core";
2
+ import { sql } from "../sql.js";
3
+ export function createSequence(db, sessionId, nextSeq = 1) {
4
+ sql `INSERT INTO session_sequences (session_id, next_seq) VALUES (${sessionId}, ${nextSeq})`.run(db);
5
+ }
6
+ export function getNextSequence(db, sessionId) {
7
+ const sequenceRow = sql `SELECT next_seq FROM session_sequences WHERE session_id = ${sessionId}`.get(db);
8
+ if (!sequenceRow) {
9
+ throw new SessionError("storage", `Missing sequence row for session ${sessionId}`);
10
+ }
11
+ return sequenceRow.next_seq;
12
+ }
13
+ export function setNextSequence(db, sessionId, nextSeq) {
14
+ sql `UPDATE session_sequences SET next_seq = ${nextSeq} WHERE session_id = ${sessionId}`.run(db);
15
+ }
16
+ export function advanceSequence(db, sessionId, seq) {
17
+ setNextSequence(db, sessionId, seq + 1);
18
+ }
19
+ export function deleteSequence(db, sessionId) {
20
+ sql `DELETE FROM session_sequences WHERE session_id = ${sessionId}`.run(db);
21
+ }
22
+ //# sourceMappingURL=session-sequences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-sequences.js","sourceRoot":"","sources":["../../../src/sqlite/storage/session-sequences.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAGhC,MAAM,UAAU,cAAc,CAAC,EAAkB,EAAE,SAAiB,EAAE,OAAO,GAAG,CAAC,EAAE;IAClF,GAAG,CAAA,gEAAgE,SAAS,KAAK,OAAO,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAAA,CACpG;AAED,MAAM,UAAU,eAAe,CAAC,EAAkB,EAAE,SAAiB,EAAE;IACtE,MAAM,WAAW,GAAG,GAAG,CAAA,6DAA6D,SAAS,EAAE,CAAC,GAAG,CAEhG,EAAE,CAAC,CAAC;IACP,IAAI,CAAC,WAAW,EAAE,CAAC;QAClB,MAAM,IAAI,YAAY,CAAC,SAAS,EAAE,oCAAoC,SAAS,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,WAAW,CAAC,QAAQ,CAAC;AAAA,CAC5B;AAED,MAAM,UAAU,eAAe,CAAC,EAAkB,EAAE,SAAiB,EAAE,OAAe,EAAE;IACvF,GAAG,CAAA,2CAA2C,OAAO,uBAAuB,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAAA,CAChG;AAED,MAAM,UAAU,eAAe,CAAC,EAAkB,EAAE,SAAiB,EAAE,GAAW,EAAE;IACnF,eAAe,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;AAAA,CACxC;AAED,MAAM,UAAU,cAAc,CAAC,EAAkB,EAAE,SAAiB,EAAE;IACrE,GAAG,CAAA,oDAAoD,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAAA,CAC3E","sourcesContent":["import { SessionError } from \"@sammorrowdrums/mcpi-agent-core\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport function createSequence(db: SqliteDatabase, sessionId: string, nextSeq = 1) {\n\tsql`INSERT INTO session_sequences (session_id, next_seq) VALUES (${sessionId}, ${nextSeq})`.run(db);\n}\n\nexport function getNextSequence(db: SqliteDatabase, sessionId: string) {\n\tconst sequenceRow = sql`SELECT next_seq FROM session_sequences WHERE session_id = ${sessionId}`.get<{\n\t\tnext_seq: number;\n\t}>(db);\n\tif (!sequenceRow) {\n\t\tthrow new SessionError(\"storage\", `Missing sequence row for session ${sessionId}`);\n\t}\n\treturn sequenceRow.next_seq;\n}\n\nexport function setNextSequence(db: SqliteDatabase, sessionId: string, nextSeq: number) {\n\tsql`UPDATE session_sequences SET next_seq = ${nextSeq} WHERE session_id = ${sessionId}`.run(db);\n}\n\nexport function advanceSequence(db: SqliteDatabase, sessionId: string, seq: number) {\n\tsetNextSequence(db, sessionId, seq + 1);\n}\n\nexport function deleteSequence(db: SqliteDatabase, sessionId: string) {\n\tsql`DELETE FROM session_sequences WHERE session_id = ${sessionId}`.run(db);\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import { type SessionStats } from "@sammorrowdrums/mcpi-agent-core";
2
+ import type { Usage } from "@sammorrowdrums/mcpi-ai";
3
+ import type { SqliteDatabase } from "../types.ts";
4
+ export interface SessionStatsRow {
5
+ session_id: string;
6
+ message_count: number;
7
+ cached_tokens: number;
8
+ uncached_tokens: number;
9
+ total_tokens: number;
10
+ cost_total: number;
11
+ }
12
+ export declare function createStats(db: SqliteDatabase, sessionId: string, messageCount?: number): void;
13
+ export declare function readStats(db: SqliteDatabase, sessionId: string): SessionStats;
14
+ export declare function incrementMessageCount(db: SqliteDatabase, sessionId: string): void;
15
+ export declare function addUsageToStats(db: SqliteDatabase, sessionId: string, usage: Usage): void;
16
+ export declare function deleteStats(db: SqliteDatabase, sessionId: string): void;
17
+ //# sourceMappingURL=session-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-stats.d.ts","sourceRoot":"","sources":["../../../src/sqlite/storage/session-stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAClF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,SAAI,GAAG,IAAI,CAIzF;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAY7E;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAKjF;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAQzF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAEvE","sourcesContent":["import { SessionError, type SessionStats } from \"@sammorrowdrums/mcpi-agent-core\";\nimport type { Usage } from \"@sammorrowdrums/mcpi-ai\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport interface SessionStatsRow {\n\tsession_id: string;\n\tmessage_count: number;\n\tcached_tokens: number;\n\tuncached_tokens: number;\n\ttotal_tokens: number;\n\tcost_total: number;\n}\n\nexport function createStats(db: SqliteDatabase, sessionId: string, messageCount = 0): void {\n\tsql`INSERT INTO session_stats\n\t\t\t(session_id, message_count, cached_tokens, uncached_tokens, total_tokens, cost_total)\n\t\t\tVALUES (${sessionId}, ${messageCount}, 0, 0, 0, 0)`.run(db);\n}\n\nexport function readStats(db: SqliteDatabase, sessionId: string): SessionStats {\n\tconst row = sql`SELECT session_id, message_count, cached_tokens, uncached_tokens, total_tokens, cost_total\n\t\tFROM session_stats\n\t\tWHERE session_id = ${sessionId}`.get<SessionStatsRow>(db);\n\tif (!row) throw new SessionError(\"storage\", `Missing stats row for session ${sessionId}`);\n\treturn {\n\t\tmessageCount: row.message_count,\n\t\tcachedTokens: row.cached_tokens,\n\t\tuncachedTokens: row.uncached_tokens,\n\t\ttotalTokens: row.total_tokens,\n\t\tcostTotal: row.cost_total,\n\t};\n}\n\nexport function incrementMessageCount(db: SqliteDatabase, sessionId: string): void {\n\tconst result = sql`UPDATE session_stats SET message_count = message_count + 1 WHERE session_id = ${sessionId}`.run(\n\t\tdb,\n\t);\n\tif (result.changes !== 1) throw new SessionError(\"storage\", `Missing stats row for session ${sessionId}`);\n}\n\nexport function addUsageToStats(db: SqliteDatabase, sessionId: string, usage: Usage): void {\n\tconst result = sql`UPDATE session_stats\n\t\tSET cached_tokens = cached_tokens + ${usage.cacheRead},\n\t\t\tuncached_tokens = uncached_tokens + ${usage.input + usage.cacheWrite},\n\t\t\ttotal_tokens = total_tokens + ${usage.totalTokens},\n\t\t\tcost_total = cost_total + ${usage.cost.total}\n\t\tWHERE session_id = ${sessionId}`.run(db);\n\tif (result.changes !== 1) throw new SessionError(\"storage\", `Missing stats row for session ${sessionId}`);\n}\n\nexport function deleteStats(db: SqliteDatabase, sessionId: string): void {\n\tsql`DELETE FROM session_stats WHERE session_id = ${sessionId}`.run(db);\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import { SessionError } from "@sammorrowdrums/mcpi-agent-core";
2
+ import { sql } from "../sql.js";
3
+ export function createStats(db, sessionId, messageCount = 0) {
4
+ sql `INSERT INTO session_stats
5
+ (session_id, message_count, cached_tokens, uncached_tokens, total_tokens, cost_total)
6
+ VALUES (${sessionId}, ${messageCount}, 0, 0, 0, 0)`.run(db);
7
+ }
8
+ export function readStats(db, sessionId) {
9
+ const row = sql `SELECT session_id, message_count, cached_tokens, uncached_tokens, total_tokens, cost_total
10
+ FROM session_stats
11
+ WHERE session_id = ${sessionId}`.get(db);
12
+ if (!row)
13
+ throw new SessionError("storage", `Missing stats row for session ${sessionId}`);
14
+ return {
15
+ messageCount: row.message_count,
16
+ cachedTokens: row.cached_tokens,
17
+ uncachedTokens: row.uncached_tokens,
18
+ totalTokens: row.total_tokens,
19
+ costTotal: row.cost_total,
20
+ };
21
+ }
22
+ export function incrementMessageCount(db, sessionId) {
23
+ const result = sql `UPDATE session_stats SET message_count = message_count + 1 WHERE session_id = ${sessionId}`.run(db);
24
+ if (result.changes !== 1)
25
+ throw new SessionError("storage", `Missing stats row for session ${sessionId}`);
26
+ }
27
+ export function addUsageToStats(db, sessionId, usage) {
28
+ const result = sql `UPDATE session_stats
29
+ SET cached_tokens = cached_tokens + ${usage.cacheRead},
30
+ uncached_tokens = uncached_tokens + ${usage.input + usage.cacheWrite},
31
+ total_tokens = total_tokens + ${usage.totalTokens},
32
+ cost_total = cost_total + ${usage.cost.total}
33
+ WHERE session_id = ${sessionId}`.run(db);
34
+ if (result.changes !== 1)
35
+ throw new SessionError("storage", `Missing stats row for session ${sessionId}`);
36
+ }
37
+ export function deleteStats(db, sessionId) {
38
+ sql `DELETE FROM session_stats WHERE session_id = ${sessionId}`.run(db);
39
+ }
40
+ //# sourceMappingURL=session-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-stats.js","sourceRoot":"","sources":["../../../src/sqlite/storage/session-stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,iCAAiC,CAAC;AAElF,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAYhC,MAAM,UAAU,WAAW,CAAC,EAAkB,EAAE,SAAiB,EAAE,YAAY,GAAG,CAAC,EAAQ;IAC1F,GAAG,CAAA;;aAES,SAAS,KAAK,YAAY,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAAA,CAC9D;AAED,MAAM,UAAU,SAAS,CAAC,EAAkB,EAAE,SAAiB,EAAgB;IAC9E,MAAM,GAAG,GAAG,GAAG,CAAA;;uBAEO,SAAS,EAAE,CAAC,GAAG,CAAkB,EAAE,CAAC,CAAC;IAC3D,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,YAAY,CAAC,SAAS,EAAE,iCAAiC,SAAS,EAAE,CAAC,CAAC;IAC1F,OAAO;QACN,YAAY,EAAE,GAAG,CAAC,aAAa;QAC/B,YAAY,EAAE,GAAG,CAAC,aAAa;QAC/B,cAAc,EAAE,GAAG,CAAC,eAAe;QACnC,WAAW,EAAE,GAAG,CAAC,YAAY;QAC7B,SAAS,EAAE,GAAG,CAAC,UAAU;KACzB,CAAC;AAAA,CACF;AAED,MAAM,UAAU,qBAAqB,CAAC,EAAkB,EAAE,SAAiB,EAAQ;IAClF,MAAM,MAAM,GAAG,GAAG,CAAA,iFAAiF,SAAS,EAAE,CAAC,GAAG,CACjH,EAAE,CACF,CAAC;IACF,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,SAAS,EAAE,iCAAiC,SAAS,EAAE,CAAC,CAAC;AAAA,CAC1G;AAED,MAAM,UAAU,eAAe,CAAC,EAAkB,EAAE,SAAiB,EAAE,KAAY,EAAQ;IAC1F,MAAM,MAAM,GAAG,GAAG,CAAA;wCACqB,KAAK,CAAC,SAAS;yCACd,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,UAAU;mCACpC,KAAK,CAAC,WAAW;+BACrB,KAAK,CAAC,IAAI,CAAC,KAAK;uBACxB,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC1C,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,SAAS,EAAE,iCAAiC,SAAS,EAAE,CAAC,CAAC;AAAA,CAC1G;AAED,MAAM,UAAU,WAAW,CAAC,EAAkB,EAAE,SAAiB,EAAQ;IACxE,GAAG,CAAA,gDAAgD,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAAA,CACvE","sourcesContent":["import { SessionError, type SessionStats } from \"@sammorrowdrums/mcpi-agent-core\";\nimport type { Usage } from \"@sammorrowdrums/mcpi-ai\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport interface SessionStatsRow {\n\tsession_id: string;\n\tmessage_count: number;\n\tcached_tokens: number;\n\tuncached_tokens: number;\n\ttotal_tokens: number;\n\tcost_total: number;\n}\n\nexport function createStats(db: SqliteDatabase, sessionId: string, messageCount = 0): void {\n\tsql`INSERT INTO session_stats\n\t\t\t(session_id, message_count, cached_tokens, uncached_tokens, total_tokens, cost_total)\n\t\t\tVALUES (${sessionId}, ${messageCount}, 0, 0, 0, 0)`.run(db);\n}\n\nexport function readStats(db: SqliteDatabase, sessionId: string): SessionStats {\n\tconst row = sql`SELECT session_id, message_count, cached_tokens, uncached_tokens, total_tokens, cost_total\n\t\tFROM session_stats\n\t\tWHERE session_id = ${sessionId}`.get<SessionStatsRow>(db);\n\tif (!row) throw new SessionError(\"storage\", `Missing stats row for session ${sessionId}`);\n\treturn {\n\t\tmessageCount: row.message_count,\n\t\tcachedTokens: row.cached_tokens,\n\t\tuncachedTokens: row.uncached_tokens,\n\t\ttotalTokens: row.total_tokens,\n\t\tcostTotal: row.cost_total,\n\t};\n}\n\nexport function incrementMessageCount(db: SqliteDatabase, sessionId: string): void {\n\tconst result = sql`UPDATE session_stats SET message_count = message_count + 1 WHERE session_id = ${sessionId}`.run(\n\t\tdb,\n\t);\n\tif (result.changes !== 1) throw new SessionError(\"storage\", `Missing stats row for session ${sessionId}`);\n}\n\nexport function addUsageToStats(db: SqliteDatabase, sessionId: string, usage: Usage): void {\n\tconst result = sql`UPDATE session_stats\n\t\tSET cached_tokens = cached_tokens + ${usage.cacheRead},\n\t\t\tuncached_tokens = uncached_tokens + ${usage.input + usage.cacheWrite},\n\t\t\ttotal_tokens = total_tokens + ${usage.totalTokens},\n\t\t\tcost_total = cost_total + ${usage.cost.total}\n\t\tWHERE session_id = ${sessionId}`.run(db);\n\tif (result.changes !== 1) throw new SessionError(\"storage\", `Missing stats row for session ${sessionId}`);\n}\n\nexport function deleteStats(db: SqliteDatabase, sessionId: string): void {\n\tsql`DELETE FROM session_stats WHERE session_id = ${sessionId}`.run(db);\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import type { SqliteDatabase, SqliteSessionMetadata } from "../types.ts";
2
+ export interface SessionRow {
3
+ id: string;
4
+ created_at: number;
5
+ metadata: string | null;
6
+ cwd: string;
7
+ parent_session_id: string | null;
8
+ has_session_name: number;
9
+ session_name: string | null;
10
+ }
11
+ export interface NewSessionRow {
12
+ id: string;
13
+ createdAt: number;
14
+ cwd: string;
15
+ parentSessionId?: string;
16
+ metadata?: Record<string, unknown>;
17
+ }
18
+ export declare function sessionExists(db: SqliteDatabase, sessionId: string): boolean;
19
+ export declare function insertSessionRow(db: SqliteDatabase, session: NewSessionRow): void;
20
+ export declare function readSessionRow(db: SqliteDatabase, sessionId: string): SessionRow | undefined;
21
+ export declare function readSessionRows(db: SqliteDatabase, options?: {
22
+ cwd?: string;
23
+ }): SessionRow[];
24
+ export declare function deleteSessionRow(db: SqliteDatabase, sessionId: string): void;
25
+ export declare function decodeSessionMetadata(row: SessionRow, path: string): SqliteSessionMetadata;
26
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/sqlite/storage/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzE,MAAM,WAAW,UAAU;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAoBD,wBAAgB,aAAa,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAElE;AAWD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,QAK1E;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,0BAenE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,cAAc,EAAE,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,gBAiBjF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,QAErE;AAoBD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,qBAAqB,CAY1F","sourcesContent":["import { assertJsonSerializable, SessionError } from \"@sammorrowdrums/mcpi-agent-core\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase, SqliteSessionMetadata } from \"../types.ts\";\n\nexport interface SessionRow {\n\tid: string;\n\tcreated_at: number;\n\tmetadata: string | null;\n\tcwd: string;\n\tparent_session_id: string | null;\n\thas_session_name: number;\n\tsession_name: string | null;\n}\n\nexport interface NewSessionRow {\n\tid: string;\n\tcreatedAt: number;\n\tcwd: string;\n\tparentSessionId?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nfunction parseMetadata(metadata: string | null, sessionId: string): Record<string, unknown> | undefined {\n\tif (metadata === null) return undefined;\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(metadata);\n\t} catch (error) {\n\t\tthrow new SessionError(\n\t\t\t\"storage\",\n\t\t\t`Invalid SQLite session ${sessionId}: metadata is not valid JSON`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new SessionError(\"storage\", `Invalid SQLite session ${sessionId}: metadata must be an object`);\n\t}\n\treturn parsed as Record<string, unknown>;\n}\n\nexport function sessionExists(db: SqliteDatabase, sessionId: string) {\n\treturn !!sql`SELECT 1 AS found FROM sessions WHERE id = ${sessionId}`.get<{ found: number }>(db);\n}\n\nfunction serializeMetadata(metadata: Record<string, unknown> | undefined): string | null {\n\tif (metadata === undefined) return null;\n\tif (typeof metadata !== \"object\" || metadata === null || Array.isArray(metadata)) {\n\t\tthrow new SessionError(\"invalid_payload\", \"SQLite session metadata must be an object\");\n\t}\n\tassertJsonSerializable(metadata);\n\treturn JSON.stringify(metadata);\n}\n\nexport function insertSessionRow(db: SqliteDatabase, session: NewSessionRow) {\n\tsql`INSERT INTO sessions (id, created_at, metadata, cwd, parent_session_id)\n\t\tVALUES (${session.id}, ${session.createdAt}, ${serializeMetadata(session.metadata)}, ${session.cwd}, ${session.parentSessionId ?? null})`.run(\n\t\tdb,\n\t);\n}\n\nexport function readSessionRow(db: SqliteDatabase, sessionId: string) {\n\treturn sql`SELECT s.id, s.created_at, s.metadata, s.cwd, s.parent_session_id,\n\t\t\tname_fact.seq IS NOT NULL AS has_session_name,\n\t\t\tname_fact.value AS session_name\n\t\tFROM sessions AS s\n\t\tLEFT JOIN facts AS name_fact\n\t\t\tON name_fact.session_id = s.id\n\t\t\tAND name_fact.kind = 'name'\n\t\t\tAND name_fact.key IS NULL\n\t\t\tAND name_fact.seq = (\n\t\t\t\tSELECT MAX(f.seq)\n\t\t\t\tFROM facts AS f\n\t\t\t\tWHERE f.session_id = s.id AND f.kind = 'name' AND f.key IS NULL\n\t\t\t)\n\t\tWHERE s.id = ${sessionId}`.get<SessionRow>(db);\n}\n\nexport function readSessionRows(db: SqliteDatabase, options: { cwd?: string } = {}) {\n\tconst where = options.cwd === undefined ? sql`` : sql`WHERE s.cwd = ${options.cwd}`;\n\treturn sql`SELECT s.id, s.created_at, s.metadata, s.cwd, s.parent_session_id,\n\t\t\tname_fact.seq IS NOT NULL AS has_session_name,\n\t\t\tname_fact.value AS session_name\n\t\tFROM sessions AS s\n\t\tLEFT JOIN facts AS name_fact\n\t\t\tON name_fact.session_id = s.id\n\t\t\tAND name_fact.kind = 'name'\n\t\t\tAND name_fact.key IS NULL\n\t\t\tAND name_fact.seq = (\n\t\t\t\tSELECT MAX(f.seq)\n\t\t\t\tFROM facts AS f\n\t\t\t\tWHERE f.session_id = s.id AND f.kind = 'name' AND f.key IS NULL\n\t\t\t)\n\t\t${where}\n\t\tORDER BY s.created_at DESC`.all<SessionRow>(db);\n}\n\nexport function deleteSessionRow(db: SqliteDatabase, sessionId: string) {\n\tsql`DELETE FROM sessions WHERE id = ${sessionId}`.run(db);\n}\n\nfunction parseSessionName(value: string | null, sessionId: string): string | undefined {\n\tif (value === null) return undefined;\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(value);\n\t} catch (error) {\n\t\tthrow new SessionError(\n\t\t\t\"storage\",\n\t\t\t`Invalid SQLite session ${sessionId}: name is not valid JSON`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n\tif (typeof parsed !== \"string\") {\n\t\tthrow new SessionError(\"storage\", `Invalid SQLite session ${sessionId}: name must be a string`);\n\t}\n\treturn parsed;\n}\n\nexport function decodeSessionMetadata(row: SessionRow, path: string): SqliteSessionMetadata {\n\tconst metadata = parseMetadata(row.metadata, row.id);\n\tconst name = row.has_session_name === 0 ? undefined : parseSessionName(row.session_name, row.id);\n\treturn {\n\t\tid: row.id,\n\t\tcreatedAt: row.created_at,\n\t\t...(name === undefined ? {} : { name }),\n\t\tcwd: row.cwd,\n\t\tpath,\n\t\tparentSessionId: row.parent_session_id ?? undefined,\n\t\tmetadata,\n\t};\n}\n"]}
@@ -0,0 +1,99 @@
1
+ import { assertJsonSerializable, SessionError } from "@sammorrowdrums/mcpi-agent-core";
2
+ import { sql } from "../sql.js";
3
+ function parseMetadata(metadata, sessionId) {
4
+ if (metadata === null)
5
+ return undefined;
6
+ let parsed;
7
+ try {
8
+ parsed = JSON.parse(metadata);
9
+ }
10
+ catch (error) {
11
+ throw new SessionError("storage", `Invalid SQLite session ${sessionId}: metadata is not valid JSON`, error instanceof Error ? error : undefined);
12
+ }
13
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
14
+ throw new SessionError("storage", `Invalid SQLite session ${sessionId}: metadata must be an object`);
15
+ }
16
+ return parsed;
17
+ }
18
+ export function sessionExists(db, sessionId) {
19
+ return !!sql `SELECT 1 AS found FROM sessions WHERE id = ${sessionId}`.get(db);
20
+ }
21
+ function serializeMetadata(metadata) {
22
+ if (metadata === undefined)
23
+ return null;
24
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
25
+ throw new SessionError("invalid_payload", "SQLite session metadata must be an object");
26
+ }
27
+ assertJsonSerializable(metadata);
28
+ return JSON.stringify(metadata);
29
+ }
30
+ export function insertSessionRow(db, session) {
31
+ sql `INSERT INTO sessions (id, created_at, metadata, cwd, parent_session_id)
32
+ VALUES (${session.id}, ${session.createdAt}, ${serializeMetadata(session.metadata)}, ${session.cwd}, ${session.parentSessionId ?? null})`.run(db);
33
+ }
34
+ export function readSessionRow(db, sessionId) {
35
+ return sql `SELECT s.id, s.created_at, s.metadata, s.cwd, s.parent_session_id,
36
+ name_fact.seq IS NOT NULL AS has_session_name,
37
+ name_fact.value AS session_name
38
+ FROM sessions AS s
39
+ LEFT JOIN facts AS name_fact
40
+ ON name_fact.session_id = s.id
41
+ AND name_fact.kind = 'name'
42
+ AND name_fact.key IS NULL
43
+ AND name_fact.seq = (
44
+ SELECT MAX(f.seq)
45
+ FROM facts AS f
46
+ WHERE f.session_id = s.id AND f.kind = 'name' AND f.key IS NULL
47
+ )
48
+ WHERE s.id = ${sessionId}`.get(db);
49
+ }
50
+ export function readSessionRows(db, options = {}) {
51
+ const where = options.cwd === undefined ? sql `` : sql `WHERE s.cwd = ${options.cwd}`;
52
+ return sql `SELECT s.id, s.created_at, s.metadata, s.cwd, s.parent_session_id,
53
+ name_fact.seq IS NOT NULL AS has_session_name,
54
+ name_fact.value AS session_name
55
+ FROM sessions AS s
56
+ LEFT JOIN facts AS name_fact
57
+ ON name_fact.session_id = s.id
58
+ AND name_fact.kind = 'name'
59
+ AND name_fact.key IS NULL
60
+ AND name_fact.seq = (
61
+ SELECT MAX(f.seq)
62
+ FROM facts AS f
63
+ WHERE f.session_id = s.id AND f.kind = 'name' AND f.key IS NULL
64
+ )
65
+ ${where}
66
+ ORDER BY s.created_at DESC`.all(db);
67
+ }
68
+ export function deleteSessionRow(db, sessionId) {
69
+ sql `DELETE FROM sessions WHERE id = ${sessionId}`.run(db);
70
+ }
71
+ function parseSessionName(value, sessionId) {
72
+ if (value === null)
73
+ return undefined;
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(value);
77
+ }
78
+ catch (error) {
79
+ throw new SessionError("storage", `Invalid SQLite session ${sessionId}: name is not valid JSON`, error instanceof Error ? error : undefined);
80
+ }
81
+ if (typeof parsed !== "string") {
82
+ throw new SessionError("storage", `Invalid SQLite session ${sessionId}: name must be a string`);
83
+ }
84
+ return parsed;
85
+ }
86
+ export function decodeSessionMetadata(row, path) {
87
+ const metadata = parseMetadata(row.metadata, row.id);
88
+ const name = row.has_session_name === 0 ? undefined : parseSessionName(row.session_name, row.id);
89
+ return {
90
+ id: row.id,
91
+ createdAt: row.created_at,
92
+ ...(name === undefined ? {} : { name }),
93
+ cwd: row.cwd,
94
+ path,
95
+ parentSessionId: row.parent_session_id ?? undefined,
96
+ metadata,
97
+ };
98
+ }
99
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../../src/sqlite/storage/sessions.ts"],"names":[],"mappings":"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","sourcesContent":["import { assertJsonSerializable, SessionError } from \"@sammorrowdrums/mcpi-agent-core\";\nimport { sql } from \"../sql.ts\";\nimport type { SqliteDatabase, SqliteSessionMetadata } from \"../types.ts\";\n\nexport interface SessionRow {\n\tid: string;\n\tcreated_at: number;\n\tmetadata: string | null;\n\tcwd: string;\n\tparent_session_id: string | null;\n\thas_session_name: number;\n\tsession_name: string | null;\n}\n\nexport interface NewSessionRow {\n\tid: string;\n\tcreatedAt: number;\n\tcwd: string;\n\tparentSessionId?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nfunction parseMetadata(metadata: string | null, sessionId: string): Record<string, unknown> | undefined {\n\tif (metadata === null) return undefined;\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(metadata);\n\t} catch (error) {\n\t\tthrow new SessionError(\n\t\t\t\"storage\",\n\t\t\t`Invalid SQLite session ${sessionId}: metadata is not valid JSON`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n\t\tthrow new SessionError(\"storage\", `Invalid SQLite session ${sessionId}: metadata must be an object`);\n\t}\n\treturn parsed as Record<string, unknown>;\n}\n\nexport function sessionExists(db: SqliteDatabase, sessionId: string) {\n\treturn !!sql`SELECT 1 AS found FROM sessions WHERE id = ${sessionId}`.get<{ found: number }>(db);\n}\n\nfunction serializeMetadata(metadata: Record<string, unknown> | undefined): string | null {\n\tif (metadata === undefined) return null;\n\tif (typeof metadata !== \"object\" || metadata === null || Array.isArray(metadata)) {\n\t\tthrow new SessionError(\"invalid_payload\", \"SQLite session metadata must be an object\");\n\t}\n\tassertJsonSerializable(metadata);\n\treturn JSON.stringify(metadata);\n}\n\nexport function insertSessionRow(db: SqliteDatabase, session: NewSessionRow) {\n\tsql`INSERT INTO sessions (id, created_at, metadata, cwd, parent_session_id)\n\t\tVALUES (${session.id}, ${session.createdAt}, ${serializeMetadata(session.metadata)}, ${session.cwd}, ${session.parentSessionId ?? null})`.run(\n\t\tdb,\n\t);\n}\n\nexport function readSessionRow(db: SqliteDatabase, sessionId: string) {\n\treturn sql`SELECT s.id, s.created_at, s.metadata, s.cwd, s.parent_session_id,\n\t\t\tname_fact.seq IS NOT NULL AS has_session_name,\n\t\t\tname_fact.value AS session_name\n\t\tFROM sessions AS s\n\t\tLEFT JOIN facts AS name_fact\n\t\t\tON name_fact.session_id = s.id\n\t\t\tAND name_fact.kind = 'name'\n\t\t\tAND name_fact.key IS NULL\n\t\t\tAND name_fact.seq = (\n\t\t\t\tSELECT MAX(f.seq)\n\t\t\t\tFROM facts AS f\n\t\t\t\tWHERE f.session_id = s.id AND f.kind = 'name' AND f.key IS NULL\n\t\t\t)\n\t\tWHERE s.id = ${sessionId}`.get<SessionRow>(db);\n}\n\nexport function readSessionRows(db: SqliteDatabase, options: { cwd?: string } = {}) {\n\tconst where = options.cwd === undefined ? sql`` : sql`WHERE s.cwd = ${options.cwd}`;\n\treturn sql`SELECT s.id, s.created_at, s.metadata, s.cwd, s.parent_session_id,\n\t\t\tname_fact.seq IS NOT NULL AS has_session_name,\n\t\t\tname_fact.value AS session_name\n\t\tFROM sessions AS s\n\t\tLEFT JOIN facts AS name_fact\n\t\t\tON name_fact.session_id = s.id\n\t\t\tAND name_fact.kind = 'name'\n\t\t\tAND name_fact.key IS NULL\n\t\t\tAND name_fact.seq = (\n\t\t\t\tSELECT MAX(f.seq)\n\t\t\t\tFROM facts AS f\n\t\t\t\tWHERE f.session_id = s.id AND f.kind = 'name' AND f.key IS NULL\n\t\t\t)\n\t\t${where}\n\t\tORDER BY s.created_at DESC`.all<SessionRow>(db);\n}\n\nexport function deleteSessionRow(db: SqliteDatabase, sessionId: string) {\n\tsql`DELETE FROM sessions WHERE id = ${sessionId}`.run(db);\n}\n\nfunction parseSessionName(value: string | null, sessionId: string): string | undefined {\n\tif (value === null) return undefined;\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(value);\n\t} catch (error) {\n\t\tthrow new SessionError(\n\t\t\t\"storage\",\n\t\t\t`Invalid SQLite session ${sessionId}: name is not valid JSON`,\n\t\t\terror instanceof Error ? error : undefined,\n\t\t);\n\t}\n\tif (typeof parsed !== \"string\") {\n\t\tthrow new SessionError(\"storage\", `Invalid SQLite session ${sessionId}: name must be a string`);\n\t}\n\treturn parsed;\n}\n\nexport function decodeSessionMetadata(row: SessionRow, path: string): SqliteSessionMetadata {\n\tconst metadata = parseMetadata(row.metadata, row.id);\n\tconst name = row.has_session_name === 0 ? undefined : parseSessionName(row.session_name, row.id);\n\treturn {\n\t\tid: row.id,\n\t\tcreatedAt: row.created_at,\n\t\t...(name === undefined ? {} : { name }),\n\t\tcwd: row.cwd,\n\t\tpath,\n\t\tparentSessionId: row.parent_session_id ?? undefined,\n\t\tmetadata,\n\t};\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { SqliteDatabase } from "../types.ts";
2
+ export interface WriterLease {
3
+ ownerId: string;
4
+ fence: number;
5
+ expiresAtMs: number;
6
+ }
7
+ export declare function acquireWriterLease(db: SqliteDatabase, sessionId: string, ownerId: string, now: number, expiresAtMs: number): {
8
+ ownerId: string;
9
+ fence: number;
10
+ expiresAtMs: number;
11
+ } | undefined;
12
+ export declare function renewWriterLease(db: SqliteDatabase, sessionId: string, lease: WriterLease, now: number, expiresAtMs: number): boolean;
13
+ export declare function releaseWriterLease(db: SqliteDatabase, sessionId: string, lease: WriterLease): void;
14
+ export declare function deleteWriterLease(db: SqliteDatabase, sessionId: string): void;
15
+ //# sourceMappingURL=writer-leases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer-leases.d.ts","sourceRoot":"","sources":["../../../src/sqlite/storage/writer-leases.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACpB;AAQD,wBAAgB,kBAAkB,CACjC,EAAE,EAAE,cAAc,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM;;;;cAWnB;AAED,wBAAgB,gBAAgB,CAC/B,EAAE,EAAE,cAAc,EAClB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,WAUnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,QAG3F;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,QAEtE","sourcesContent":["import { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport interface WriterLease {\n\townerId: string;\n\tfence: number;\n\texpiresAtMs: number;\n}\n\ninterface WriterLeaseRow {\n\towner_id: string;\n\tfence: number;\n\texpires_at_ms: number;\n}\n\nexport function acquireWriterLease(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\townerId: string,\n\tnow: number,\n\texpiresAtMs: number,\n) {\n\tconst row = sql`INSERT INTO writer_leases (session_id, owner_id, fence, expires_at_ms)\n\t\tVALUES (${sessionId}, ${ownerId}, 1, ${expiresAtMs})\n\t\tON CONFLICT(session_id) DO UPDATE SET\n\t\t\towner_id = excluded.owner_id,\n\t\t\tfence = writer_leases.fence + 1,\n\t\t\texpires_at_ms = excluded.expires_at_ms\n\t\tWHERE writer_leases.expires_at_ms <= ${now}\n\t\tRETURNING owner_id, fence, expires_at_ms`.get<WriterLeaseRow>(db);\n\treturn row === undefined ? undefined : { ownerId: row.owner_id, fence: row.fence, expiresAtMs: row.expires_at_ms };\n}\n\nexport function renewWriterLease(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tlease: WriterLease,\n\tnow: number,\n\texpiresAtMs: number,\n) {\n\tconst result = sql`UPDATE writer_leases\n\t\tSET expires_at_ms = ${expiresAtMs}\n\t\tWHERE session_id = ${sessionId}\n\t\t\tAND owner_id = ${lease.ownerId}\n\t\t\tAND fence = ${lease.fence}\n\t\t\tAND expires_at_ms > ${now}`.run(db);\n\tif (result.changes === 1) lease.expiresAtMs = expiresAtMs;\n\treturn result.changes === 1;\n}\n\nexport function releaseWriterLease(db: SqliteDatabase, sessionId: string, lease: WriterLease) {\n\tsql`DELETE FROM writer_leases\n\t\tWHERE session_id = ${sessionId} AND owner_id = ${lease.ownerId} AND fence = ${lease.fence}`.run(db);\n}\n\nexport function deleteWriterLease(db: SqliteDatabase, sessionId: string) {\n\tsql`DELETE FROM writer_leases WHERE session_id = ${sessionId}`.run(db);\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import { sql } from "../sql.js";
2
+ export function acquireWriterLease(db, sessionId, ownerId, now, expiresAtMs) {
3
+ const row = sql `INSERT INTO writer_leases (session_id, owner_id, fence, expires_at_ms)
4
+ VALUES (${sessionId}, ${ownerId}, 1, ${expiresAtMs})
5
+ ON CONFLICT(session_id) DO UPDATE SET
6
+ owner_id = excluded.owner_id,
7
+ fence = writer_leases.fence + 1,
8
+ expires_at_ms = excluded.expires_at_ms
9
+ WHERE writer_leases.expires_at_ms <= ${now}
10
+ RETURNING owner_id, fence, expires_at_ms`.get(db);
11
+ return row === undefined ? undefined : { ownerId: row.owner_id, fence: row.fence, expiresAtMs: row.expires_at_ms };
12
+ }
13
+ export function renewWriterLease(db, sessionId, lease, now, expiresAtMs) {
14
+ const result = sql `UPDATE writer_leases
15
+ SET expires_at_ms = ${expiresAtMs}
16
+ WHERE session_id = ${sessionId}
17
+ AND owner_id = ${lease.ownerId}
18
+ AND fence = ${lease.fence}
19
+ AND expires_at_ms > ${now}`.run(db);
20
+ if (result.changes === 1)
21
+ lease.expiresAtMs = expiresAtMs;
22
+ return result.changes === 1;
23
+ }
24
+ export function releaseWriterLease(db, sessionId, lease) {
25
+ sql `DELETE FROM writer_leases
26
+ WHERE session_id = ${sessionId} AND owner_id = ${lease.ownerId} AND fence = ${lease.fence}`.run(db);
27
+ }
28
+ export function deleteWriterLease(db, sessionId) {
29
+ sql `DELETE FROM writer_leases WHERE session_id = ${sessionId}`.run(db);
30
+ }
31
+ //# sourceMappingURL=writer-leases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer-leases.js","sourceRoot":"","sources":["../../../src/sqlite/storage/writer-leases.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAehC,MAAM,UAAU,kBAAkB,CACjC,EAAkB,EAClB,SAAiB,EACjB,OAAe,EACf,GAAW,EACX,WAAmB,EAClB;IACD,MAAM,GAAG,GAAG,GAAG,CAAA;YACJ,SAAS,KAAK,OAAO,QAAQ,WAAW;;;;;yCAKX,GAAG;2CACD,CAAC,GAAG,CAAiB,EAAE,CAAC,CAAC;IACnE,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,CAAC,aAAa,EAAE,CAAC;AAAA,CACnH;AAED,MAAM,UAAU,gBAAgB,CAC/B,EAAkB,EAClB,SAAiB,EACjB,KAAkB,EAClB,GAAW,EACX,WAAmB,EAClB;IACD,MAAM,MAAM,GAAG,GAAG,CAAA;wBACK,WAAW;uBACZ,SAAS;oBACZ,KAAK,CAAC,OAAO;iBAChB,KAAK,CAAC,KAAK;yBACH,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACtC,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC;QAAE,KAAK,CAAC,WAAW,GAAG,WAAW,CAAC;IAC1D,OAAO,MAAM,CAAC,OAAO,KAAK,CAAC,CAAC;AAAA,CAC5B;AAED,MAAM,UAAU,kBAAkB,CAAC,EAAkB,EAAE,SAAiB,EAAE,KAAkB,EAAE;IAC7F,GAAG,CAAA;uBACmB,SAAS,mBAAmB,KAAK,CAAC,OAAO,gBAAgB,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAAA,CACrG;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAkB,EAAE,SAAiB,EAAE;IACxE,GAAG,CAAA,gDAAgD,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAAA,CACvE","sourcesContent":["import { sql } from \"../sql.ts\";\nimport type { SqliteDatabase } from \"../types.ts\";\n\nexport interface WriterLease {\n\townerId: string;\n\tfence: number;\n\texpiresAtMs: number;\n}\n\ninterface WriterLeaseRow {\n\towner_id: string;\n\tfence: number;\n\texpires_at_ms: number;\n}\n\nexport function acquireWriterLease(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\townerId: string,\n\tnow: number,\n\texpiresAtMs: number,\n) {\n\tconst row = sql`INSERT INTO writer_leases (session_id, owner_id, fence, expires_at_ms)\n\t\tVALUES (${sessionId}, ${ownerId}, 1, ${expiresAtMs})\n\t\tON CONFLICT(session_id) DO UPDATE SET\n\t\t\towner_id = excluded.owner_id,\n\t\t\tfence = writer_leases.fence + 1,\n\t\t\texpires_at_ms = excluded.expires_at_ms\n\t\tWHERE writer_leases.expires_at_ms <= ${now}\n\t\tRETURNING owner_id, fence, expires_at_ms`.get<WriterLeaseRow>(db);\n\treturn row === undefined ? undefined : { ownerId: row.owner_id, fence: row.fence, expiresAtMs: row.expires_at_ms };\n}\n\nexport function renewWriterLease(\n\tdb: SqliteDatabase,\n\tsessionId: string,\n\tlease: WriterLease,\n\tnow: number,\n\texpiresAtMs: number,\n) {\n\tconst result = sql`UPDATE writer_leases\n\t\tSET expires_at_ms = ${expiresAtMs}\n\t\tWHERE session_id = ${sessionId}\n\t\t\tAND owner_id = ${lease.ownerId}\n\t\t\tAND fence = ${lease.fence}\n\t\t\tAND expires_at_ms > ${now}`.run(db);\n\tif (result.changes === 1) lease.expiresAtMs = expiresAtMs;\n\treturn result.changes === 1;\n}\n\nexport function releaseWriterLease(db: SqliteDatabase, sessionId: string, lease: WriterLease) {\n\tsql`DELETE FROM writer_leases\n\t\tWHERE session_id = ${sessionId} AND owner_id = ${lease.ownerId} AND fence = ${lease.fence}`.run(db);\n}\n\nexport function deleteWriterLease(db: SqliteDatabase, sessionId: string) {\n\tsql`DELETE FROM writer_leases WHERE session_id = ${sessionId}`.run(db);\n}\n"]}
@@ -0,0 +1,45 @@
1
+ import type { FileSystem, SessionCreateOptions, SessionMetadata } from "@sammorrowdrums/mcpi-agent-core";
2
+ /** Result of a prepared SQLite statement execution. */
3
+ export interface SqliteRunResult {
4
+ /** Number of rows changed by the statement. */
5
+ changes: number;
6
+ /** Inserted row id when the backend exposes one. */
7
+ lastInsertRowid?: number;
8
+ }
9
+ /** Prepared SQLite statement capability used by the SQLite session backend. */
10
+ export interface SqliteStatement {
11
+ run(...params: unknown[]): SqliteRunResult;
12
+ get<TRow extends object>(...params: unknown[]): TRow | undefined;
13
+ all<TRow extends object>(...params: unknown[]): TRow[];
14
+ iterate<TRow extends object>(...params: unknown[]): Iterable<TRow>;
15
+ }
16
+ /** SQLite database capability used by the SQLite session backend. */
17
+ export interface SqliteDatabase {
18
+ exec(sql: string): void;
19
+ prepare(sql: string): SqliteStatement;
20
+ /** Runs a synchronous write transaction. The callback must not return a promise. */
21
+ transaction<T>(fn: () => T): T;
22
+ close(): void;
23
+ }
24
+ export interface SqliteDatabaseFactory {
25
+ open(path: string): Promise<SqliteDatabase>;
26
+ }
27
+ export interface SqliteSessionMetadata extends SessionMetadata {
28
+ cwd: string;
29
+ path: string;
30
+ parentSessionId?: string;
31
+ /** Current session name projected from SQLite global facts. */
32
+ name?: string;
33
+ /** Opaque application-owned metadata. */
34
+ metadata?: Record<string, unknown>;
35
+ }
36
+ export interface SqliteSessionCreateOptions extends SessionCreateOptions {
37
+ cwd: string;
38
+ parentSessionId?: string;
39
+ metadata?: Record<string, unknown>;
40
+ }
41
+ export interface SqliteSessionListOptions {
42
+ cwd?: string;
43
+ }
44
+ export type SqliteSessionRepositoryEnv = Pick<FileSystem, "absolutePath" | "createDir" | "exists">;
45
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sqlite/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEzG,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC/B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC/B,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,eAAe,CAAC;IAC3C,GAAG,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IACjE,GAAG,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;IACvD,OAAO,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;CACnE;AAED,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC9B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,oFAAoF;IACpF,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,KAAK,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACrC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC7D,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,0BAA2B,SAAQ,oBAAoB;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAAC,UAAU,EAAE,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAC,CAAC","sourcesContent":["import type { FileSystem, SessionCreateOptions, SessionMetadata } from \"@sammorrowdrums/mcpi-agent-core\";\n\n/** Result of a prepared SQLite statement execution. */\nexport interface SqliteRunResult {\n\t/** Number of rows changed by the statement. */\n\tchanges: number;\n\t/** Inserted row id when the backend exposes one. */\n\tlastInsertRowid?: number;\n}\n\n/** Prepared SQLite statement capability used by the SQLite session backend. */\nexport interface SqliteStatement {\n\trun(...params: unknown[]): SqliteRunResult;\n\tget<TRow extends object>(...params: unknown[]): TRow | undefined;\n\tall<TRow extends object>(...params: unknown[]): TRow[];\n\titerate<TRow extends object>(...params: unknown[]): Iterable<TRow>;\n}\n\n/** SQLite database capability used by the SQLite session backend. */\nexport interface SqliteDatabase {\n\texec(sql: string): void;\n\tprepare(sql: string): SqliteStatement;\n\t/** Runs a synchronous write transaction. The callback must not return a promise. */\n\ttransaction<T>(fn: () => T): T;\n\tclose(): void;\n}\n\nexport interface SqliteDatabaseFactory {\n\topen(path: string): Promise<SqliteDatabase>;\n}\n\nexport interface SqliteSessionMetadata extends SessionMetadata {\n\tcwd: string;\n\tpath: string;\n\tparentSessionId?: string;\n\t/** Current session name projected from SQLite global facts. */\n\tname?: string;\n\t/** Opaque application-owned metadata. */\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface SqliteSessionCreateOptions extends SessionCreateOptions {\n\tcwd: string;\n\tparentSessionId?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface SqliteSessionListOptions {\n\tcwd?: string;\n}\n\nexport type SqliteSessionRepositoryEnv = Pick<FileSystem, \"absolutePath\" | \"createDir\" | \"exists\">;\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sqlite/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { FileSystem, SessionCreateOptions, SessionMetadata } from \"@sammorrowdrums/mcpi-agent-core\";\n\n/** Result of a prepared SQLite statement execution. */\nexport interface SqliteRunResult {\n\t/** Number of rows changed by the statement. */\n\tchanges: number;\n\t/** Inserted row id when the backend exposes one. */\n\tlastInsertRowid?: number;\n}\n\n/** Prepared SQLite statement capability used by the SQLite session backend. */\nexport interface SqliteStatement {\n\trun(...params: unknown[]): SqliteRunResult;\n\tget<TRow extends object>(...params: unknown[]): TRow | undefined;\n\tall<TRow extends object>(...params: unknown[]): TRow[];\n\titerate<TRow extends object>(...params: unknown[]): Iterable<TRow>;\n}\n\n/** SQLite database capability used by the SQLite session backend. */\nexport interface SqliteDatabase {\n\texec(sql: string): void;\n\tprepare(sql: string): SqliteStatement;\n\t/** Runs a synchronous write transaction. The callback must not return a promise. */\n\ttransaction<T>(fn: () => T): T;\n\tclose(): void;\n}\n\nexport interface SqliteDatabaseFactory {\n\topen(path: string): Promise<SqliteDatabase>;\n}\n\nexport interface SqliteSessionMetadata extends SessionMetadata {\n\tcwd: string;\n\tpath: string;\n\tparentSessionId?: string;\n\t/** Current session name projected from SQLite global facts. */\n\tname?: string;\n\t/** Opaque application-owned metadata. */\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface SqliteSessionCreateOptions extends SessionCreateOptions {\n\tcwd: string;\n\tparentSessionId?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nexport interface SqliteSessionListOptions {\n\tcwd?: string;\n}\n\nexport type SqliteSessionRepositoryEnv = Pick<FileSystem, \"absolutePath\" | \"createDir\" | \"exists\">;\n"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@sammorrowdrums/mcpi-session-backend-sqlite-node",
3
+ "version": "0.85.0-bootstrap.0",
4
+ "description": "Node sqlite session backend for @sammorrowdrums/mcpi-agent-core sessions",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "clean": "node -e \"import('node:fs/promises').then(fs=>fs.rm('dist',{recursive:true,force:true}))\"",
22
+ "build": "tsgo -p tsconfig.build.json && node ./scripts/prepare-dist.mjs copy-sqlite-migrations",
23
+ "test": "vitest --run",
24
+ "coverage": "vitest --run --coverage",
25
+ "prepublishOnly": "npm run clean && npm run build"
26
+ },
27
+ "author": "Earendil Works",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/SamMorrowDrums/mcpi.git",
32
+ "directory": "packages/session-backends/sqlite-node"
33
+ },
34
+ "homepage": "https://github.com/SamMorrowDrums/mcpi/tree/main/packages/session-backends/sqlite-node#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/SamMorrowDrums/mcpi/issues"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "registry": "https://registry.npmjs.org/"
41
+ },
42
+ "engines": {
43
+ "node": ">=22.19.0"
44
+ },
45
+ "dependencies": {
46
+ "@sammorrowdrums/mcpi-ai": "^0.85.0-bootstrap.0",
47
+ "@sammorrowdrums/mcpi-agent-core": "^0.85.0-bootstrap.0"
48
+ },
49
+ "devDependencies": {
50
+ "@vitest/coverage-v8": "4.1.9",
51
+ "vitest": "4.1.9"
52
+ }
53
+ }