@jobshimo/browser-link 0.27.1 → 0.28.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/dist/activity/db.d.ts +27 -0
- package/dist/activity/db.js +98 -0
- package/dist/activity/db.js.map +1 -0
- package/dist/activity/extract.d.ts +32 -0
- package/dist/activity/extract.js +126 -0
- package/dist/activity/extract.js.map +1 -0
- package/dist/activity/paths.d.ts +16 -0
- package/dist/activity/paths.js +24 -0
- package/dist/activity/paths.js.map +1 -0
- package/dist/activity/queries.d.ts +100 -0
- package/dist/activity/queries.js +286 -0
- package/dist/activity/queries.js.map +1 -0
- package/dist/activity/types.d.ts +119 -0
- package/dist/activity/types.js +41 -0
- package/dist/activity/types.js.map +1 -0
- package/dist/bridge/ws-bridge.d.ts +18 -1
- package/dist/bridge/ws-bridge.js +107 -0
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/cdp/inpage/deep-query.d.ts +1 -1
- package/dist/cdp/inpage/deep-query.js +1 -1
- package/dist/cdp/inpage/dom-helpers.d.ts +1 -1
- package/dist/cdp/inpage/dom-helpers.js +1 -1
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/activity.d.ts +11 -0
- package/dist/commands/activity.js +347 -0
- package/dist/commands/activity.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js.map +1 -1
- package/dist/extension/activity.d.ts +201 -0
- package/dist/extension/activity.html +662 -0
- package/dist/extension/activity.js +555 -0
- package/dist/extension/activity.js.map +1 -0
- package/dist/extension/background.js +146 -0
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/inpage/deep-query.d.ts +1 -1
- package/dist/extension/inpage/deep-query.js +1 -1
- package/dist/extension/inpage/dom-helpers.d.ts +1 -1
- package/dist/extension/inpage/dom-helpers.js +1 -1
- package/dist/extension/manifest.json +3 -2
- package/dist/extension/popup.html +58 -0
- package/dist/extension/popup.js +11 -0
- package/dist/extension/popup.js.map +1 -1
- package/dist/messages.d.ts +80 -2
- package/dist/server.js +30 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +50 -2
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +34 -7
- package/dist/tools/browser-dispatch.js +107 -1
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Rows kept before the oldest are pruned.
|
|
4
|
+
*
|
|
5
|
+
* Sized so a heavy day of agent work never evicts the morning: a `repeat`
|
|
6
|
+
* flow can dispatch a few hundred actions in one call, so a 200-row ceiling
|
|
7
|
+
* like `BridgeEventLog`'s would make the trail useless for exactly the runs
|
|
8
|
+
* worth auditing. 50k rows is a few tens of MB at this row width — cheap for
|
|
9
|
+
* a file on disk, and unlike the in-memory event log nothing here competes
|
|
10
|
+
* with the process's working set.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_ACTIVITY_ROWS = 50000;
|
|
13
|
+
export declare function getActivityDb(): Database.Database;
|
|
14
|
+
/**
|
|
15
|
+
* AUTOINCREMENT on the primary key is deliberate, and it is the reason the
|
|
16
|
+
* `sinceId` cursor is safe.
|
|
17
|
+
*
|
|
18
|
+
* Without it SQLite reuses the rowids of deleted rows, so pruning the oldest
|
|
19
|
+
* rows could hand a NEW row an id a tailing panel had already seen and
|
|
20
|
+
* skipped past — the panel would silently never render it. AUTOINCREMENT
|
|
21
|
+
* costs one extra table (`sqlite_sequence`) and buys a monotonic cursor that
|
|
22
|
+
* survives pruning.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pruneIfDue(db: Database.Database, force?: boolean): number;
|
|
25
|
+
/** Close and forget the handle. Tests use this between temp data dirs; the
|
|
26
|
+
* CLI uses it so a `clear --vacuum` is not fighting its own open WAL. */
|
|
27
|
+
export declare function closeActivityDb(): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { getActivityDbPath } from './paths.js';
|
|
5
|
+
let dbInstance = null;
|
|
6
|
+
/**
|
|
7
|
+
* Rows kept before the oldest are pruned.
|
|
8
|
+
*
|
|
9
|
+
* Sized so a heavy day of agent work never evicts the morning: a `repeat`
|
|
10
|
+
* flow can dispatch a few hundred actions in one call, so a 200-row ceiling
|
|
11
|
+
* like `BridgeEventLog`'s would make the trail useless for exactly the runs
|
|
12
|
+
* worth auditing. 50k rows is a few tens of MB at this row width — cheap for
|
|
13
|
+
* a file on disk, and unlike the in-memory event log nothing here competes
|
|
14
|
+
* with the process's working set.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_ACTIVITY_ROWS = 50_000;
|
|
17
|
+
/**
|
|
18
|
+
* How many inserts between prune sweeps.
|
|
19
|
+
*
|
|
20
|
+
* Pruning on every insert would run a DELETE with a subquery on the hot path
|
|
21
|
+
* of every single agent action. Pruning on a counter means the table can
|
|
22
|
+
* briefly exceed `MAX_ACTIVITY_ROWS` by at most this much, which costs
|
|
23
|
+
* nothing and keeps the write path a single INSERT.
|
|
24
|
+
*/
|
|
25
|
+
const PRUNE_INTERVAL = 500;
|
|
26
|
+
let insertsSincePrune = 0;
|
|
27
|
+
export function getActivityDb() {
|
|
28
|
+
if (dbInstance)
|
|
29
|
+
return dbInstance;
|
|
30
|
+
const path = getActivityDbPath();
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
32
|
+
const db = new Database(path);
|
|
33
|
+
db.pragma('journal_mode = WAL');
|
|
34
|
+
runMigrations(db);
|
|
35
|
+
dbInstance = db;
|
|
36
|
+
return db;
|
|
37
|
+
}
|
|
38
|
+
function runMigrations(db) {
|
|
39
|
+
// Same idempotent CREATE-IF-NOT-EXISTS pattern as map/db.ts: an existing
|
|
40
|
+
// file from an earlier version picks up new tables on next open without
|
|
41
|
+
// touching a single stored row.
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS activity (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
at TEXT NOT NULL,
|
|
46
|
+
tool TEXT NOT NULL,
|
|
47
|
+
tab_id TEXT,
|
|
48
|
+
transport TEXT CHECK (transport IN ('extension', 'cdp')),
|
|
49
|
+
url TEXT,
|
|
50
|
+
title TEXT,
|
|
51
|
+
agent TEXT,
|
|
52
|
+
agent_pid INTEGER,
|
|
53
|
+
selector TEXT,
|
|
54
|
+
payload TEXT,
|
|
55
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('ok', 'error')),
|
|
56
|
+
error TEXT,
|
|
57
|
+
duration_ms INTEGER NOT NULL,
|
|
58
|
+
flow_id TEXT
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_activity_at ON activity(at);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_activity_tab ON activity(tab_id);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_activity_flow ON activity(flow_id);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_activity_agent ON activity(agent);
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* AUTOINCREMENT on the primary key is deliberate, and it is the reason the
|
|
69
|
+
* `sinceId` cursor is safe.
|
|
70
|
+
*
|
|
71
|
+
* Without it SQLite reuses the rowids of deleted rows, so pruning the oldest
|
|
72
|
+
* rows could hand a NEW row an id a tailing panel had already seen and
|
|
73
|
+
* skipped past — the panel would silently never render it. AUTOINCREMENT
|
|
74
|
+
* costs one extra table (`sqlite_sequence`) and buys a monotonic cursor that
|
|
75
|
+
* survives pruning.
|
|
76
|
+
*/
|
|
77
|
+
export function pruneIfDue(db, force = false) {
|
|
78
|
+
insertsSincePrune += 1;
|
|
79
|
+
if (!force && insertsSincePrune < PRUNE_INTERVAL)
|
|
80
|
+
return 0;
|
|
81
|
+
insertsSincePrune = 0;
|
|
82
|
+
const result = db
|
|
83
|
+
.prepare(`DELETE FROM activity WHERE id <= (
|
|
84
|
+
SELECT MAX(id) FROM activity
|
|
85
|
+
) - ?`)
|
|
86
|
+
.run(MAX_ACTIVITY_ROWS);
|
|
87
|
+
return result.changes;
|
|
88
|
+
}
|
|
89
|
+
/** Close and forget the handle. Tests use this between temp data dirs; the
|
|
90
|
+
* CLI uses it so a `clear --vacuum` is not fighting its own open WAL. */
|
|
91
|
+
export function closeActivityDb() {
|
|
92
|
+
if (!dbInstance)
|
|
93
|
+
return;
|
|
94
|
+
dbInstance.close();
|
|
95
|
+
dbInstance = null;
|
|
96
|
+
insertsSincePrune = 0;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/activity/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C,IAAI,UAAU,GAA6B,IAAI,CAAC;AAEhD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC;;;;;;;GAOG;AACH,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAE1B,MAAM,UAAU,aAAa;IAC3B,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAClC,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,UAAU,GAAG,EAAE,CAAC;IAChB,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,aAAa,CAAC,EAAqB;IAC1C,yEAAyE;IACzE,wEAAwE;IACxE,gCAAgC;IAChC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;GAuBP,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,EAAqB,EAAE,KAAK,GAAG,KAAK;IAC7D,iBAAiB,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC,KAAK,IAAI,iBAAiB,GAAG,cAAc;QAAE,OAAO,CAAC,CAAC;IAC3D,iBAAiB,GAAG,CAAC,CAAC;IACtB,MAAM,MAAM,GAAG,EAAE;SACd,OAAO,CACN;;aAEO,CACR;SACA,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC,OAAO,CAAC;AACxB,CAAC;AAED;yEACyE;AACzE,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC,UAAU;QAAE,OAAO;IACxB,UAAU,CAAC,KAAK,EAAE,CAAC;IACnB,UAAU,GAAG,IAAI,CAAC;IAClB,iBAAiB,GAAG,CAAC,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ActivityInput, ActivityTransport } from './types.js';
|
|
2
|
+
import type { AgentCaller } from '../tools/tab-claims.js';
|
|
3
|
+
export declare function extractSelector(tool: string, args: unknown): string | null;
|
|
4
|
+
export declare function extractPayload(tool: string, args: unknown): string | null;
|
|
5
|
+
/** The browser-link tab id the call targeted, when it takes one. */
|
|
6
|
+
export declare function extractTabId(args: unknown): string | null;
|
|
7
|
+
/** Flow id, for the two tools that address an existing flow by id. Steps
|
|
8
|
+
* dispatched INSIDE a flow are stamped by the flow runner, not here. */
|
|
9
|
+
export declare function extractFlowId(args: unknown): string | null;
|
|
10
|
+
/** `cdp:`-prefixed ids are the cdp-direct transport; everything else reached
|
|
11
|
+
* the page through the extension. Mirrors `isCdpTabId` without importing it,
|
|
12
|
+
* so the trail keeps working if that predicate ever gains transport-selection
|
|
13
|
+
* side effects. */
|
|
14
|
+
export declare function transportFor(tabId: string | null): ActivityTransport | null;
|
|
15
|
+
/** Self-declared label first, binary as the fallback. `agent_id` alone is a
|
|
16
|
+
* uuid nobody can read; the point of this column is that a human scanning an
|
|
17
|
+
* exported trail can tell Claude Code from OpenCode at a glance. */
|
|
18
|
+
export declare function agentNameFor(caller: AgentCaller | undefined): string | null;
|
|
19
|
+
export interface BuildRowInput {
|
|
20
|
+
tool: string;
|
|
21
|
+
args: unknown;
|
|
22
|
+
caller?: AgentCaller;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
error?: unknown;
|
|
25
|
+
/** False when `activity.record-payloads` is off: the row is still written,
|
|
26
|
+
* with its shape intact, minus the free text. */
|
|
27
|
+
recordPayloads: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Assemble the row. Deliberately total — every branch returns something
|
|
30
|
+
* storable, because a tool call that produced a row we could not describe is
|
|
31
|
+
* still a tool call that happened. */
|
|
32
|
+
export declare function buildRow({ tool, args, caller, durationMs, error, recordPayloads, }: BuildRowInput): ActivityInput;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn one tool dispatch into a trail row.
|
|
3
|
+
*
|
|
4
|
+
* This module exists so the dispatcher wrapper stays four lines. Knowing that
|
|
5
|
+
* `type`'s interesting text is `args.text` while `evaluate`'s is
|
|
6
|
+
* `args.expression` is knowledge about the TOOL SURFACE, and it belongs next
|
|
7
|
+
* to the trail that consumes it — not smeared through a 30-arm switch whose
|
|
8
|
+
* job is routing.
|
|
9
|
+
*/
|
|
10
|
+
/** Which argument carries the CSS selector, per tool. Tools absent from the
|
|
11
|
+
* map simply record no selector. */
|
|
12
|
+
const SELECTOR_ARG = {
|
|
13
|
+
'browser.click': 'selector',
|
|
14
|
+
'browser.type': 'selector',
|
|
15
|
+
'browser.press': 'selector',
|
|
16
|
+
'browser.wait_for': 'selector',
|
|
17
|
+
'browser.snapshot': 'within_selector',
|
|
18
|
+
'browser.canvas_screenshot': 'selector',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Which argument carries the free text worth keeping, per tool.
|
|
22
|
+
*
|
|
23
|
+
* Only tools whose payload actually reconstructs the action appear here. A
|
|
24
|
+
* `browser.snapshot`'s `max_interactive` is a knob, not a payload; recording
|
|
25
|
+
* knobs would bury the three fields that matter (what was typed, what was
|
|
26
|
+
* evaluated, where it navigated) under noise.
|
|
27
|
+
*/
|
|
28
|
+
const PAYLOAD_ARG = {
|
|
29
|
+
'browser.navigate': 'url',
|
|
30
|
+
'browser.type': 'text',
|
|
31
|
+
'browser.find': 'text',
|
|
32
|
+
'browser.evaluate': 'expression',
|
|
33
|
+
'browser.press': 'key',
|
|
34
|
+
};
|
|
35
|
+
function readString(args, key) {
|
|
36
|
+
if (typeof args !== 'object' || args === null)
|
|
37
|
+
return null;
|
|
38
|
+
const value = args[key];
|
|
39
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
40
|
+
}
|
|
41
|
+
/** `browser.flow` has no single payload argument — its substance is the step
|
|
42
|
+
* list. Summarising it as `"5 steps: find → click → type"` keeps one trail row
|
|
43
|
+
* readable where inlining the whole JSON would not, and the per-step detail is
|
|
44
|
+
* already recorded by the steps' own rows. */
|
|
45
|
+
function summariseFlow(args) {
|
|
46
|
+
if (typeof args !== 'object' || args === null)
|
|
47
|
+
return null;
|
|
48
|
+
const steps = args.steps;
|
|
49
|
+
if (!Array.isArray(steps))
|
|
50
|
+
return null;
|
|
51
|
+
const kinds = steps
|
|
52
|
+
.map((step) => typeof step === 'object' && step !== null ? Object.keys(step)[0] : null)
|
|
53
|
+
.filter((k) => typeof k === 'string');
|
|
54
|
+
if (kinds.length === 0)
|
|
55
|
+
return `${steps.length} steps`;
|
|
56
|
+
return `${steps.length} steps: ${kinds.join(' → ')}`;
|
|
57
|
+
}
|
|
58
|
+
export function extractSelector(tool, args) {
|
|
59
|
+
const key = SELECTOR_ARG[tool];
|
|
60
|
+
return key === undefined ? null : readString(args, key);
|
|
61
|
+
}
|
|
62
|
+
export function extractPayload(tool, args) {
|
|
63
|
+
if (tool === 'browser.flow')
|
|
64
|
+
return summariseFlow(args);
|
|
65
|
+
const key = PAYLOAD_ARG[tool];
|
|
66
|
+
return key === undefined ? null : readString(args, key);
|
|
67
|
+
}
|
|
68
|
+
/** The browser-link tab id the call targeted, when it takes one. */
|
|
69
|
+
export function extractTabId(args) {
|
|
70
|
+
return readString(args, 'tab_id');
|
|
71
|
+
}
|
|
72
|
+
/** Flow id, for the two tools that address an existing flow by id. Steps
|
|
73
|
+
* dispatched INSIDE a flow are stamped by the flow runner, not here. */
|
|
74
|
+
export function extractFlowId(args) {
|
|
75
|
+
return readString(args, 'flow_id');
|
|
76
|
+
}
|
|
77
|
+
/** `cdp:`-prefixed ids are the cdp-direct transport; everything else reached
|
|
78
|
+
* the page through the extension. Mirrors `isCdpTabId` without importing it,
|
|
79
|
+
* so the trail keeps working if that predicate ever gains transport-selection
|
|
80
|
+
* side effects. */
|
|
81
|
+
export function transportFor(tabId) {
|
|
82
|
+
if (tabId === null)
|
|
83
|
+
return null;
|
|
84
|
+
return tabId.startsWith('cdp:') ? 'cdp' : 'extension';
|
|
85
|
+
}
|
|
86
|
+
/** Self-declared label first, binary as the fallback. `agent_id` alone is a
|
|
87
|
+
* uuid nobody can read; the point of this column is that a human scanning an
|
|
88
|
+
* exported trail can tell Claude Code from OpenCode at a glance. */
|
|
89
|
+
export function agentNameFor(caller) {
|
|
90
|
+
if (!caller)
|
|
91
|
+
return null;
|
|
92
|
+
if (caller.label && caller.label.length > 0)
|
|
93
|
+
return caller.label;
|
|
94
|
+
return caller.binary && caller.binary.length > 0 ? caller.binary : null;
|
|
95
|
+
}
|
|
96
|
+
/** Assemble the row. Deliberately total — every branch returns something
|
|
97
|
+
* storable, because a tool call that produced a row we could not describe is
|
|
98
|
+
* still a tool call that happened. */
|
|
99
|
+
export function buildRow({ tool, args, caller, durationMs, error, recordPayloads, }) {
|
|
100
|
+
const tabId = extractTabId(args);
|
|
101
|
+
return {
|
|
102
|
+
tool,
|
|
103
|
+
tabId,
|
|
104
|
+
transport: transportFor(tabId),
|
|
105
|
+
// Filled in by the dispatcher wrapper, which is the only caller that can
|
|
106
|
+
// see the tab table. Null here keeps this module free of bridge deps.
|
|
107
|
+
url: null,
|
|
108
|
+
title: null,
|
|
109
|
+
agent: agentNameFor(caller),
|
|
110
|
+
agentPid: caller?.pid ?? null,
|
|
111
|
+
selector: extractSelector(tool, args),
|
|
112
|
+
payload: recordPayloads ? extractPayload(tool, args) : null,
|
|
113
|
+
outcome: error === undefined ? 'ok' : 'error',
|
|
114
|
+
error: error === undefined ? null : errorMessage(error),
|
|
115
|
+
durationMs,
|
|
116
|
+
flowId: extractFlowId(args),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function errorMessage(error) {
|
|
120
|
+
if (error instanceof Error)
|
|
121
|
+
return error.message;
|
|
122
|
+
if (typeof error === 'string')
|
|
123
|
+
return error;
|
|
124
|
+
return String(error);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/activity/extract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activity trail gets its OWN SQLite file, next to `map.db` rather than
|
|
3
|
+
* inside it.
|
|
4
|
+
*
|
|
5
|
+
* The two have opposite lifecycles. `map.db` is small, slow-growing and
|
|
6
|
+
* precious — it holds what the agent learned about the user's apps, and losing
|
|
7
|
+
* it means relearning every selector. The trail is append-only, high-churn and
|
|
8
|
+
* disposable: thousands of rows a session, cleared whenever the user feels like
|
|
9
|
+
* it.
|
|
10
|
+
*
|
|
11
|
+
* Putting them in one file would mean the trail's WAL growth and its
|
|
12
|
+
* `activity clear` VACUUM both operate on the file holding the map, and it
|
|
13
|
+
* would make "back up my map" ambiguous. Two files, two retention policies,
|
|
14
|
+
* no shared blast radius.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getActivityDbPath(): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
// The data dir resolver is shared, not map-specific — it predates this module
|
|
3
|
+
// and simply happens to live under map/. Imported rather than duplicated so
|
|
4
|
+
// $BROWSER_LINK_DATA_DIR keeps meaning one thing for the whole process.
|
|
5
|
+
import { getDataDir } from '../map/paths.js';
|
|
6
|
+
/**
|
|
7
|
+
* The activity trail gets its OWN SQLite file, next to `map.db` rather than
|
|
8
|
+
* inside it.
|
|
9
|
+
*
|
|
10
|
+
* The two have opposite lifecycles. `map.db` is small, slow-growing and
|
|
11
|
+
* precious — it holds what the agent learned about the user's apps, and losing
|
|
12
|
+
* it means relearning every selector. The trail is append-only, high-churn and
|
|
13
|
+
* disposable: thousands of rows a session, cleared whenever the user feels like
|
|
14
|
+
* it.
|
|
15
|
+
*
|
|
16
|
+
* Putting them in one file would mean the trail's WAL growth and its
|
|
17
|
+
* `activity clear` VACUUM both operate on the file holding the map, and it
|
|
18
|
+
* would make "back up my map" ambiguous. Two files, two retention policies,
|
|
19
|
+
* no shared blast radius.
|
|
20
|
+
*/
|
|
21
|
+
export function getActivityDbPath() {
|
|
22
|
+
return join(getDataDir(), 'activity.db');
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/activity/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,8EAA8E;AAC9E,4EAA4E;AAC5E,wEAAwE;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,IAAI,CAAC,UAAU,EAAE,EAAE,aAAa,CAAC,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { ActivityInput, ActivityPage, ActivityQuery, ActivityRecord } from './types.js';
|
|
3
|
+
/** Hard ceiling on rows a single read can return, whatever `limit` asks for.
|
|
4
|
+
* The panel pages; nothing needs ten thousand rows in one response, and an
|
|
5
|
+
* unbounded read is how a renderer freezes a browser. */
|
|
6
|
+
export declare const MAX_QUERY_LIMIT = 500;
|
|
7
|
+
/** Longest stored `payload` / `error`. A `browser.evaluate` expression can be
|
|
8
|
+
* arbitrarily long and an error can carry a page's worth of text; neither is
|
|
9
|
+
* more legible at 40 KB than at 4 KB, and the trail is meant to stay
|
|
10
|
+
* greppable. Truncation is marked so a reader never mistakes a cut string for
|
|
11
|
+
* the whole thing. */
|
|
12
|
+
export declare const MAX_TEXT_LEN = 4000;
|
|
13
|
+
export declare function truncate(value: string | null, max?: number): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Append one dispatched call to the trail.
|
|
16
|
+
*
|
|
17
|
+
* NEVER THROWS. Recording is observation, not the work: an agent's click must
|
|
18
|
+
* not fail because the disk is full or the DB is locked. The caller
|
|
19
|
+
* (`browser-dispatch.ts`) treats this as fire-and-forget, and a lost row is
|
|
20
|
+
* strictly better than a lost action.
|
|
21
|
+
*/
|
|
22
|
+
export declare function record(input: ActivityInput, db?: Database.Database): ActivityRecord | null;
|
|
23
|
+
/**
|
|
24
|
+
* Read a page of the trail, NEWEST FIRST.
|
|
25
|
+
*
|
|
26
|
+
* Newest-first is what both readers want and it is the only ordering that
|
|
27
|
+
* makes `limit` honest: with oldest-first, "the last 200" would require
|
|
28
|
+
* counting the whole table before choosing an offset. The window reverses the
|
|
29
|
+
* page for display; the CLI prints it as-is.
|
|
30
|
+
*/
|
|
31
|
+
export declare function query(q?: ActivityQuery, db?: Database.Database): ActivityPage;
|
|
32
|
+
/** Every distinct agent that appears in the trail, most recently seen first.
|
|
33
|
+
* Feeds the window's filter chips — a hardcoded list would go stale the
|
|
34
|
+
* moment someone wires up a new MCP client. */
|
|
35
|
+
export declare function listAgents(db?: Database.Database): {
|
|
36
|
+
agent: string;
|
|
37
|
+
count: number;
|
|
38
|
+
}[];
|
|
39
|
+
/** Delete the trail, or the part of it older than `before`. Returns rows
|
|
40
|
+
* removed. VACUUM is the caller's call (the CLI offers it) — it rewrites the
|
|
41
|
+
* whole file and has no business running inside a tool dispatch. */
|
|
42
|
+
export declare function clear(before?: string, db?: Database.Database): number;
|
|
43
|
+
/** An inclusive date window. Both ends optional: `{ from }` alone means
|
|
44
|
+
* "everything since", `{ to }` alone means "everything up to", `{}` means the
|
|
45
|
+
* whole trail. ISO-8601 strings, compared lexically — which is exactly right
|
|
46
|
+
* for the UTC ISO timestamps `record` writes. */
|
|
47
|
+
export interface ActivityRange {
|
|
48
|
+
from?: string;
|
|
49
|
+
to?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How many rows a purge of this range WOULD remove.
|
|
53
|
+
*
|
|
54
|
+
* Exists so the UI can tell the user the number before they confirm, not
|
|
55
|
+
* after. "Delete 4,812 of 12,483 actions" is a decision; "Delete?" is a
|
|
56
|
+
* gamble, and this delete is permanent.
|
|
57
|
+
*/
|
|
58
|
+
export declare function countInRange(range: ActivityRange, db?: Database.Database): number;
|
|
59
|
+
/**
|
|
60
|
+
* Delete every row inside an inclusive date range. Returns rows removed.
|
|
61
|
+
*
|
|
62
|
+
* `vacuum` defaults ON here, unlike `clear`. The whole point of purging from
|
|
63
|
+
* the UI is that the file got big, and a DELETE alone leaves the pages
|
|
64
|
+
* allocated — the user would purge half the trail, watch the reported size not
|
|
65
|
+
* move, and reasonably conclude it did nothing. VACUUM rewrites the file so
|
|
66
|
+
* the number they are watching actually drops.
|
|
67
|
+
*/
|
|
68
|
+
export declare function purgeRange(range: ActivityRange, options?: {
|
|
69
|
+
vacuum?: boolean;
|
|
70
|
+
}, db?: Database.Database): number;
|
|
71
|
+
/** What the trail currently costs, for the panel that must always show it. */
|
|
72
|
+
export interface ActivityStats {
|
|
73
|
+
rows: number;
|
|
74
|
+
/** Bytes on disk: the DB plus its WAL and shared-memory sidecars. Reporting
|
|
75
|
+
* only the .db file would understate a busy trail badly — WAL mode parks
|
|
76
|
+
* recent writes in `activity.db-wal` until a checkpoint folds them in. */
|
|
77
|
+
bytes: number;
|
|
78
|
+
/** ISO timestamps of the oldest and newest row, or null on an empty trail.
|
|
79
|
+
* Lets the UI say "since Aug 3" instead of a bare row count. */
|
|
80
|
+
oldestAt: string | null;
|
|
81
|
+
newestAt: string | null;
|
|
82
|
+
/** The ceiling from `db.ts`, so the UI can show how close the trail is to
|
|
83
|
+
* pruning itself rather than hardcoding a number that could drift. */
|
|
84
|
+
maxRows: number;
|
|
85
|
+
}
|
|
86
|
+
export declare function stats(db?: Database.Database): ActivityStats;
|
|
87
|
+
/**
|
|
88
|
+
* Stream the whole trail for export, oldest first, in batches.
|
|
89
|
+
*
|
|
90
|
+
* A generator rather than an array on purpose: an export of 50k rows must not
|
|
91
|
+
* materialise 50k objects to hand to a JSON serializer. Oldest-first because
|
|
92
|
+
* an exported file is read top-to-bottom as a story, which is the opposite of
|
|
93
|
+
* how a live panel is read.
|
|
94
|
+
*/
|
|
95
|
+
export declare function exportAll(filter?: ActivityQuery, db?: Database.Database): Generator<ActivityRecord>;
|
|
96
|
+
/** Strip the free-text columns from a record on its way out of the machine.
|
|
97
|
+
* Applied at EXPORT time, never at record time — see the PRIVACY CONTRACT in
|
|
98
|
+
* `types.ts`. The row keeps its shape so a redacted export is still a valid,
|
|
99
|
+
* diffable trail; only what was typed or evaluated goes. */
|
|
100
|
+
export declare function redact(record: ActivityRecord): ActivityRecord;
|