@juli-os/ledger 0.1.6 → 0.2.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/README.md +1 -1
- package/dist/contracts/entities.d.ts +1 -0
- package/dist/index.js +84 -37
- package/dist/juli-ledger.js +84 -37
- package/dist/platform/ledger/store.d.ts +4 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The L1 layer of the juli open-source ladder: **the append-only ledger of record
|
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| Workflow lifecycle | `createLedgerStore(path)` → `createWorkflow / getWorkflow / listWorkflows / setWorkflowStatus / createStep / insertStepsAt …` | Durable jobs + steps + statuses in SQLite, with dynamic mid-flight step insertion (single transaction, tail renumbered) |
|
|
10
10
|
| Conversation threading | `latestByMessageId(msgId)` / `recentBySender(from, since)` | Reply-chain matching for email bots and support inboxes — message-id causal anchor + sender/time-window candidate narrowing |
|
|
11
|
-
| Case files | `emptyCaseFile() / loadCaseFile / saveCaseFile` | A per-job audit dossier: original → extract → executions → draft → decisions → artifacts |
|
|
11
|
+
| Case files | `emptyCaseFile(now) / loadCaseFile / saveCaseFile` | A per-job audit dossier: original → extract → executions → draft → decisions → artifacts |
|
|
12
12
|
| Usage & cost | `createUsageStore(path)` → `record / stats / timeline / byLocalDay`, `priceUsage()` | Token accounting per model / day / session with built-in pricing |
|
|
13
13
|
|
|
14
14
|
## Peel boundary
|
package/dist/index.js
CHANGED
|
@@ -110,7 +110,7 @@ var parseCaseFile = (raw, now) => {
|
|
|
110
110
|
decisions: pick(arr("decisions"), (d) => "action" in d),
|
|
111
111
|
references: arr("references").filter((x) => typeof x === "string"),
|
|
112
112
|
artifacts: pick(arr("artifacts"), (a) => "id" in a),
|
|
113
|
-
updatedAt: str2("updated_at") || base.updatedAt
|
|
113
|
+
updatedAt: str2("updatedAt") || str2("updated_at") || base.updatedAt
|
|
114
114
|
};
|
|
115
115
|
};
|
|
116
116
|
var serializeCaseFile = (cf) => JSON.stringify(cf);
|
|
@@ -240,7 +240,8 @@ var rowToWorkflow = (r) => ({
|
|
|
240
240
|
session: str(r["session"]),
|
|
241
241
|
project: str(r["project"]),
|
|
242
242
|
path: str(r["path"]),
|
|
243
|
-
threadId: r["thread_id"] === null
|
|
243
|
+
threadId: r["thread_id"] === null ? null : str(r["thread_id"]),
|
|
244
|
+
tenant: str(r["tenant"]) || "internal",
|
|
244
245
|
meta: asRecord(safeParse(str(r["meta"]))),
|
|
245
246
|
createdAt: str(r["created_at"]),
|
|
246
247
|
updatedAt: str(r["updated_at"])
|
|
@@ -280,6 +281,8 @@ var pruneImpl = (db, retentionDays, nowIso) => {
|
|
|
280
281
|
try {
|
|
281
282
|
db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
|
|
282
283
|
db.prepare("DELETE FROM relays WHERE workflow_id=?").run(r.id);
|
|
284
|
+
db.prepare("DELETE FROM prompts WHERE run_id IN (SELECT id FROM runs WHERE workflow_id=?)").run(r.id);
|
|
285
|
+
db.prepare("DELETE FROM runs WHERE workflow_id=?").run(r.id);
|
|
283
286
|
db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
|
|
284
287
|
db.exec("COMMIT");
|
|
285
288
|
} catch (e) {
|
|
@@ -305,7 +308,11 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
305
308
|
if (!wfCols.includes("thread_id")) {
|
|
306
309
|
db.exec("ALTER TABLE workflows ADD COLUMN thread_id TEXT");
|
|
307
310
|
}
|
|
311
|
+
if (!wfCols.includes("tenant")) {
|
|
312
|
+
db.exec("ALTER TABLE workflows ADD COLUMN tenant TEXT NOT NULL DEFAULT 'internal'");
|
|
313
|
+
}
|
|
308
314
|
db.exec("CREATE INDEX IF NOT EXISTS idx_workflows_thread ON workflows(thread_id)");
|
|
315
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_workflows_tenant ON workflows(tenant, created_at)");
|
|
309
316
|
const stepCols = db.prepare("PRAGMA table_info(workflow_steps)").all().map((c) => c.name);
|
|
310
317
|
if (!stepCols.includes("started_at")) db.exec("ALTER TABLE workflow_steps ADD COLUMN started_at TEXT");
|
|
311
318
|
if (!stepCols.includes("completed_at")) db.exec("ALTER TABLE workflow_steps ADD COLUMN completed_at TEXT");
|
|
@@ -327,7 +334,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
327
334
|
const activeRunOf = (session) => {
|
|
328
335
|
const r = db.prepare(
|
|
329
336
|
`SELECT r.id AS id, r.workflow_id AS workflow_id, r.session AS session,
|
|
330
|
-
r.
|
|
337
|
+
r.created_at AS created_at FROM runs r
|
|
331
338
|
JOIN workflows w ON w.id = r.workflow_id
|
|
332
339
|
WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
|
|
333
340
|
ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
|
|
@@ -347,19 +354,36 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
347
354
|
const id = ids.newId("wf");
|
|
348
355
|
const ts = now();
|
|
349
356
|
db.prepare(
|
|
350
|
-
`INSERT INTO workflows (id, parent_id, kind, title, status, owner, meta, created_at, updated_at)
|
|
351
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
352
|
-
).run(id, w.parentId ?? null, w.kind, w.title, w.status ?? "running", w.owner ?? null, JSON.stringify(w.meta ?? {}), ts, ts);
|
|
357
|
+
`INSERT INTO workflows (id, parent_id, kind, title, status, owner, tenant, meta, created_at, updated_at)
|
|
358
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
359
|
+
).run(id, w.parentId ?? null, w.kind, w.title, w.status ?? "running", w.owner ?? null, w.tenant || "internal", JSON.stringify(w.meta ?? {}), ts, ts);
|
|
353
360
|
return getWorkflowById(id);
|
|
354
361
|
},
|
|
355
362
|
getWorkflow(id) {
|
|
356
363
|
return getWorkflowById(id);
|
|
357
364
|
},
|
|
358
|
-
listWorkflows(limit = 100, status) {
|
|
365
|
+
listWorkflows(limit = 100, status, tenant) {
|
|
366
|
+
const where = [];
|
|
367
|
+
const params = [];
|
|
359
368
|
if (status !== void 0 && status !== "") {
|
|
360
|
-
|
|
369
|
+
where.push("status=?");
|
|
370
|
+
params.push(status);
|
|
371
|
+
}
|
|
372
|
+
if (tenant !== void 0 && tenant !== "") {
|
|
373
|
+
where.push("tenant=?");
|
|
374
|
+
params.push(tenant);
|
|
361
375
|
}
|
|
362
|
-
|
|
376
|
+
const sql = where.length > 0 ? `SELECT * FROM workflows WHERE ${where.join(" AND ")} ORDER BY created_at DESC, rowid DESC LIMIT ?` : "SELECT * FROM workflows ORDER BY created_at DESC, rowid DESC LIMIT ?";
|
|
377
|
+
return db.prepare(sql).all(...params, limit).map(rowToWorkflow);
|
|
378
|
+
},
|
|
379
|
+
setWorkflowTenant(id, tenant) {
|
|
380
|
+
const next = tenant === "" ? "internal" : tenant;
|
|
381
|
+
const cur = getWorkflowById(id);
|
|
382
|
+
if (cur === null || cur.tenant === next) return;
|
|
383
|
+
db.prepare("UPDATE workflows SET tenant=?, updated_at=? WHERE id=?").run(next, now(), id);
|
|
384
|
+
db.prepare(
|
|
385
|
+
"INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('workflow', ?, 'tenant_changed', ?, ?)"
|
|
386
|
+
).run(id, JSON.stringify({ from: cur.tenant, to: next }), now());
|
|
363
387
|
},
|
|
364
388
|
setWorkflowSession(id, session) {
|
|
365
389
|
db.prepare("UPDATE workflows SET session=?, updated_at=? WHERE id=?").run(session, now(), id);
|
|
@@ -521,7 +545,18 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
521
545
|
).run(now(), now(), id);
|
|
522
546
|
return Number(res.changes) === 1;
|
|
523
547
|
},
|
|
524
|
-
listSteps(status, limit = 100) {
|
|
548
|
+
listSteps(status, limit = 100, tenant) {
|
|
549
|
+
if (tenant !== void 0 && tenant !== "") {
|
|
550
|
+
return db.prepare(
|
|
551
|
+
`SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
|
|
552
|
+
st.title AS title, st.status AS status,
|
|
553
|
+
COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
|
|
554
|
+
COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at,
|
|
555
|
+
st.started_at AS started_at, st.completed_at AS completed_at
|
|
556
|
+
FROM workflow_steps st JOIN workflows w ON w.id = st.workflow_id
|
|
557
|
+
WHERE st.status=? AND w.tenant=? ORDER BY st.updated_at DESC LIMIT ?`
|
|
558
|
+
).all(status, tenant, limit).map(rowToStep);
|
|
559
|
+
}
|
|
525
560
|
return db.prepare(
|
|
526
561
|
`SELECT ${STEP_COLS} FROM workflow_steps WHERE status=? ORDER BY updated_at DESC LIMIT ?`
|
|
527
562
|
).all(status, limit).map(rowToStep);
|
|
@@ -551,8 +586,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
551
586
|
sessionHasOpenWork(session, excludeWorkflowId) {
|
|
552
587
|
const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
|
|
553
588
|
const openFilter = `status IN (${sqlInList(ACTIVE_WORKFLOW_STATUSES)})`;
|
|
554
|
-
const
|
|
555
|
-
const
|
|
589
|
+
const exclude = excludeWorkflowId !== void 0 && excludeWorkflowId !== "";
|
|
590
|
+
const sql = `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor}${exclude ? " AND id != ?2" : ""}`;
|
|
591
|
+
const row = exclude ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
|
|
556
592
|
return Number(row["n"]) > 0;
|
|
557
593
|
},
|
|
558
594
|
appendEvent(entityType, entityId, type, payload = {}) {
|
|
@@ -593,14 +629,15 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
593
629
|
},
|
|
594
630
|
createRun(workflowId, session) {
|
|
595
631
|
const id = ids.newId("run");
|
|
632
|
+
const ts = now();
|
|
596
633
|
db.prepare(`INSERT INTO runs (id, workflow_id, session, status, created_at)
|
|
597
|
-
VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session,
|
|
634
|
+
VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, ts);
|
|
598
635
|
return {
|
|
599
636
|
id,
|
|
600
637
|
workflowId,
|
|
601
638
|
session,
|
|
602
639
|
status: "running",
|
|
603
|
-
createdAt:
|
|
640
|
+
createdAt: ts
|
|
604
641
|
};
|
|
605
642
|
},
|
|
606
643
|
activeRunForSession: (session) => activeRunOf(session),
|
|
@@ -623,17 +660,20 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
623
660
|
).get(dedupPattern(key));
|
|
624
661
|
return r !== void 0;
|
|
625
662
|
},
|
|
626
|
-
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
* LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
|
|
630
|
-
* 成对转义(message-id 几乎必含 `_`)。 */
|
|
663
|
+
/** 全量出站邮件登记(2026-09-20 幽灵单缺口):send 步之外,通知邮件
|
|
664
|
+
* (emailNotifier)等所有出站信的 message_id 也落 email_sent 事件——
|
|
665
|
+
* 发给轮询邮箱自身的告警信不再被当新来信建单([juli] 待审批 幽灵单)。 */
|
|
631
666
|
recordSentMessageId(messageId, meta) {
|
|
632
667
|
if (messageId === "") return;
|
|
633
668
|
db.prepare(
|
|
634
669
|
`INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('email', ?, 'email_sent', ?, ?)`
|
|
635
670
|
).run(messageId, JSON.stringify(meta ?? {}), now());
|
|
636
671
|
},
|
|
672
|
+
/** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
|
|
673
|
+
* 发送记录——completed send 步的 output.message_id(email_sent 事件同源
|
|
674
|
+
* 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
|
|
675
|
+
* LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
|
|
676
|
+
* 成对转义(message-id 几乎必含 `_`)。 */
|
|
637
677
|
sentMessageIdExists(messageId) {
|
|
638
678
|
if (messageId === "") return false;
|
|
639
679
|
const registered = db.prepare(
|
|
@@ -652,9 +692,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
652
692
|
},
|
|
653
693
|
createPrompt(runId, content, source) {
|
|
654
694
|
const id = ids.newId("pr");
|
|
695
|
+
const ts = now();
|
|
655
696
|
db.prepare(`INSERT INTO prompts (id, run_id, content, source, status, created_at)
|
|
656
|
-
VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source,
|
|
657
|
-
return { id, runId, content, status: "submitted", createdAt:
|
|
697
|
+
VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, ts);
|
|
698
|
+
return { id, runId, content, status: "submitted", createdAt: ts };
|
|
658
699
|
},
|
|
659
700
|
prune(retentionDays) {
|
|
660
701
|
return pruneImpl(db, retentionDays, now());
|
|
@@ -698,9 +739,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
698
739
|
if (arr === void 0) members.set(root, [w.id]);
|
|
699
740
|
else arr.push(w.id);
|
|
700
741
|
}
|
|
701
|
-
const views = [...members.entries()].map(([taskId,
|
|
742
|
+
const views = [...members.entries()].map(([taskId, memberIds]) => {
|
|
702
743
|
const card = byId.get(taskId);
|
|
703
|
-
return { taskId, title: card.title, status: card.status, workflowIds:
|
|
744
|
+
return { taskId, title: card.title, status: card.status, workflowIds: memberIds };
|
|
704
745
|
}).sort((a, b) => {
|
|
705
746
|
const ca = byId.get(a.taskId)?.createdAt ?? "";
|
|
706
747
|
const cb = byId.get(b.taskId)?.createdAt ?? "";
|
|
@@ -1043,9 +1084,9 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1043
1084
|
outputTokens: n(r["o"])
|
|
1044
1085
|
}));
|
|
1045
1086
|
return {
|
|
1046
|
-
totalCalls: n(total
|
|
1047
|
-
totalInputTokens: n(total
|
|
1048
|
-
totalOutputTokens: n(total
|
|
1087
|
+
totalCalls: n(total["c"]),
|
|
1088
|
+
totalInputTokens: n(total["i"]),
|
|
1089
|
+
totalOutputTokens: n(total["o"]),
|
|
1049
1090
|
byModel
|
|
1050
1091
|
};
|
|
1051
1092
|
},
|
|
@@ -1088,7 +1129,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1088
1129
|
return windowUsageRows(db, tmuxSession, windows);
|
|
1089
1130
|
},
|
|
1090
1131
|
sessionWindowsPriced(sessions, windows) {
|
|
1091
|
-
if (sessions.length === 0 || windows.length === 0) return priceUsage([]);
|
|
1092
1132
|
return priceUsage(sessions.flatMap((s) => windowUsageRows(db, s, windows)));
|
|
1093
1133
|
},
|
|
1094
1134
|
byLocalDay(days = 7) {
|
|
@@ -1122,10 +1162,17 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1122
1162
|
return mapped > 0 && total > 0 && filled * 2 < total;
|
|
1123
1163
|
},
|
|
1124
1164
|
resetClaudeIngest() {
|
|
1125
|
-
db.exec("
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1165
|
+
db.exec("BEGIN");
|
|
1166
|
+
try {
|
|
1167
|
+
db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
|
|
1168
|
+
db.exec("DELETE FROM claude_ingest_offset");
|
|
1169
|
+
db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
|
|
1170
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(now().toISOString());
|
|
1171
|
+
db.exec("COMMIT");
|
|
1172
|
+
} catch (e) {
|
|
1173
|
+
db.exec("ROLLBACK");
|
|
1174
|
+
throw e;
|
|
1175
|
+
}
|
|
1129
1176
|
},
|
|
1130
1177
|
recent(limit = 100) {
|
|
1131
1178
|
return db.prepare("SELECT * FROM prompt_usage ORDER BY id DESC LIMIT ?").all(limit).map((r) => ({
|
|
@@ -1171,7 +1218,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1171
1218
|
}
|
|
1172
1219
|
for (const f of files) {
|
|
1173
1220
|
const stem = f.replace(/\.jsonl$/, "");
|
|
1174
|
-
count += ingestFile(db, stem, join(dir, f));
|
|
1221
|
+
count += ingestFile(db, stem, join(dir, f), now);
|
|
1175
1222
|
}
|
|
1176
1223
|
}
|
|
1177
1224
|
} catch {
|
|
@@ -1185,7 +1232,7 @@ var tmuxOf = (db, claudeId) => {
|
|
|
1185
1232
|
const r = db.prepare("SELECT tmux_session FROM claude_sessions WHERE claude_session_id=?").get(claudeId);
|
|
1186
1233
|
return r?.["tmux_session"] ?? "";
|
|
1187
1234
|
};
|
|
1188
|
-
var ingestFile = (db, claudeId, transcriptPath) => {
|
|
1235
|
+
var ingestFile = (db, claudeId, transcriptPath, now) => {
|
|
1189
1236
|
let fd;
|
|
1190
1237
|
try {
|
|
1191
1238
|
fd = openSync(transcriptPath, "r");
|
|
@@ -1215,7 +1262,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
|
|
|
1215
1262
|
const sess = tmuxOf(db, claudeId);
|
|
1216
1263
|
for (const line of complete.split("\n")) {
|
|
1217
1264
|
if (line === "") continue;
|
|
1218
|
-
const rec = parseAssistantLine(line);
|
|
1265
|
+
const rec = parseAssistantLine(line, now);
|
|
1219
1266
|
if (!rec) continue;
|
|
1220
1267
|
db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
|
|
1221
1268
|
VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
|
|
@@ -1231,7 +1278,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
|
|
|
1231
1278
|
}
|
|
1232
1279
|
db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
|
|
1233
1280
|
VALUES (?, ?, ?)
|
|
1234
|
-
ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, (
|
|
1281
|
+
ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, now().toISOString());
|
|
1235
1282
|
db.exec("COMMIT");
|
|
1236
1283
|
} catch (e) {
|
|
1237
1284
|
db.exec("ROLLBACK");
|
|
@@ -1242,7 +1289,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
|
|
|
1242
1289
|
closeSync(fd);
|
|
1243
1290
|
}
|
|
1244
1291
|
};
|
|
1245
|
-
var parseAssistantLine = (line) => {
|
|
1292
|
+
var parseAssistantLine = (line, now) => {
|
|
1246
1293
|
try {
|
|
1247
1294
|
const v = JSON.parse(line);
|
|
1248
1295
|
if (v.type !== "assistant") return null;
|
|
@@ -1257,7 +1304,7 @@ var parseAssistantLine = (line) => {
|
|
|
1257
1304
|
inputTokens: input,
|
|
1258
1305
|
outputTokens: output,
|
|
1259
1306
|
cacheRead,
|
|
1260
|
-
timestamp: typeof v.timestamp === "string" ? v.timestamp : (
|
|
1307
|
+
timestamp: typeof v.timestamp === "string" ? v.timestamp : now().toISOString(),
|
|
1261
1308
|
callContext: v.uuid ?? ""
|
|
1262
1309
|
};
|
|
1263
1310
|
} catch {
|
package/dist/juli-ledger.js
CHANGED
|
@@ -105,7 +105,7 @@ var parseCaseFile = (raw, now) => {
|
|
|
105
105
|
decisions: pick(arr("decisions"), (d) => "action" in d),
|
|
106
106
|
references: arr("references").filter((x) => typeof x === "string"),
|
|
107
107
|
artifacts: pick(arr("artifacts"), (a) => "id" in a),
|
|
108
|
-
updatedAt: str2("updated_at") || base.updatedAt
|
|
108
|
+
updatedAt: str2("updatedAt") || str2("updated_at") || base.updatedAt
|
|
109
109
|
};
|
|
110
110
|
};
|
|
111
111
|
var serializeCaseFile = (cf) => JSON.stringify(cf);
|
|
@@ -198,7 +198,8 @@ var rowToWorkflow = (r) => ({
|
|
|
198
198
|
session: str(r["session"]),
|
|
199
199
|
project: str(r["project"]),
|
|
200
200
|
path: str(r["path"]),
|
|
201
|
-
threadId: r["thread_id"] === null
|
|
201
|
+
threadId: r["thread_id"] === null ? null : str(r["thread_id"]),
|
|
202
|
+
tenant: str(r["tenant"]) || "internal",
|
|
202
203
|
meta: asRecord(safeParse(str(r["meta"]))),
|
|
203
204
|
createdAt: str(r["created_at"]),
|
|
204
205
|
updatedAt: str(r["updated_at"])
|
|
@@ -238,6 +239,8 @@ var pruneImpl = (db, retentionDays, nowIso) => {
|
|
|
238
239
|
try {
|
|
239
240
|
db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
|
|
240
241
|
db.prepare("DELETE FROM relays WHERE workflow_id=?").run(r.id);
|
|
242
|
+
db.prepare("DELETE FROM prompts WHERE run_id IN (SELECT id FROM runs WHERE workflow_id=?)").run(r.id);
|
|
243
|
+
db.prepare("DELETE FROM runs WHERE workflow_id=?").run(r.id);
|
|
241
244
|
db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
|
|
242
245
|
db.exec("COMMIT");
|
|
243
246
|
} catch (e) {
|
|
@@ -263,7 +266,11 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
263
266
|
if (!wfCols.includes("thread_id")) {
|
|
264
267
|
db.exec("ALTER TABLE workflows ADD COLUMN thread_id TEXT");
|
|
265
268
|
}
|
|
269
|
+
if (!wfCols.includes("tenant")) {
|
|
270
|
+
db.exec("ALTER TABLE workflows ADD COLUMN tenant TEXT NOT NULL DEFAULT 'internal'");
|
|
271
|
+
}
|
|
266
272
|
db.exec("CREATE INDEX IF NOT EXISTS idx_workflows_thread ON workflows(thread_id)");
|
|
273
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_workflows_tenant ON workflows(tenant, created_at)");
|
|
267
274
|
const stepCols = db.prepare("PRAGMA table_info(workflow_steps)").all().map((c) => c.name);
|
|
268
275
|
if (!stepCols.includes("started_at")) db.exec("ALTER TABLE workflow_steps ADD COLUMN started_at TEXT");
|
|
269
276
|
if (!stepCols.includes("completed_at")) db.exec("ALTER TABLE workflow_steps ADD COLUMN completed_at TEXT");
|
|
@@ -285,7 +292,7 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
285
292
|
const activeRunOf = (session) => {
|
|
286
293
|
const r = db.prepare(
|
|
287
294
|
`SELECT r.id AS id, r.workflow_id AS workflow_id, r.session AS session,
|
|
288
|
-
r.
|
|
295
|
+
r.created_at AS created_at FROM runs r
|
|
289
296
|
JOIN workflows w ON w.id = r.workflow_id
|
|
290
297
|
WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
|
|
291
298
|
ORDER BY r.created_at DESC, r.rowid DESC LIMIT 1`
|
|
@@ -305,19 +312,36 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
305
312
|
const id = ids.newId("wf");
|
|
306
313
|
const ts = now();
|
|
307
314
|
db.prepare(
|
|
308
|
-
`INSERT INTO workflows (id, parent_id, kind, title, status, owner, meta, created_at, updated_at)
|
|
309
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
310
|
-
).run(id, w.parentId ?? null, w.kind, w.title, w.status ?? "running", w.owner ?? null, JSON.stringify(w.meta ?? {}), ts, ts);
|
|
315
|
+
`INSERT INTO workflows (id, parent_id, kind, title, status, owner, tenant, meta, created_at, updated_at)
|
|
316
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
317
|
+
).run(id, w.parentId ?? null, w.kind, w.title, w.status ?? "running", w.owner ?? null, w.tenant || "internal", JSON.stringify(w.meta ?? {}), ts, ts);
|
|
311
318
|
return getWorkflowById(id);
|
|
312
319
|
},
|
|
313
320
|
getWorkflow(id) {
|
|
314
321
|
return getWorkflowById(id);
|
|
315
322
|
},
|
|
316
|
-
listWorkflows(limit = 100, status) {
|
|
323
|
+
listWorkflows(limit = 100, status, tenant) {
|
|
324
|
+
const where = [];
|
|
325
|
+
const params = [];
|
|
317
326
|
if (status !== void 0 && status !== "") {
|
|
318
|
-
|
|
327
|
+
where.push("status=?");
|
|
328
|
+
params.push(status);
|
|
329
|
+
}
|
|
330
|
+
if (tenant !== void 0 && tenant !== "") {
|
|
331
|
+
where.push("tenant=?");
|
|
332
|
+
params.push(tenant);
|
|
319
333
|
}
|
|
320
|
-
|
|
334
|
+
const sql = where.length > 0 ? `SELECT * FROM workflows WHERE ${where.join(" AND ")} ORDER BY created_at DESC, rowid DESC LIMIT ?` : "SELECT * FROM workflows ORDER BY created_at DESC, rowid DESC LIMIT ?";
|
|
335
|
+
return db.prepare(sql).all(...params, limit).map(rowToWorkflow);
|
|
336
|
+
},
|
|
337
|
+
setWorkflowTenant(id, tenant) {
|
|
338
|
+
const next = tenant === "" ? "internal" : tenant;
|
|
339
|
+
const cur = getWorkflowById(id);
|
|
340
|
+
if (cur === null || cur.tenant === next) return;
|
|
341
|
+
db.prepare("UPDATE workflows SET tenant=?, updated_at=? WHERE id=?").run(next, now(), id);
|
|
342
|
+
db.prepare(
|
|
343
|
+
"INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('workflow', ?, 'tenant_changed', ?, ?)"
|
|
344
|
+
).run(id, JSON.stringify({ from: cur.tenant, to: next }), now());
|
|
321
345
|
},
|
|
322
346
|
setWorkflowSession(id, session) {
|
|
323
347
|
db.prepare("UPDATE workflows SET session=?, updated_at=? WHERE id=?").run(session, now(), id);
|
|
@@ -479,7 +503,18 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
479
503
|
).run(now(), now(), id);
|
|
480
504
|
return Number(res.changes) === 1;
|
|
481
505
|
},
|
|
482
|
-
listSteps(status, limit = 100) {
|
|
506
|
+
listSteps(status, limit = 100, tenant) {
|
|
507
|
+
if (tenant !== void 0 && tenant !== "") {
|
|
508
|
+
return db.prepare(
|
|
509
|
+
`SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
|
|
510
|
+
st.title AS title, st.status AS status,
|
|
511
|
+
COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
|
|
512
|
+
COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at,
|
|
513
|
+
st.started_at AS started_at, st.completed_at AS completed_at
|
|
514
|
+
FROM workflow_steps st JOIN workflows w ON w.id = st.workflow_id
|
|
515
|
+
WHERE st.status=? AND w.tenant=? ORDER BY st.updated_at DESC LIMIT ?`
|
|
516
|
+
).all(status, tenant, limit).map(rowToStep);
|
|
517
|
+
}
|
|
483
518
|
return db.prepare(
|
|
484
519
|
`SELECT ${STEP_COLS} FROM workflow_steps WHERE status=? ORDER BY updated_at DESC LIMIT ?`
|
|
485
520
|
).all(status, limit).map(rowToStep);
|
|
@@ -509,8 +544,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
509
544
|
sessionHasOpenWork(session, excludeWorkflowId) {
|
|
510
545
|
const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
|
|
511
546
|
const openFilter = `status IN (${sqlInList(ACTIVE_WORKFLOW_STATUSES)})`;
|
|
512
|
-
const
|
|
513
|
-
const
|
|
547
|
+
const exclude = excludeWorkflowId !== void 0 && excludeWorkflowId !== "";
|
|
548
|
+
const sql = `SELECT COUNT(*) AS n FROM workflows WHERE ${openFilter} AND ${anchor}${exclude ? " AND id != ?2" : ""}`;
|
|
549
|
+
const row = exclude ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
|
|
514
550
|
return Number(row["n"]) > 0;
|
|
515
551
|
},
|
|
516
552
|
appendEvent(entityType, entityId, type, payload = {}) {
|
|
@@ -551,14 +587,15 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
551
587
|
},
|
|
552
588
|
createRun(workflowId, session) {
|
|
553
589
|
const id = ids.newId("run");
|
|
590
|
+
const ts = now();
|
|
554
591
|
db.prepare(`INSERT INTO runs (id, workflow_id, session, status, created_at)
|
|
555
|
-
VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session,
|
|
592
|
+
VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, ts);
|
|
556
593
|
return {
|
|
557
594
|
id,
|
|
558
595
|
workflowId,
|
|
559
596
|
session,
|
|
560
597
|
status: "running",
|
|
561
|
-
createdAt:
|
|
598
|
+
createdAt: ts
|
|
562
599
|
};
|
|
563
600
|
},
|
|
564
601
|
activeRunForSession: (session) => activeRunOf(session),
|
|
@@ -581,17 +618,20 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
581
618
|
).get(dedupPattern(key));
|
|
582
619
|
return r !== void 0;
|
|
583
620
|
},
|
|
584
|
-
/**
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
* LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
|
|
588
|
-
* 成对转义(message-id 几乎必含 `_`)。 */
|
|
621
|
+
/** 全量出站邮件登记(2026-09-20 幽灵单缺口):send 步之外,通知邮件
|
|
622
|
+
* (emailNotifier)等所有出站信的 message_id 也落 email_sent 事件——
|
|
623
|
+
* 发给轮询邮箱自身的告警信不再被当新来信建单([juli] 待审批 幽灵单)。 */
|
|
589
624
|
recordSentMessageId(messageId, meta) {
|
|
590
625
|
if (messageId === "") return;
|
|
591
626
|
db.prepare(
|
|
592
627
|
`INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('email', ?, 'email_sent', ?, ?)`
|
|
593
628
|
).run(messageId, JSON.stringify(meta ?? {}), now());
|
|
594
629
|
},
|
|
630
|
+
/** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
|
|
631
|
+
* 发送记录——completed send 步的 output.message_id(email_sent 事件同源
|
|
632
|
+
* 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
|
|
633
|
+
* LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 jsonFieldPattern
|
|
634
|
+
* 成对转义(message-id 几乎必含 `_`)。 */
|
|
595
635
|
sentMessageIdExists(messageId) {
|
|
596
636
|
if (messageId === "") return false;
|
|
597
637
|
const registered = db.prepare(
|
|
@@ -610,9 +650,10 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
610
650
|
},
|
|
611
651
|
createPrompt(runId, content, source) {
|
|
612
652
|
const id = ids.newId("pr");
|
|
653
|
+
const ts = now();
|
|
613
654
|
db.prepare(`INSERT INTO prompts (id, run_id, content, source, status, created_at)
|
|
614
|
-
VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source,
|
|
615
|
-
return { id, runId, content, status: "submitted", createdAt:
|
|
655
|
+
VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, ts);
|
|
656
|
+
return { id, runId, content, status: "submitted", createdAt: ts };
|
|
616
657
|
},
|
|
617
658
|
prune(retentionDays) {
|
|
618
659
|
return pruneImpl(db, retentionDays, now());
|
|
@@ -656,9 +697,9 @@ var createLedgerStore = (dbPath, deps = {}) => {
|
|
|
656
697
|
if (arr === void 0) members.set(root, [w.id]);
|
|
657
698
|
else arr.push(w.id);
|
|
658
699
|
}
|
|
659
|
-
const views = [...members.entries()].map(([taskId,
|
|
700
|
+
const views = [...members.entries()].map(([taskId, memberIds]) => {
|
|
660
701
|
const card = byId.get(taskId);
|
|
661
|
-
return { taskId, title: card.title, status: card.status, workflowIds:
|
|
702
|
+
return { taskId, title: card.title, status: card.status, workflowIds: memberIds };
|
|
662
703
|
}).sort((a, b) => {
|
|
663
704
|
const ca = byId.get(a.taskId)?.createdAt ?? "";
|
|
664
705
|
const cb = byId.get(b.taskId)?.createdAt ?? "";
|
|
@@ -1001,9 +1042,9 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1001
1042
|
outputTokens: n(r["o"])
|
|
1002
1043
|
}));
|
|
1003
1044
|
return {
|
|
1004
|
-
totalCalls: n(total
|
|
1005
|
-
totalInputTokens: n(total
|
|
1006
|
-
totalOutputTokens: n(total
|
|
1045
|
+
totalCalls: n(total["c"]),
|
|
1046
|
+
totalInputTokens: n(total["i"]),
|
|
1047
|
+
totalOutputTokens: n(total["o"]),
|
|
1007
1048
|
byModel
|
|
1008
1049
|
};
|
|
1009
1050
|
},
|
|
@@ -1046,7 +1087,6 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1046
1087
|
return windowUsageRows(db, tmuxSession, windows);
|
|
1047
1088
|
},
|
|
1048
1089
|
sessionWindowsPriced(sessions, windows) {
|
|
1049
|
-
if (sessions.length === 0 || windows.length === 0) return priceUsage([]);
|
|
1050
1090
|
return priceUsage(sessions.flatMap((s) => windowUsageRows(db, s, windows)));
|
|
1051
1091
|
},
|
|
1052
1092
|
byLocalDay(days = 7) {
|
|
@@ -1080,10 +1120,17 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1080
1120
|
return mapped > 0 && total > 0 && filled * 2 < total;
|
|
1081
1121
|
},
|
|
1082
1122
|
resetClaudeIngest() {
|
|
1083
|
-
db.exec("
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1123
|
+
db.exec("BEGIN");
|
|
1124
|
+
try {
|
|
1125
|
+
db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
|
|
1126
|
+
db.exec("DELETE FROM claude_ingest_offset");
|
|
1127
|
+
db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
|
|
1128
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(now().toISOString());
|
|
1129
|
+
db.exec("COMMIT");
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
db.exec("ROLLBACK");
|
|
1132
|
+
throw e;
|
|
1133
|
+
}
|
|
1087
1134
|
},
|
|
1088
1135
|
recent(limit = 100) {
|
|
1089
1136
|
return db.prepare("SELECT * FROM prompt_usage ORDER BY id DESC LIMIT ?").all(limit).map((r) => ({
|
|
@@ -1129,7 +1176,7 @@ var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
|
1129
1176
|
}
|
|
1130
1177
|
for (const f of files) {
|
|
1131
1178
|
const stem = f.replace(/\.jsonl$/, "");
|
|
1132
|
-
count += ingestFile(db, stem, join(dir2, f));
|
|
1179
|
+
count += ingestFile(db, stem, join(dir2, f), now);
|
|
1133
1180
|
}
|
|
1134
1181
|
}
|
|
1135
1182
|
} catch {
|
|
@@ -1143,7 +1190,7 @@ var tmuxOf = (db, claudeId) => {
|
|
|
1143
1190
|
const r = db.prepare("SELECT tmux_session FROM claude_sessions WHERE claude_session_id=?").get(claudeId);
|
|
1144
1191
|
return r?.["tmux_session"] ?? "";
|
|
1145
1192
|
};
|
|
1146
|
-
var ingestFile = (db, claudeId, transcriptPath) => {
|
|
1193
|
+
var ingestFile = (db, claudeId, transcriptPath, now) => {
|
|
1147
1194
|
let fd;
|
|
1148
1195
|
try {
|
|
1149
1196
|
fd = openSync(transcriptPath, "r");
|
|
@@ -1173,7 +1220,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
|
|
|
1173
1220
|
const sess = tmuxOf(db, claudeId);
|
|
1174
1221
|
for (const line of complete.split("\n")) {
|
|
1175
1222
|
if (line === "") continue;
|
|
1176
|
-
const rec = parseAssistantLine(line);
|
|
1223
|
+
const rec = parseAssistantLine(line, now);
|
|
1177
1224
|
if (!rec) continue;
|
|
1178
1225
|
db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
|
|
1179
1226
|
VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
|
|
@@ -1189,7 +1236,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
|
|
|
1189
1236
|
}
|
|
1190
1237
|
db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
|
|
1191
1238
|
VALUES (?, ?, ?)
|
|
1192
|
-
ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, (
|
|
1239
|
+
ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, now().toISOString());
|
|
1193
1240
|
db.exec("COMMIT");
|
|
1194
1241
|
} catch (e) {
|
|
1195
1242
|
db.exec("ROLLBACK");
|
|
@@ -1200,7 +1247,7 @@ var ingestFile = (db, claudeId, transcriptPath) => {
|
|
|
1200
1247
|
closeSync(fd);
|
|
1201
1248
|
}
|
|
1202
1249
|
};
|
|
1203
|
-
var parseAssistantLine = (line) => {
|
|
1250
|
+
var parseAssistantLine = (line, now) => {
|
|
1204
1251
|
try {
|
|
1205
1252
|
const v = JSON.parse(line);
|
|
1206
1253
|
if (v.type !== "assistant") return null;
|
|
@@ -1215,7 +1262,7 @@ var parseAssistantLine = (line) => {
|
|
|
1215
1262
|
inputTokens: input,
|
|
1216
1263
|
outputTokens: output,
|
|
1217
1264
|
cacheRead,
|
|
1218
|
-
timestamp: typeof v.timestamp === "string" ? v.timestamp : (
|
|
1265
|
+
timestamp: typeof v.timestamp === "string" ? v.timestamp : now().toISOString(),
|
|
1219
1266
|
callContext: v.uuid ?? ""
|
|
1220
1267
|
};
|
|
1221
1268
|
} catch {
|
|
@@ -12,11 +12,13 @@ export interface LifecycleStore {
|
|
|
12
12
|
parentId?: string;
|
|
13
13
|
status?: WorkflowStatus;
|
|
14
14
|
owner?: string;
|
|
15
|
+
tenant?: string;
|
|
15
16
|
}): Workflow;
|
|
16
17
|
getWorkflow(id: string): Workflow | null;
|
|
17
|
-
listWorkflows(limit?: number, status?: string): readonly Workflow[];
|
|
18
|
+
listWorkflows(limit?: number, status?: string, tenant?: string): readonly Workflow[];
|
|
18
19
|
setWorkflowStatus(id: string, status: WorkflowStatus): void;
|
|
19
20
|
setWorkflowSession(id: string, session: string): void;
|
|
21
|
+
setWorkflowTenant(id: string, tenant: string): void;
|
|
20
22
|
updateWorkflowMeta(id: string, patch: JsonRecord): JsonRecord;
|
|
21
23
|
setWorkflowRouting(id: string, session: string, project: string): void;
|
|
22
24
|
assignThread(workflowId: string, threadId: string): void;
|
|
@@ -64,7 +66,7 @@ export interface LifecycleStore {
|
|
|
64
66
|
setStepOutput(id: string, patch: JsonRecord): void;
|
|
65
67
|
appendStepOutputItem(id: string, key: string, entry: JsonValue, cap: number): void;
|
|
66
68
|
claimRunningStep(id: string): boolean;
|
|
67
|
-
listSteps(status: StepStatus, limit?: number): readonly Step[];
|
|
69
|
+
listSteps(status: StepStatus, limit?: number, tenant?: string): readonly Step[];
|
|
68
70
|
runningAgentStepForSession(session: string): Step | null;
|
|
69
71
|
sessionHasOpenWork(session: string, excludeWorkflowId?: string): boolean;
|
|
70
72
|
appendEvent(entityType: 'workflow' | 'step' | 'judgment' | 'agent', entityId: string, type: string, payload?: JsonRecord): LedgerEvent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juli-os/ledger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "juli-os L1 layer: the ledger of record for agent work — workflows, steps, case files, and token usage in one zero-dependency SQLite layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,11 +27,15 @@
|
|
|
27
27
|
"build": "tsc -p tsconfig.build.json && esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/index.js && esbuild bin/juli-ledger.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/juli-ledger.js && chmod +x dist/juli-ledger.js && python3 scripts/postbuild.py",
|
|
28
28
|
"test": "node --test \"test/*.test.ts\"",
|
|
29
29
|
"smoke:dist": "node --input-type=module -e \"import('./dist/index.js').then(async m => { const { join } = await import('node:path'); const { tmpdir } = await import('node:os'); const s = m.createLedgerStore(join(tmpdir(), 'peel-dist-' + Date.now() + '.db')); const wf = s.createWorkflow({kind:'case',title:'dist smoke'}); if (!s.getWorkflow(wf.id)) process.exit(1); console.log('DIST SMOKE OK'); })\"",
|
|
30
|
+
"docs:gen": "node scripts/gen-docs.mjs && typedoc",
|
|
31
|
+
"docs:build": "npm run docs:gen && mkdocs build --strict",
|
|
30
32
|
"prepublishOnly": "npm run build"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"@types/node": "^22.0.0",
|
|
34
36
|
"esbuild": "^0.24.0",
|
|
37
|
+
"typedoc": "^0.28.20",
|
|
38
|
+
"typedoc-plugin-markdown": "^4.14.1",
|
|
35
39
|
"typescript": "^5.6.0"
|
|
36
40
|
},
|
|
37
41
|
"publishConfig": {
|