@phnx-labs/agents-cli 1.22.28 → 1.22.30
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/CHANGELOG.md +88 -0
- package/README.md +39 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +30 -15
- package/dist/commands/sessions-browser.js +6 -6
- package/dist/commands/sessions-favorite.d.ts +7 -7
- package/dist/commands/sessions-favorite.js +30 -30
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions.d.ts +12 -1
- package/dist/commands/sessions.js +259 -20
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +16 -0
- package/dist/lib/menubar/snapshot.js +22 -1
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -0
- package/dist/lib/secrets/agent.js +32 -2
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/favorites.d.ts +2 -2
- package/dist/lib/session/favorites.js +2 -2
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +81 -154
- package/package.json +4 -1
|
@@ -142,6 +142,27 @@ export function shouldSelfHealForUpgrade(persistent, storeSize, runningVersion,
|
|
|
142
142
|
export function shouldTeardownVersionSkewedBroker(realHeldBundles) {
|
|
143
143
|
return realHeldBundles === 0;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Whether a version-skewed client may evict the reachable broker at all. The
|
|
147
|
+
* held-bundle gate above is necessary but not sufficient: a broker the always-on
|
|
148
|
+
* daemon is hosting must NEVER be client-evicted, even when it holds zero
|
|
149
|
+
* unlocks. teardownStaleBroker() recognizes only the standalone broker's
|
|
150
|
+
* pidPath() O_EXCL claim (the daemon writes ownerPath(), never pidPath()), so
|
|
151
|
+
* evicting a daemon-hosted broker unlinks its socket WITHOUT stopping the daemon;
|
|
152
|
+
* the daemon then keeps hostedBroker != null and shouldTakeOverBroker() refuses
|
|
153
|
+
* to re-host, orphaning its broker until the daemon restarts while every reader
|
|
154
|
+
* falls onto cold one-off brokers that re-prompt Touch ID — the storm. Deferring
|
|
155
|
+
* is safe: daemon code-version upgrades are handled by postinstall.js restarting
|
|
156
|
+
* it, and agentPing() already gated on PROTOCOL_VERSION, so a code-skewed daemon
|
|
157
|
+
* broker is still wire-compatible. Only when NO daemon owns the broker (churning
|
|
158
|
+
* dev installs with a dead/absent daemon — the case #435's client twin was built
|
|
159
|
+
* for) does the zero-held-bundles teardown apply, exactly as before.
|
|
160
|
+
*/
|
|
161
|
+
export function shouldClientEvictSkewedBroker(daemonRunning, realHeldBundles) {
|
|
162
|
+
if (daemonRunning)
|
|
163
|
+
return false;
|
|
164
|
+
return shouldTeardownVersionSkewedBroker(realHeldBundles);
|
|
165
|
+
}
|
|
145
166
|
function onDarwin() {
|
|
146
167
|
return process.platform === 'darwin';
|
|
147
168
|
}
|
|
@@ -175,6 +196,10 @@ function agentDir() {
|
|
|
175
196
|
function socketPath() {
|
|
176
197
|
return path.join(agentDir(), 'agent.sock');
|
|
177
198
|
}
|
|
199
|
+
/** Public accessor for the broker's socket path — `agents daemon status`/`services` reads it for display. */
|
|
200
|
+
export function secretsBrokerSocketPath() {
|
|
201
|
+
return socketPath();
|
|
202
|
+
}
|
|
178
203
|
function pidPath() {
|
|
179
204
|
return path.join(agentDir(), 'agent.pid');
|
|
180
205
|
}
|
|
@@ -345,7 +370,7 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
345
370
|
// the broker is running pre-upgrade code and should be restarted.
|
|
346
371
|
return { ok: true, cmd: 'ping', version: PROTOCOL_VERSION, cliVersion: getCliVersion() };
|
|
347
372
|
case 'get': {
|
|
348
|
-
// Walk own-harness → global so
|
|
373
|
+
// Walk own-harness → global so an `--agent` grant wins over a global one and
|
|
349
374
|
// an unscoped unlock serves every harness (bundleScopeChain).
|
|
350
375
|
for (const scope of bundleScopeChain(req.harness)) {
|
|
351
376
|
const key = scopedBundleKey(req.name, scope);
|
|
@@ -1334,7 +1359,12 @@ export async function ensureAgentRunning(timeoutMs = 5000) {
|
|
|
1334
1359
|
if (ping.reachable) {
|
|
1335
1360
|
if (ping.cliVersion === undefined || ping.cliVersion === getCliVersionFresh())
|
|
1336
1361
|
return true;
|
|
1337
|
-
|
|
1362
|
+
// A reachable but version-skewed broker: tear it down ONLY when no daemon
|
|
1363
|
+
// hosts it and it holds no unlocks. Evicting a daemon-hosted broker orphans
|
|
1364
|
+
// the daemon's socket and starts the Touch ID storm — see
|
|
1365
|
+
// shouldClientEvictSkewedBroker.
|
|
1366
|
+
const { isDaemonRunning } = await import('../daemon.js');
|
|
1367
|
+
if (!shouldClientEvictSkewedBroker(isDaemonRunning(), (await agentStatus()).length))
|
|
1338
1368
|
return true;
|
|
1339
1369
|
await teardownStaleBroker();
|
|
1340
1370
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (bundles.ts).
|
|
5
5
|
*
|
|
6
6
|
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
-
* the bundle is invisible. `agents secrets unlock --
|
|
7
|
+
* the bundle is invisible. `agents secrets unlock --agent <agent>` exists to NARROW
|
|
8
8
|
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
9
|
*
|
|
10
10
|
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* and throw at runtime even though tsc is happy.
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
16
|
-
* Scope of an unlock that was not narrowed with `--
|
|
16
|
+
* Scope of an unlock that was not narrowed with `--agent`: readable by every
|
|
17
17
|
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
18
|
*/
|
|
19
19
|
export declare const GLOBAL_HARNESS = "*";
|
|
20
20
|
/**
|
|
21
21
|
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
22
|
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
-
* `--
|
|
23
|
+
* `--agent claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
24
|
* everyone — not a fallback papering over a miss.
|
|
25
25
|
*/
|
|
26
26
|
export declare function bundleScopeChain(harness: string | undefined): string[];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (bundles.ts).
|
|
5
5
|
*
|
|
6
6
|
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
-
* the bundle is invisible. `agents secrets unlock --
|
|
7
|
+
* the bundle is invisible. `agents secrets unlock --agent <agent>` exists to NARROW
|
|
8
8
|
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
9
|
*
|
|
10
10
|
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* and throw at runtime even though tsc is happy.
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
16
|
-
* Scope of an unlock that was not narrowed with `--
|
|
16
|
+
* Scope of an unlock that was not narrowed with `--agent`: readable by every
|
|
17
17
|
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
18
|
*/
|
|
19
19
|
export const GLOBAL_HARNESS = '*';
|
|
20
20
|
/**
|
|
21
21
|
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
22
|
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
-
* `--
|
|
23
|
+
* `--agent claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
24
|
* everyone — not a fallback papering over a miss.
|
|
25
25
|
*/
|
|
26
26
|
export function bundleScopeChain(harness) {
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare const RESOURCE_INDEX_VERSION = 1;
|
|
|
28
28
|
*/
|
|
29
29
|
/** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
|
|
30
30
|
export declare const INSIGHTS_EXTRACTOR_VERSION = 6;
|
|
31
|
+
export declare const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
31
32
|
/** Raw row shape returned from the sessions table. */
|
|
32
33
|
export interface SessionRow {
|
|
33
34
|
id: string;
|
|
@@ -390,6 +391,20 @@ export declare function writeSessionInsights<T>(entries: Array<{
|
|
|
390
391
|
}>): void;
|
|
391
392
|
/** Drop every cached facet row. Backs `agents insights --refresh`. */
|
|
392
393
|
export declare function clearSessionInsights(): void;
|
|
394
|
+
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
395
|
+
export declare function readSessionPreviewCache<T>(id: string, sourceStamp: {
|
|
396
|
+
fileMtimeMs: number | null;
|
|
397
|
+
fileSize: number | null;
|
|
398
|
+
}): T | undefined;
|
|
399
|
+
/** Persist normalized preview data against the exact transcript bytes parsed. */
|
|
400
|
+
export declare function writeSessionPreviewCache<T>(entry: {
|
|
401
|
+
id: string;
|
|
402
|
+
fileMtimeMs: number | null;
|
|
403
|
+
fileSize: number | null;
|
|
404
|
+
preview: T;
|
|
405
|
+
}): void;
|
|
406
|
+
/** Plugin provenance already indexed for resources used by one session. */
|
|
407
|
+
export declare function getSessionPlugins(id: string): string[];
|
|
393
408
|
export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'account';
|
|
394
409
|
/**
|
|
395
410
|
* Smart-launch affinity priors: group sessions by origin machine, harness, or
|
package/dist/lib/session/db.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
-
import { parseSession } from './parse.js';
|
|
12
|
+
import { parseSession, sessionFilePathContainer } from './parse.js';
|
|
13
13
|
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
14
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
15
15
|
import { query as queryEvents, queryToolUsageForSessions } from '../events.js';
|
|
@@ -305,6 +305,18 @@ CREATE TABLE IF NOT EXISTS session_insights (
|
|
|
305
305
|
computed_at INTEGER NOT NULL,
|
|
306
306
|
facets TEXT NOT NULL
|
|
307
307
|
);
|
|
308
|
+
|
|
309
|
+
-- Normalized data behind sessions preview. Like session_insights this is a
|
|
310
|
+
-- lazy, stamp-validated cache: opening one session parses only that transcript,
|
|
311
|
+
-- while subsequent processes reuse the derived preview until its bytes change.
|
|
312
|
+
CREATE TABLE IF NOT EXISTS session_preview_cache (
|
|
313
|
+
session_id TEXT PRIMARY KEY,
|
|
314
|
+
file_mtime_ms INTEGER,
|
|
315
|
+
file_size INTEGER,
|
|
316
|
+
extractor_version INTEGER NOT NULL,
|
|
317
|
+
computed_at INTEGER NOT NULL,
|
|
318
|
+
preview_json TEXT NOT NULL
|
|
319
|
+
);
|
|
308
320
|
`;
|
|
309
321
|
/**
|
|
310
322
|
* Bumping this invalidates every cached facet row without touching the schema
|
|
@@ -314,6 +326,7 @@ CREATE TABLE IF NOT EXISTS session_insights (
|
|
|
314
326
|
*/
|
|
315
327
|
/** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
|
|
316
328
|
export const INSIGHTS_EXTRACTOR_VERSION = 6;
|
|
329
|
+
export const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
317
330
|
let dbInstance = null;
|
|
318
331
|
/**
|
|
319
332
|
* Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
|
|
@@ -2291,18 +2304,36 @@ function clearSessionExistenceCache() {
|
|
|
2291
2304
|
directoryMembershipSweepCount = 0;
|
|
2292
2305
|
}
|
|
2293
2306
|
function findMissingFilePaths(filePaths) {
|
|
2307
|
+
// Existence is decided on the CONTAINER file, never the raw stored path. A
|
|
2308
|
+
// composite `file_path` (`<container>#<id>`, e.g. OpenCode's `opencode.db#ses_…`)
|
|
2309
|
+
// names a row INSIDE a shared file — its basename is never a directory entry,
|
|
2310
|
+
// so a dirname/basename membership check on the composite string classified
|
|
2311
|
+
// every such row as deleted and pruned it (RUSH-2357). Group the original
|
|
2312
|
+
// paths under the container we actually stat, and map the verdict back so the
|
|
2313
|
+
// returned set still holds the original `file_path` strings the caller keys on.
|
|
2294
2314
|
const byDir = new Map();
|
|
2295
2315
|
for (const p of filePaths) {
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2316
|
+
const container = sessionFilePathContainer(p);
|
|
2317
|
+
const dir = path.dirname(container);
|
|
2318
|
+
const base = path.basename(container);
|
|
2319
|
+
let bases = byDir.get(dir);
|
|
2320
|
+
if (!bases) {
|
|
2321
|
+
bases = new Map();
|
|
2322
|
+
byDir.set(dir, bases);
|
|
2301
2323
|
}
|
|
2302
|
-
|
|
2324
|
+
let originals = bases.get(base);
|
|
2325
|
+
if (!originals) {
|
|
2326
|
+
originals = [];
|
|
2327
|
+
bases.set(base, originals);
|
|
2328
|
+
}
|
|
2329
|
+
originals.push(p);
|
|
2303
2330
|
}
|
|
2304
2331
|
const missing = new Set();
|
|
2305
|
-
|
|
2332
|
+
const markMissing = (originals) => {
|
|
2333
|
+
for (const original of originals)
|
|
2334
|
+
missing.add(original);
|
|
2335
|
+
};
|
|
2336
|
+
for (const [dir, bases] of byDir) {
|
|
2306
2337
|
let entries;
|
|
2307
2338
|
try {
|
|
2308
2339
|
const stat = fs.statSync(dir);
|
|
@@ -2324,17 +2355,16 @@ function findMissingFilePaths(filePaths) {
|
|
|
2324
2355
|
// Directory itself is gone (or unreadable) — every file in it is missing.
|
|
2325
2356
|
// Also covers the race where readdir loses to a concurrent delete: fall
|
|
2326
2357
|
// back to a direct stat rather than assuming existence.
|
|
2327
|
-
for (const base of
|
|
2358
|
+
for (const [base, originals] of bases) {
|
|
2328
2359
|
const filePath = path.join(dir, base);
|
|
2329
2360
|
if (!fs.existsSync(filePath))
|
|
2330
|
-
|
|
2361
|
+
markMissing(originals);
|
|
2331
2362
|
}
|
|
2332
2363
|
continue;
|
|
2333
2364
|
}
|
|
2334
|
-
for (const base of
|
|
2335
|
-
const filePath = path.join(dir, base);
|
|
2365
|
+
for (const [base, originals] of bases) {
|
|
2336
2366
|
if (!entries.has(base))
|
|
2337
|
-
|
|
2367
|
+
markMissing(originals);
|
|
2338
2368
|
}
|
|
2339
2369
|
}
|
|
2340
2370
|
return missing;
|
|
@@ -2471,6 +2501,49 @@ export function writeSessionInsights(entries) {
|
|
|
2471
2501
|
export function clearSessionInsights() {
|
|
2472
2502
|
getDB().exec(`DELETE FROM session_insights`);
|
|
2473
2503
|
}
|
|
2504
|
+
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
2505
|
+
export function readSessionPreviewCache(id, sourceStamp) {
|
|
2506
|
+
const row = getDB().prepare(`
|
|
2507
|
+
SELECT pc.preview_json AS previewJson
|
|
2508
|
+
FROM session_preview_cache pc
|
|
2509
|
+
WHERE pc.session_id = ?
|
|
2510
|
+
AND pc.extractor_version = ?
|
|
2511
|
+
AND pc.file_mtime_ms IS ?
|
|
2512
|
+
AND pc.file_size IS ?
|
|
2513
|
+
`).get(id, PREVIEW_EXTRACTOR_VERSION, sourceStamp.fileMtimeMs, sourceStamp.fileSize);
|
|
2514
|
+
if (!row)
|
|
2515
|
+
return undefined;
|
|
2516
|
+
try {
|
|
2517
|
+
return JSON.parse(row.previewJson);
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
return undefined;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
/** Persist normalized preview data against the exact transcript bytes parsed. */
|
|
2524
|
+
export function writeSessionPreviewCache(entry) {
|
|
2525
|
+
getDB().prepare(`
|
|
2526
|
+
INSERT INTO session_preview_cache
|
|
2527
|
+
(session_id, file_mtime_ms, file_size, extractor_version, computed_at, preview_json)
|
|
2528
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2529
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
2530
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
2531
|
+
file_size = excluded.file_size,
|
|
2532
|
+
extractor_version = excluded.extractor_version,
|
|
2533
|
+
computed_at = excluded.computed_at,
|
|
2534
|
+
preview_json = excluded.preview_json
|
|
2535
|
+
`).run(entry.id, entry.fileMtimeMs, entry.fileSize, PREVIEW_EXTRACTOR_VERSION, Date.now(), JSON.stringify(entry.preview));
|
|
2536
|
+
}
|
|
2537
|
+
/** Plugin provenance already indexed for resources used by one session. */
|
|
2538
|
+
export function getSessionPlugins(id) {
|
|
2539
|
+
const rows = getDB().prepare(`
|
|
2540
|
+
SELECT DISTINCT plugin
|
|
2541
|
+
FROM session_resource_usage
|
|
2542
|
+
WHERE session_id = ? AND plugin IS NOT NULL AND plugin <> ''
|
|
2543
|
+
ORDER BY plugin COLLATE NOCASE
|
|
2544
|
+
`).all(id);
|
|
2545
|
+
return rows.map(row => row.plugin);
|
|
2546
|
+
}
|
|
2474
2547
|
export function queryAffinityRollup(options) {
|
|
2475
2548
|
const db = getDB();
|
|
2476
2549
|
const where = [];
|
|
@@ -2708,7 +2781,9 @@ export function backfillResourceUsage(filter = {}, onProgress) {
|
|
|
2708
2781
|
result.scanned++;
|
|
2709
2782
|
let stamp;
|
|
2710
2783
|
try {
|
|
2711
|
-
|
|
2784
|
+
// Composite rows (`<container>#<id>`) stat their container file — the
|
|
2785
|
+
// per-session bytes/mtime stamp is derived during discovery, not here.
|
|
2786
|
+
const st = fs.statSync(sessionFilePathContainer(meta.filePath));
|
|
2712
2787
|
stamp = { fileMtimeMs: st.mtimeMs, fileSize: st.size };
|
|
2713
2788
|
}
|
|
2714
2789
|
catch {
|
|
@@ -2787,7 +2862,7 @@ export function topSessionsByCost(n, options = {}) {
|
|
|
2787
2862
|
// Over-fetch a small buffer to survive the on-disk liveness filter below.
|
|
2788
2863
|
const sql = `SELECT * FROM sessions ${whereCost} ORDER BY cost_usd DESC, timestamp DESC LIMIT ${limit + 16}`;
|
|
2789
2864
|
const rows = db.prepare(sql).all(...params);
|
|
2790
|
-
const live = rows.filter(r => !r.file_path || fs.existsSync(r.file_path));
|
|
2865
|
+
const live = rows.filter(r => !r.file_path || fs.existsSync(sessionFilePathContainer(r.file_path)));
|
|
2791
2866
|
return live.slice(0, limit).map(r => ({
|
|
2792
2867
|
meta: rowToMeta(r),
|
|
2793
2868
|
costUsd: r.cost_usd ?? 0,
|
|
@@ -18,7 +18,7 @@ import { getAgentsDir, getUserAgentsDir, getHistoryDir, getRunsDir } from '../st
|
|
|
18
18
|
import { shortCodexHome } from '../codex-home.js';
|
|
19
19
|
import { parseTimeFilter } from './relative-time.js';
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
21
|
-
import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
|
|
21
|
+
import { AGENTS, agentConfigDirName, getCliVersion, resolveOpenCodeAccountId } from '../agents.js';
|
|
22
22
|
import { walkForFilesWithStat } from '../fs-walk.js';
|
|
23
23
|
import { hasCommand } from '../cli-resources.js';
|
|
24
24
|
import { execFileShellSpec } from '../platform/exec.js';
|
|
@@ -28,7 +28,7 @@ import { deriveShortId } from './short-id.js';
|
|
|
28
28
|
import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
29
29
|
import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
|
|
30
30
|
import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
|
|
31
|
-
import { parseAntigravity, parseCursor } from './parse.js';
|
|
31
|
+
import { parseAntigravity, parseCursor, splitSessionFilePath } from './parse.js';
|
|
32
32
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
33
33
|
import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
|
|
34
34
|
import { machineForSessionFile } from './origin-machine.js';
|
|
@@ -335,6 +335,15 @@ export function isManagedSessionFile(filePath) {
|
|
|
335
335
|
// someone's dotfile dir, so scoping must not silently swallow them.
|
|
336
336
|
if (!filePath || !path.isAbsolute(filePath))
|
|
337
337
|
return true;
|
|
338
|
+
// A composite file_path (`<container>#<id>`) names a row inside a single shared
|
|
339
|
+
// DB the scanner reads from one fixed location (OpenCode's `opencode.db`) — that
|
|
340
|
+
// store is never a per-install dotfile under a version home, so the managed-vs-
|
|
341
|
+
// unmanaged split does not apply: there is exactly one store, not a "your own"
|
|
342
|
+
// copy to hide. Classifying it as unmanaged hid every OpenCode row from default
|
|
343
|
+
// listings once any agent was managed (RUSH-2357). Keyed off the composite FORM,
|
|
344
|
+
// so any future single-DB harness inherits this.
|
|
345
|
+
if (splitSessionFilePath(filePath).fragment !== undefined)
|
|
346
|
+
return true;
|
|
338
347
|
const roots = [
|
|
339
348
|
...VERSIONS_ROOTS.map((root) => path.join(root, 'versions')),
|
|
340
349
|
path.join(getHistoryDir(), 'backups'),
|
|
@@ -1838,39 +1847,24 @@ function readAntigravityMeta(filePath, currentVersion) {
|
|
|
1838
1847
|
const OPENCODE_DB = path.join(HOME, '.local', 'share', 'opencode', 'opencode.db');
|
|
1839
1848
|
let cachedOpenCodeAccount;
|
|
1840
1849
|
/**
|
|
1841
|
-
*
|
|
1850
|
+
* The active OpenCode account: the provider ids with a valid credential in
|
|
1851
|
+
* `auth.json`, joined (e.g. `"anthropic+muse-spark"`) — see
|
|
1852
|
+
* `resolveOpenCodeAccountId` in `../agents.js`, the single source of truth
|
|
1853
|
+
* `agents view`/`agents doctor` also read.
|
|
1842
1854
|
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1855
|
+
* OpenCode's `opencode.db` also carries `account`/`account_state`/
|
|
1856
|
+
* `control_account` tables that look like the obvious source, but on a real,
|
|
1857
|
+
* actively-used install (yosemite-s1, 1.16.0, 35 applied migrations) all
|
|
1858
|
+
* three are permanently empty — no migration populates them and no session
|
|
1859
|
+
* has ever written a row. Querying them (the pre-fix behavior here) always
|
|
1860
|
+
* returned undefined, credential or not; `auth.json` is what OpenCode
|
|
1861
|
+
* actually reads at runtime.
|
|
1845
1862
|
*/
|
|
1846
|
-
function getOpenCodeAccount(
|
|
1863
|
+
function getOpenCodeAccount() {
|
|
1847
1864
|
if (cachedOpenCodeAccount !== undefined)
|
|
1848
1865
|
return cachedOpenCodeAccount || undefined;
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
let owned;
|
|
1852
|
-
try {
|
|
1853
|
-
const db = handle ?? (fs.existsSync(OPENCODE_DB) ? (owned = new Database(OPENCODE_DB)) : undefined);
|
|
1854
|
-
if (db) {
|
|
1855
|
-
const row = db
|
|
1856
|
-
.prepare('SELECT email FROM control_account WHERE active=1 LIMIT 1;')
|
|
1857
|
-
.get();
|
|
1858
|
-
const out = typeof row?.email === 'string' ? row.email.trim() : '';
|
|
1859
|
-
if (out) {
|
|
1860
|
-
cachedOpenCodeAccount = out;
|
|
1861
|
-
return out;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
catch { /* DB not accessible, sqlite module unavailable, or query failed */ }
|
|
1866
|
-
finally {
|
|
1867
|
-
try {
|
|
1868
|
-
owned?.close();
|
|
1869
|
-
}
|
|
1870
|
-
catch { /* best-effort close */ }
|
|
1871
|
-
}
|
|
1872
|
-
cachedOpenCodeAccount = '';
|
|
1873
|
-
return undefined;
|
|
1866
|
+
cachedOpenCodeAccount = resolveOpenCodeAccountId(HOME) ?? '';
|
|
1867
|
+
return cachedOpenCodeAccount || undefined;
|
|
1874
1868
|
}
|
|
1875
1869
|
/**
|
|
1876
1870
|
* The per-session ledger stamp for an OpenCode row: the newest write time across
|
|
@@ -1945,12 +1939,28 @@ async function scanOpenCodeIncremental() {
|
|
|
1945
1939
|
// works on every OS — the CLI is absent on Windows.
|
|
1946
1940
|
let db;
|
|
1947
1941
|
try {
|
|
1942
|
+
db = new Database(OPENCODE_DB);
|
|
1943
|
+
// OpenCode's `session` schema varies by version: `cost` and `model` are newer
|
|
1944
|
+
// columns. Probe once and select NULL where absent, so an older opencode.db
|
|
1945
|
+
// still scans instead of throwing "no such column" and dropping every session.
|
|
1946
|
+
const sessionCols = new Set(db.prepare('PRAGMA table_info(session);').all()
|
|
1947
|
+
.map(c => (typeof c.name === 'string' ? c.name : '')));
|
|
1948
|
+
const costExpr = sessionCols.has('cost') ? 's.cost' : 'NULL';
|
|
1949
|
+
const modelExpr = sessionCols.has('model') ? 's.model' : 'NULL';
|
|
1950
|
+
// Every `json_extract` / `json_type` below is guarded by `json_valid`.
|
|
1951
|
+
// SQLite raises "malformed JSON" on a non-JSON value, and that aborts the
|
|
1952
|
+
// WHOLE query — so a single unparseable `part`/`message` row anywhere in the
|
|
1953
|
+
// shared DB would drop EVERY OpenCode session from the index, silently in a
|
|
1954
|
+
// non-TTY run (the handler below only prints when stderr is a TTY). A
|
|
1955
|
+
// poisoned row must cost that row, not the harness.
|
|
1948
1956
|
const query = `
|
|
1949
1957
|
SELECT
|
|
1950
1958
|
s.id AS id,
|
|
1951
1959
|
s.title AS title,
|
|
1952
1960
|
s.directory AS directory,
|
|
1953
1961
|
s.version AS version,
|
|
1962
|
+
${costExpr} AS cost,
|
|
1963
|
+
${modelExpr} AS model,
|
|
1954
1964
|
s.time_created AS time_created,
|
|
1955
1965
|
s.time_updated AS time_updated,
|
|
1956
1966
|
COALESCE(stats.message_count, 0) AS message_count,
|
|
@@ -1958,15 +1968,20 @@ async function scanOpenCodeIncremental() {
|
|
|
1958
1968
|
COALESCE(stats.message_bytes, 0) AS message_bytes,
|
|
1959
1969
|
COALESCE(parts.last_part_at, 0) AS last_part_at,
|
|
1960
1970
|
COALESCE(parts.part_bytes, 0) AS part_bytes,
|
|
1971
|
+
COALESCE(parts.tool_call_count, 0) AS tool_call_count,
|
|
1961
1972
|
stats.token_count AS token_count,
|
|
1962
1973
|
stats.output_tokens AS output_tokens,
|
|
1974
|
+
stats.input_tokens AS input_tokens,
|
|
1975
|
+
stats.cache_read_tokens AS cache_read_tokens,
|
|
1976
|
+
stats.cache_write_tokens AS cache_write_tokens,
|
|
1963
1977
|
COALESCE(stats.has_token_data, 0) AS has_token_data
|
|
1964
1978
|
FROM session s
|
|
1965
1979
|
LEFT JOIN (
|
|
1966
1980
|
SELECT
|
|
1967
1981
|
session_id,
|
|
1968
1982
|
MAX(time_created) AS last_part_at,
|
|
1969
|
-
SUM(LENGTH(CAST(data AS BLOB))) AS part_bytes
|
|
1983
|
+
SUM(LENGTH(CAST(data AS BLOB))) AS part_bytes,
|
|
1984
|
+
SUM(CASE WHEN json_valid(data) AND json_extract(data, '$.type') = 'tool' THEN 1 ELSE 0 END) AS tool_call_count
|
|
1970
1985
|
FROM part
|
|
1971
1986
|
GROUP BY session_id
|
|
1972
1987
|
) parts ON parts.session_id = s.id
|
|
@@ -1976,15 +1991,18 @@ async function scanOpenCodeIncremental() {
|
|
|
1976
1991
|
COUNT(*) AS message_count,
|
|
1977
1992
|
MAX(time_created) AS last_message_at,
|
|
1978
1993
|
SUM(LENGTH(CAST(data AS BLOB))) AS message_bytes,
|
|
1979
|
-
SUM(
|
|
1994
|
+
SUM(CASE WHEN json_valid(data) THEN
|
|
1980
1995
|
COALESCE(json_extract(data, '$.tokens.input'), 0) +
|
|
1981
1996
|
COALESCE(json_extract(data, '$.tokens.output'), 0) +
|
|
1982
1997
|
COALESCE(json_extract(data, '$.tokens.reasoning'), 0) +
|
|
1983
1998
|
COALESCE(json_extract(data, '$.tokens.cache.read'), 0) +
|
|
1984
1999
|
COALESCE(json_extract(data, '$.tokens.cache.write'), 0)
|
|
1985
|
-
) AS token_count,
|
|
1986
|
-
SUM(COALESCE(json_extract(data, '$.tokens.output'), 0)) AS output_tokens,
|
|
1987
|
-
|
|
2000
|
+
ELSE 0 END) AS token_count,
|
|
2001
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.output'), 0) ELSE 0 END) AS output_tokens,
|
|
2002
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.input'), 0) ELSE 0 END) AS input_tokens,
|
|
2003
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.cache.read'), 0) ELSE 0 END) AS cache_read_tokens,
|
|
2004
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.cache.write'), 0) ELSE 0 END) AS cache_write_tokens,
|
|
2005
|
+
MAX(CASE WHEN json_valid(data) AND json_type(data, '$.tokens') IS NOT NULL THEN 1 ELSE 0 END) AS has_token_data
|
|
1988
2006
|
FROM message
|
|
1989
2007
|
GROUP BY session_id
|
|
1990
2008
|
) stats ON stats.session_id = s.id
|
|
@@ -1992,8 +2010,7 @@ async function scanOpenCodeIncremental() {
|
|
|
1992
2010
|
ORDER BY time_created DESC
|
|
1993
2011
|
LIMIT 1000;
|
|
1994
2012
|
`.replace(/\n/g, ' ');
|
|
1995
|
-
|
|
1996
|
-
const account = getOpenCodeAccount(db);
|
|
2013
|
+
const account = getOpenCodeAccount();
|
|
1997
2014
|
const rows = db.prepare(query).all();
|
|
1998
2015
|
// Two passes. First derive each row's identity + per-session stamp, then
|
|
1999
2016
|
// bulk-load the prior stamps in ONE query and keep only the rows that
|
|
@@ -2034,13 +2051,40 @@ async function scanOpenCodeIncremental() {
|
|
|
2034
2051
|
const messageCount = asInt(row.message_count);
|
|
2035
2052
|
const tokenCount = asInt(row.token_count);
|
|
2036
2053
|
const outputTokens = asInt(row.output_tokens);
|
|
2054
|
+
const inputTokens = asInt(row.input_tokens);
|
|
2055
|
+
const cacheReadTokens = asInt(row.cache_read_tokens);
|
|
2056
|
+
const cacheWriteTokens = asInt(row.cache_write_tokens);
|
|
2037
2057
|
const hasTokenData = asInt(row.has_token_data) === 1;
|
|
2058
|
+
const toolCallCount = asInt(row.tool_call_count);
|
|
2038
2059
|
const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
|
|
2039
2060
|
// OpenCode is one shared DB, not one file per session — its row carries a
|
|
2040
2061
|
// per-session updated time. Set lastActivity explicitly (falling back to
|
|
2041
2062
|
// creation, never the whole-DB mtime the ScanStamp would otherwise supply).
|
|
2042
2063
|
const lastActivity = Number.isNaN(timeUpdated) ? timestamp : new Date(timeUpdated).toISOString();
|
|
2043
2064
|
const topic = title || undefined;
|
|
2065
|
+
// Duration is the session-row span; a missing/degenerate pair yields no value
|
|
2066
|
+
// rather than a negative or NaN.
|
|
2067
|
+
const durationMs = Number.isFinite(timeCreated) && Number.isFinite(timeUpdated) && timeUpdated > timeCreated
|
|
2068
|
+
? timeUpdated - timeCreated
|
|
2069
|
+
: undefined;
|
|
2070
|
+
// OpenCode stores the model as JSON (`{"id":"…","providerID":"…"}`); the
|
|
2071
|
+
// index tracks the model id.
|
|
2072
|
+
let model;
|
|
2073
|
+
if (typeof row.model === 'string' && row.model.trim()) {
|
|
2074
|
+
try {
|
|
2075
|
+
const parsed = JSON.parse(row.model);
|
|
2076
|
+
if (typeof parsed?.id === 'string' && parsed.id.trim())
|
|
2077
|
+
model = parsed.id;
|
|
2078
|
+
}
|
|
2079
|
+
catch { /* non-JSON model string — leave unset */ }
|
|
2080
|
+
}
|
|
2081
|
+
// `cost` is a REAL rollup OpenCode maintains on the session row (0 for a
|
|
2082
|
+
// zero-priced provider, which is a real value, not "unknown").
|
|
2083
|
+
const costUsd = typeof row.cost === 'number' ? row.cost : undefined;
|
|
2084
|
+
// Worktree slug is a pure function of cwd (`.agents/worktrees/<slug>/`),
|
|
2085
|
+
// so it derives for OpenCode exactly as it does for every other harness.
|
|
2086
|
+
const cwd = directory ? normalizeCwd(directory) : undefined;
|
|
2087
|
+
const worktreeSlug = detectWorktree(cwd)?.slug;
|
|
2044
2088
|
const meta = {
|
|
2045
2089
|
id,
|
|
2046
2090
|
shortId: deriveShortId(id, /^ses_/),
|
|
@@ -2048,14 +2092,22 @@ async function scanOpenCodeIncremental() {
|
|
|
2048
2092
|
timestamp,
|
|
2049
2093
|
lastActivity,
|
|
2050
2094
|
project: directory ? path.basename(directory) : undefined,
|
|
2051
|
-
cwd
|
|
2095
|
+
cwd,
|
|
2052
2096
|
filePath,
|
|
2053
2097
|
version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
|
|
2054
2098
|
account,
|
|
2055
2099
|
topic,
|
|
2100
|
+
model,
|
|
2101
|
+
costUsd,
|
|
2102
|
+
durationMs,
|
|
2103
|
+
worktreeSlug,
|
|
2104
|
+
toolCallCount: Number.isNaN(toolCallCount) ? undefined : toolCallCount,
|
|
2056
2105
|
messageCount: Number.isNaN(messageCount) ? undefined : messageCount,
|
|
2057
2106
|
tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
|
|
2058
2107
|
outputTokens: hasTokenData && !Number.isNaN(outputTokens) ? outputTokens : undefined,
|
|
2108
|
+
inputTokens: hasTokenData && !Number.isNaN(inputTokens) ? inputTokens : undefined,
|
|
2109
|
+
cacheReadTokens: hasTokenData && !Number.isNaN(cacheReadTokens) ? cacheReadTokens : undefined,
|
|
2110
|
+
cacheWriteTokens: hasTokenData && !Number.isNaN(cacheWriteTokens) ? cacheWriteTokens : undefined,
|
|
2059
2111
|
};
|
|
2060
2112
|
entries.push({ meta, content: topic || '', scan });
|
|
2061
2113
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Favorited
|
|
3
|
-
*
|
|
2
|
+
* Favorited sessions — the durable "keep this one handy" mark a human puts on a
|
|
3
|
+
* session, deliberately kept OUT of the session index.
|
|
4
4
|
*
|
|
5
5
|
* `sessions.db` is a rebuildable CACHE: a reindex or a schema bump throws its
|
|
6
6
|
* rows away and re-derives them from the transcripts on disk. A favorite is not
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Favorited
|
|
3
|
-
*
|
|
2
|
+
* Favorited sessions — the durable "keep this one handy" mark a human puts on a
|
|
3
|
+
* session, deliberately kept OUT of the session index.
|
|
4
4
|
*
|
|
5
5
|
* `sessions.db` is a rebuildable CACHE: a reindex or a schema bump throws its
|
|
6
6
|
* rows away and re-derives them from the transcripts on disk. A favorite is not
|
|
@@ -103,6 +103,33 @@ export declare function parseGemini(filePath: string): SessionEvent[];
|
|
|
103
103
|
* request step and a completion step that share the id).
|
|
104
104
|
*/
|
|
105
105
|
export declare function parseAntigravity(dbPath: string): SessionEvent[];
|
|
106
|
+
/**
|
|
107
|
+
* Separator between a container file and the id it holds inside a *composite*
|
|
108
|
+
* session `file_path`. Some harnesses keep every session in ONE file (OpenCode's
|
|
109
|
+
* single `opencode.db`), so the index stores `<container>#<session-id>` rather
|
|
110
|
+
* than one path per session — e.g.
|
|
111
|
+
* `/home/u/.local/share/opencode/opencode.db#ses_02410a2c…`.
|
|
112
|
+
*/
|
|
113
|
+
export declare const SESSION_FILE_PATH_SEP = "#";
|
|
114
|
+
/**
|
|
115
|
+
* Split a stored session `file_path` into its on-disk container and the optional
|
|
116
|
+
* in-container fragment. For a plain per-session file the container is the path
|
|
117
|
+
* itself and `fragment` is undefined; for a composite path it is the part before
|
|
118
|
+
* the first `#` and the id after it.
|
|
119
|
+
*/
|
|
120
|
+
export declare function splitSessionFilePath(filePath: string): {
|
|
121
|
+
container: string;
|
|
122
|
+
fragment: string | undefined;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* The filesystem path whose existence/stat decides whether a session row is
|
|
126
|
+
* stale. For a composite `file_path` this is the CONTAINER file — the row is a
|
|
127
|
+
* record inside it, never a filesystem entry of its own — so a composite session
|
|
128
|
+
* is stale only when its container is gone. For a plain path it is the path
|
|
129
|
+
* itself. Keying off the composite FORM (a `#` fragment), not any harness name,
|
|
130
|
+
* means any future single-file-DB harness inherits the correct behavior.
|
|
131
|
+
*/
|
|
132
|
+
export declare function sessionFilePathContainer(filePath: string): string;
|
|
106
133
|
/**
|
|
107
134
|
* Parse an OpenCode session from its SQLite database.
|
|
108
135
|
* filePath format: "/path/to/opencode.db#session_id"
|
|
@@ -130,6 +157,42 @@ export declare function parseAntigravity(dbPath: string): SessionEvent[];
|
|
|
130
157
|
* (from summary.json), falling back to the transcript's mtime.
|
|
131
158
|
*/
|
|
132
159
|
export declare function parseGrok(filePath: string): SessionEvent[];
|
|
160
|
+
/**
|
|
161
|
+
* The transcript query {@link parseOpenCode} runs, exported so a test can assert
|
|
162
|
+
* the projection's real cost against a database instead of re-typing the SQL.
|
|
163
|
+
*
|
|
164
|
+
* A tool part is PROJECTED to exactly the fields the `case 'tool'` branch below
|
|
165
|
+
* reads — `tool`, `callID`, `state.status`, `state.input`, `state.output` —
|
|
166
|
+
* rather than carried whole. Two things this has to get right at once:
|
|
167
|
+
*
|
|
168
|
+
* - Keep `state.input`. It carries `filePath` / `command`, and losing it is
|
|
169
|
+
* what left `recentDirectoriesTouched` empty (RUSH-2358).
|
|
170
|
+
* - Stay bounded. A tool part is not bounded by its output: on a real
|
|
171
|
+
* `opencode.db` the largest part is 1,346,068 bytes of which
|
|
172
|
+
* `state.attachments` (a base64 data URL from `read`) is 1,345,674, while
|
|
173
|
+
* `state.output` is 23. Truncating only `state.output` therefore bounded
|
|
174
|
+
* nothing — it took one session's loaded tool payload from 299,365 to
|
|
175
|
+
* 1,911,209 bytes. The projection drops `attachments` (and every other
|
|
176
|
+
* unread key) outright, caps `state.output`, and collapses an oversized
|
|
177
|
+
* `state.input` to just its addressing fields. Those are the keys the
|
|
178
|
+
* enrichment reads — `filePath`/`path` for an edit and `cwd`/`workdir`/
|
|
179
|
+
* `working_directory` for a shell (`extractRecentDirectoriesTouched` in
|
|
180
|
+
* state.ts), plus `command`/`description`, themselves capped since a command
|
|
181
|
+
* can be arbitrarily long. Above the cap every other input key (an `edit`'s
|
|
182
|
+
* `oldString`/`newString`) is gone, deliberately — nothing downstream reads
|
|
183
|
+
* them, and they are the weight.
|
|
184
|
+
*
|
|
185
|
+
* `json_valid` guards every `json_extract`: SQLite raises "malformed JSON" on a
|
|
186
|
+
* non-JSON value, which aborts the WHOLE query, so one bad `part` row would
|
|
187
|
+
* otherwise cost the entire transcript. Note the single-argument form is
|
|
188
|
+
* RFC-8259-strict — it rejects a JSONB blob that `json_extract` would accept.
|
|
189
|
+
* That is correct for today's schema (`part.data` / `message.data` are TEXT on
|
|
190
|
+
* a real database); if OpenCode ever migrates them to JSONB, these guards must
|
|
191
|
+
* move to `json_valid(data, 6)` or they will drop every row.
|
|
192
|
+
*
|
|
193
|
+
* The session id is bound as a parameter; the two caps are numeric literals.
|
|
194
|
+
*/
|
|
195
|
+
export declare const OPENCODE_TRANSCRIPT_QUERY: string;
|
|
133
196
|
export declare function parseOpenCode(filePath: string): SessionEvent[];
|
|
134
197
|
/** Parse a Rush JSONL session file into normalized events. */
|
|
135
198
|
export declare function parseRush(filePath: string): SessionEvent[];
|