@agentstrack/collector 0.1.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/CHANGELOG.md +168 -0
- package/LICENSE +202 -0
- package/README.md +779 -0
- package/dist/adapters/account.d.ts +22 -0
- package/dist/adapters/account.js +142 -0
- package/dist/adapters/account.js.map +1 -0
- package/dist/adapters/claude.d.ts +49 -0
- package/dist/adapters/claude.js +259 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.d.ts +64 -0
- package/dist/adapters/codex.js +350 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/opencode.d.ts +79 -0
- package/dist/adapters/opencode.js +338 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/adapters/types.d.ts +97 -0
- package/dist/adapters/types.js +20 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +450 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/service.d.ts +3 -0
- package/dist/commands/service.js +86 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/config.d.ts +67 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon.d.ts +57 -0
- package/dist/daemon.js +368 -0
- package/dist/daemon.js.map +1 -0
- package/dist/git/commits.d.ts +34 -0
- package/dist/git/commits.js +85 -0
- package/dist/git/commits.js.map +1 -0
- package/dist/git/repo.d.ts +36 -0
- package/dist/git/repo.js +141 -0
- package/dist/git/repo.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/privacy/mode.d.ts +11 -0
- package/dist/privacy/mode.js +17 -0
- package/dist/privacy/mode.js.map +1 -0
- package/dist/privacy/paths.d.ts +11 -0
- package/dist/privacy/paths.js +30 -0
- package/dist/privacy/paths.js.map +1 -0
- package/dist/privacy/pipeline.d.ts +26 -0
- package/dist/privacy/pipeline.js +112 -0
- package/dist/privacy/pipeline.js.map +1 -0
- package/dist/privacy/redact.d.ts +29 -0
- package/dist/privacy/redact.js +58 -0
- package/dist/privacy/redact.js.map +1 -0
- package/dist/queue/spool.d.ts +47 -0
- package/dist/queue/spool.js +152 -0
- package/dist/queue/spool.js.map +1 -0
- package/dist/queue/tailer.d.ts +25 -0
- package/dist/queue/tailer.js +48 -0
- package/dist/queue/tailer.js.map +1 -0
- package/dist/schema.d.ts +76 -0
- package/dist/schema.js +46 -0
- package/dist/schema.js.map +1 -0
- package/dist/sessions/title.d.ts +2 -0
- package/dist/sessions/title.js +11 -0
- package/dist/sessions/title.js.map +1 -0
- package/dist/transport/client.d.ts +76 -0
- package/dist/transport/client.js +80 -0
- package/dist/transport/client.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EventEnvelope } from '../schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Durable local spool.
|
|
4
|
+
*
|
|
5
|
+
* Everything the collector observes lands here first and is only deleted once
|
|
6
|
+
* the server has acknowledged it. That is what makes the collector survive
|
|
7
|
+
* being offline, killed mid-flight, or pointed at an API that is down — a
|
|
8
|
+
* developer's day of work is never lost to a network blip.
|
|
9
|
+
*
|
|
10
|
+
* Also stores per-file read offsets so a restart resumes mid-file instead of
|
|
11
|
+
* re-uploading or skipping.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Spool {
|
|
14
|
+
private readonly db;
|
|
15
|
+
constructor(path: string);
|
|
16
|
+
private migrate;
|
|
17
|
+
enqueue(events: EventEnvelope[]): number;
|
|
18
|
+
/** Oldest-first so a backlog drains in the order it happened. */
|
|
19
|
+
peek(limit: number): {
|
|
20
|
+
eventId: string;
|
|
21
|
+
event: EventEnvelope;
|
|
22
|
+
}[];
|
|
23
|
+
ack(eventIds: string[]): void;
|
|
24
|
+
/**
|
|
25
|
+
* Records a failed attempt so poison events can be dropped eventually.
|
|
26
|
+
*
|
|
27
|
+
* Two things this must not do, both of which it used to:
|
|
28
|
+
* - delete across the whole table. Only the events in THIS batch have just
|
|
29
|
+
* failed; an untouched event elsewhere in the spool is not poison.
|
|
30
|
+
* - honour maxAttempts <= 0. `attempts >= 0` matches every row including
|
|
31
|
+
* never-attempted ones, so a config of 0 wiped the entire queue on the
|
|
32
|
+
* first network blip. At least one attempt must always be allowed.
|
|
33
|
+
*/
|
|
34
|
+
fail(eventIds: string[], maxAttempts: number): number;
|
|
35
|
+
depth(): number;
|
|
36
|
+
getCheckpoint(path: string): {
|
|
37
|
+
inode: string;
|
|
38
|
+
offset: number;
|
|
39
|
+
size: number;
|
|
40
|
+
} | null;
|
|
41
|
+
setCheckpoint(path: string, inode: string, offset: number, size: number): void;
|
|
42
|
+
getMeta(key: string): string | null;
|
|
43
|
+
setMeta(key: string, value: string): void;
|
|
44
|
+
/** Stable per-install id, generated once. */
|
|
45
|
+
installId(): string;
|
|
46
|
+
close(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { chmodSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Durable local spool.
|
|
7
|
+
*
|
|
8
|
+
* Everything the collector observes lands here first and is only deleted once
|
|
9
|
+
* the server has acknowledged it. That is what makes the collector survive
|
|
10
|
+
* being offline, killed mid-flight, or pointed at an API that is down — a
|
|
11
|
+
* developer's day of work is never lost to a network blip.
|
|
12
|
+
*
|
|
13
|
+
* Also stores per-file read offsets so a restart resumes mid-file instead of
|
|
14
|
+
* re-uploading or skipping.
|
|
15
|
+
*/
|
|
16
|
+
export class Spool {
|
|
17
|
+
db;
|
|
18
|
+
constructor(path) {
|
|
19
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
20
|
+
this.db = new Database(path);
|
|
21
|
+
// The spool holds un-uploaded telemetry; default umask makes it world-readable.
|
|
22
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
23
|
+
try {
|
|
24
|
+
chmodSync(`${path}${suffix}`, 0o600);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// WAL/SHM may not exist yet; the main db is the one that matters.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// WAL so a crash mid-write cannot corrupt the queue.
|
|
31
|
+
this.db.pragma('journal_mode = WAL');
|
|
32
|
+
this.db.pragma('synchronous = NORMAL');
|
|
33
|
+
this.migrate();
|
|
34
|
+
}
|
|
35
|
+
migrate() {
|
|
36
|
+
this.db.exec(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
38
|
+
event_id TEXT PRIMARY KEY,
|
|
39
|
+
body TEXT NOT NULL,
|
|
40
|
+
created_at INTEGER NOT NULL,
|
|
41
|
+
attempts INTEGER NOT NULL DEFAULT 0
|
|
42
|
+
);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
46
|
+
path TEXT PRIMARY KEY,
|
|
47
|
+
inode TEXT NOT NULL,
|
|
48
|
+
offset INTEGER NOT NULL,
|
|
49
|
+
size INTEGER NOT NULL
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
53
|
+
key TEXT PRIMARY KEY,
|
|
54
|
+
value TEXT NOT NULL
|
|
55
|
+
);
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
enqueue(events) {
|
|
59
|
+
if (events.length === 0)
|
|
60
|
+
return 0;
|
|
61
|
+
const stmt = this.db.prepare('INSERT OR IGNORE INTO events (event_id, body, created_at) VALUES (?, ?, ?)');
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const insertAll = this.db.transaction((batch) => {
|
|
64
|
+
let written = 0;
|
|
65
|
+
for (const event of batch)
|
|
66
|
+
written += stmt.run(event.event_id, JSON.stringify(event), now).changes;
|
|
67
|
+
return written;
|
|
68
|
+
});
|
|
69
|
+
return insertAll(events);
|
|
70
|
+
}
|
|
71
|
+
/** Oldest-first so a backlog drains in the order it happened. */
|
|
72
|
+
peek(limit) {
|
|
73
|
+
const rows = this.db
|
|
74
|
+
.prepare('SELECT event_id, body FROM events ORDER BY created_at ASC, rowid ASC LIMIT ?')
|
|
75
|
+
.all(limit);
|
|
76
|
+
return rows.flatMap((row) => {
|
|
77
|
+
try {
|
|
78
|
+
return [{ eventId: row.event_id, event: JSON.parse(row.body) }];
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Unparseable row would block the queue forever; drop it.
|
|
82
|
+
this.ack([row.event_id]);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
ack(eventIds) {
|
|
88
|
+
if (eventIds.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
const stmt = this.db.prepare('DELETE FROM events WHERE event_id = ?');
|
|
91
|
+
this.db.transaction((ids) => ids.forEach((id) => stmt.run(id)))(eventIds);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Records a failed attempt so poison events can be dropped eventually.
|
|
95
|
+
*
|
|
96
|
+
* Two things this must not do, both of which it used to:
|
|
97
|
+
* - delete across the whole table. Only the events in THIS batch have just
|
|
98
|
+
* failed; an untouched event elsewhere in the spool is not poison.
|
|
99
|
+
* - honour maxAttempts <= 0. `attempts >= 0` matches every row including
|
|
100
|
+
* never-attempted ones, so a config of 0 wiped the entire queue on the
|
|
101
|
+
* first network blip. At least one attempt must always be allowed.
|
|
102
|
+
*/
|
|
103
|
+
fail(eventIds, maxAttempts) {
|
|
104
|
+
if (eventIds.length === 0)
|
|
105
|
+
return 0;
|
|
106
|
+
const limit = Math.max(1, maxAttempts);
|
|
107
|
+
const bump = this.db.prepare('UPDATE events SET attempts = attempts + 1 WHERE event_id = ?');
|
|
108
|
+
const drop = this.db.prepare('DELETE FROM events WHERE event_id = ? AND attempts >= ?');
|
|
109
|
+
return this.db.transaction((ids) => {
|
|
110
|
+
let dropped = 0;
|
|
111
|
+
for (const id of ids) {
|
|
112
|
+
bump.run(id);
|
|
113
|
+
dropped += drop.run(id, limit).changes;
|
|
114
|
+
}
|
|
115
|
+
return dropped;
|
|
116
|
+
})(eventIds);
|
|
117
|
+
}
|
|
118
|
+
depth() {
|
|
119
|
+
return this.db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
|
|
120
|
+
}
|
|
121
|
+
getCheckpoint(path) {
|
|
122
|
+
return (this.db.prepare('SELECT inode, offset, size FROM checkpoints WHERE path = ?').get(path) ?? null);
|
|
123
|
+
}
|
|
124
|
+
setCheckpoint(path, inode, offset, size) {
|
|
125
|
+
this.db
|
|
126
|
+
.prepare(`INSERT INTO checkpoints (path, inode, offset, size) VALUES (?, ?, ?, ?)
|
|
127
|
+
ON CONFLICT(path) DO UPDATE SET inode = excluded.inode, offset = excluded.offset, size = excluded.size`)
|
|
128
|
+
.run(path, inode, offset, size);
|
|
129
|
+
}
|
|
130
|
+
getMeta(key) {
|
|
131
|
+
return (this.db.prepare('SELECT value FROM meta WHERE key = ?').get(key)?.value ??
|
|
132
|
+
null);
|
|
133
|
+
}
|
|
134
|
+
setMeta(key, value) {
|
|
135
|
+
this.db
|
|
136
|
+
.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value')
|
|
137
|
+
.run(key, value);
|
|
138
|
+
}
|
|
139
|
+
/** Stable per-install id, generated once. */
|
|
140
|
+
installId() {
|
|
141
|
+
let id = this.getMeta('install_id');
|
|
142
|
+
if (!id) {
|
|
143
|
+
id = randomUUID();
|
|
144
|
+
this.setMeta('install_id', id);
|
|
145
|
+
}
|
|
146
|
+
return id;
|
|
147
|
+
}
|
|
148
|
+
close() {
|
|
149
|
+
this.db.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=spool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spool.js","sourceRoot":"","sources":["../../src/queue/spool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Spool } from './spool.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resumable line tailer.
|
|
4
|
+
*
|
|
5
|
+
* Checkpoints (path, inode, offset) after every read so a restart continues
|
|
6
|
+
* exactly where it stopped. Three failure modes it must survive:
|
|
7
|
+
*
|
|
8
|
+
* - rotation/replacement: the inode changes, so the old offset is meaningless
|
|
9
|
+
* and we start from zero.
|
|
10
|
+
* - truncation: the file is smaller than our offset, so the offset is past
|
|
11
|
+
* the end and we start from zero.
|
|
12
|
+
* - a partial trailing line: the agent is mid-write. We must NOT consume it —
|
|
13
|
+
* emitting half a JSON object loses that event permanently, because the
|
|
14
|
+
* remainder arrives on the next pass as an orphaned fragment and both
|
|
15
|
+
* halves fail to parse. So the checkpoint stops at the last complete
|
|
16
|
+
* newline and the partial line is re-read whole next time.
|
|
17
|
+
*
|
|
18
|
+
* Offsets are byte offsets, computed from the buffer rather than from decoded
|
|
19
|
+
* strings — a multi-byte character would otherwise desynchronise the position.
|
|
20
|
+
*/
|
|
21
|
+
export interface TailResult {
|
|
22
|
+
lines: string[];
|
|
23
|
+
bytesRead: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function tailFile(path: string, spool: Spool): Promise<TailResult>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
const NEWLINE = 0x0a;
|
|
4
|
+
export async function tailFile(path, spool) {
|
|
5
|
+
if (!existsSync(path))
|
|
6
|
+
return { lines: [], bytesRead: 0 };
|
|
7
|
+
const stats = statSync(path);
|
|
8
|
+
const inode = String(stats.ino);
|
|
9
|
+
const checkpoint = spool.getCheckpoint(path);
|
|
10
|
+
let start = 0;
|
|
11
|
+
if (checkpoint && checkpoint.inode === inode && checkpoint.offset <= stats.size) {
|
|
12
|
+
start = checkpoint.offset;
|
|
13
|
+
}
|
|
14
|
+
// else: rotated (inode differs) or truncated (offset > size) — reread from 0.
|
|
15
|
+
if (start >= stats.size) {
|
|
16
|
+
spool.setCheckpoint(path, inode, stats.size, stats.size);
|
|
17
|
+
return { lines: [], bytesRead: 0 };
|
|
18
|
+
}
|
|
19
|
+
const length = stats.size - start;
|
|
20
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
21
|
+
const handle = await open(path, 'r');
|
|
22
|
+
try {
|
|
23
|
+
await handle.read(buffer, 0, length, start);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await handle.close();
|
|
27
|
+
}
|
|
28
|
+
// Consume only up to the final newline. Anything after it is a line the
|
|
29
|
+
// agent has not finished writing.
|
|
30
|
+
const lastNewline = buffer.lastIndexOf(NEWLINE);
|
|
31
|
+
if (lastNewline === -1) {
|
|
32
|
+
// No complete line yet — leave the checkpoint untouched so the whole
|
|
33
|
+
// partial line is re-read once it is terminated.
|
|
34
|
+
return { lines: [], bytesRead: 0 };
|
|
35
|
+
}
|
|
36
|
+
const consumable = buffer.subarray(0, lastNewline + 1);
|
|
37
|
+
const lines = consumable
|
|
38
|
+
.toString('utf8')
|
|
39
|
+
.split('\n')
|
|
40
|
+
// The trailing element after the final \n is always '' — drop it, and drop
|
|
41
|
+
// any blank lines rather than handing '' to a parser.
|
|
42
|
+
.filter((line) => line.length > 0)
|
|
43
|
+
.map((line) => (line.endsWith('\r') ? line.slice(0, -1) : line));
|
|
44
|
+
const consumed = start + consumable.length;
|
|
45
|
+
spool.setCheckpoint(path, inode, consumed, stats.size);
|
|
46
|
+
return { lines, bytesRead: consumable.length };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=tailer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tailer.js","sourceRoot":"","sources":["../../src/queue/tailer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AA2BxC,MAAM,OAAO,GAAG,IAAI,CAAC;AAErB,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,KAAY;IACvD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IAE1D,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAE7C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,UAAU,IAAI,UAAU,CAAC,KAAK,KAAK,KAAK,IAAI,UAAU,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAChF,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC;IAC5B,CAAC;IACD,8EAA8E;IAE9E,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACzD,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACrC,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAED,wEAAwE;IACxE,kCAAkC;IAClC,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAChD,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,qEAAqE;QACrE,iDAAiD;QACjD,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACrC,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,UAAU;SACrB,QAAQ,CAAC,MAAM,CAAC;SAChB,KAAK,CAAC,IAAI,CAAC;QACZ,2EAA2E;QAC3E,sDAAsD;SACrD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;SACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEnE,MAAM,QAAQ,GAAG,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC;IAC3C,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACvD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;AACjD,CAAC"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* AgentsTrack telemetry envelope, schema v1.
|
|
4
|
+
*
|
|
5
|
+
* This file is the collector's copy of the wire contract, documented in
|
|
6
|
+
* docs/EVENT_SCHEMA.md. The server holds the canonical EVENT_SCHEMA.json and
|
|
7
|
+
* validates against the same shape; `npm test` includes a contract test that
|
|
8
|
+
* asserts the two agree whenever the server repo is checked out alongside
|
|
9
|
+
* this one (or AGENTSTRACK_SERVER_REPO points at it).
|
|
10
|
+
*
|
|
11
|
+
* Note what is NOT here: organization_id and user_id. The server derives both
|
|
12
|
+
* from your API key. A collector cannot name its own tenant, by design.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SCHEMA_VERSION = 1;
|
|
15
|
+
export declare const AGENTS: readonly ["claude_code", "codex", "gemini_cli", "opencode", "cursor", "cline", "copilot_cli", "other"];
|
|
16
|
+
export type AgentId = (typeof AGENTS)[number];
|
|
17
|
+
export declare const EVENT_TYPES: readonly ["session.started", "session.ended", "user.prompted", "agent.turn.started", "agent.turn.ended", "model.request", "model.response", "tool.started", "tool.completed", "tool.failed", "file.read", "file.changed", "command.executed", "git.commit", "git.branch_changed", "usage.reported", "error", "heartbeat"];
|
|
18
|
+
export type EventType = (typeof EVENT_TYPES)[number];
|
|
19
|
+
export declare const PRIVACY_MODES: readonly ["metadata", "analytics", "full"];
|
|
20
|
+
export type PrivacyMode = (typeof PRIVACY_MODES)[number];
|
|
21
|
+
export interface TokenUsage {
|
|
22
|
+
input_tokens: number;
|
|
23
|
+
cached_input_tokens: number;
|
|
24
|
+
cache_creation_input_tokens: number;
|
|
25
|
+
output_tokens: number;
|
|
26
|
+
reasoning_output_tokens: number;
|
|
27
|
+
}
|
|
28
|
+
export declare const emptyUsage: () => TokenUsage;
|
|
29
|
+
export declare const EventEnvelope: z.ZodObject<{
|
|
30
|
+
event_id: z.ZodString;
|
|
31
|
+
schema_version: z.ZodLiteral<1>;
|
|
32
|
+
occurred_at: z.ZodString;
|
|
33
|
+
collector_id: z.ZodString;
|
|
34
|
+
session_id: z.ZodString;
|
|
35
|
+
agent: z.ZodEnum<{
|
|
36
|
+
claude_code: "claude_code";
|
|
37
|
+
codex: "codex";
|
|
38
|
+
gemini_cli: "gemini_cli";
|
|
39
|
+
opencode: "opencode";
|
|
40
|
+
cursor: "cursor";
|
|
41
|
+
cline: "cline";
|
|
42
|
+
copilot_cli: "copilot_cli";
|
|
43
|
+
other: "other";
|
|
44
|
+
}>;
|
|
45
|
+
agent_version: z.ZodOptional<z.ZodString>;
|
|
46
|
+
event_type: z.ZodEnum<{
|
|
47
|
+
"session.started": "session.started";
|
|
48
|
+
"session.ended": "session.ended";
|
|
49
|
+
"user.prompted": "user.prompted";
|
|
50
|
+
"agent.turn.started": "agent.turn.started";
|
|
51
|
+
"agent.turn.ended": "agent.turn.ended";
|
|
52
|
+
"model.request": "model.request";
|
|
53
|
+
"model.response": "model.response";
|
|
54
|
+
"tool.started": "tool.started";
|
|
55
|
+
"tool.completed": "tool.completed";
|
|
56
|
+
"tool.failed": "tool.failed";
|
|
57
|
+
"file.read": "file.read";
|
|
58
|
+
"file.changed": "file.changed";
|
|
59
|
+
"command.executed": "command.executed";
|
|
60
|
+
"git.commit": "git.commit";
|
|
61
|
+
"git.branch_changed": "git.branch_changed";
|
|
62
|
+
"usage.reported": "usage.reported";
|
|
63
|
+
error: "error";
|
|
64
|
+
heartbeat: "heartbeat";
|
|
65
|
+
}>;
|
|
66
|
+
payload: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
export type EventEnvelope = z.infer<typeof EventEnvelope>;
|
|
69
|
+
export interface RepoContext {
|
|
70
|
+
remote_hash?: string;
|
|
71
|
+
remote_owner?: string;
|
|
72
|
+
remote_name?: string;
|
|
73
|
+
branch?: string;
|
|
74
|
+
project_path?: string;
|
|
75
|
+
project_name?: string;
|
|
76
|
+
}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* AgentsTrack telemetry envelope, schema v1.
|
|
4
|
+
*
|
|
5
|
+
* This file is the collector's copy of the wire contract, documented in
|
|
6
|
+
* docs/EVENT_SCHEMA.md. The server holds the canonical EVENT_SCHEMA.json and
|
|
7
|
+
* validates against the same shape; `npm test` includes a contract test that
|
|
8
|
+
* asserts the two agree whenever the server repo is checked out alongside
|
|
9
|
+
* this one (or AGENTSTRACK_SERVER_REPO points at it).
|
|
10
|
+
*
|
|
11
|
+
* Note what is NOT here: organization_id and user_id. The server derives both
|
|
12
|
+
* from your API key. A collector cannot name its own tenant, by design.
|
|
13
|
+
*/
|
|
14
|
+
export const SCHEMA_VERSION = 1;
|
|
15
|
+
export const AGENTS = ['claude_code', 'codex', 'gemini_cli', 'opencode', 'cursor', 'cline', 'copilot_cli', 'other'];
|
|
16
|
+
export const EVENT_TYPES = [
|
|
17
|
+
'session.started', 'session.ended', 'user.prompted',
|
|
18
|
+
'agent.turn.started', 'agent.turn.ended',
|
|
19
|
+
'model.request', 'model.response',
|
|
20
|
+
'tool.started', 'tool.completed', 'tool.failed',
|
|
21
|
+
'file.read', 'file.changed', 'command.executed',
|
|
22
|
+
'git.commit', 'git.branch_changed',
|
|
23
|
+
'usage.reported', 'error', 'heartbeat',
|
|
24
|
+
];
|
|
25
|
+
export const PRIVACY_MODES = ['metadata', 'analytics', 'full'];
|
|
26
|
+
export const emptyUsage = () => ({
|
|
27
|
+
input_tokens: 0,
|
|
28
|
+
cached_input_tokens: 0,
|
|
29
|
+
cache_creation_input_tokens: 0,
|
|
30
|
+
output_tokens: 0,
|
|
31
|
+
reasoning_output_tokens: 0,
|
|
32
|
+
});
|
|
33
|
+
export const EventEnvelope = z.object({
|
|
34
|
+
event_id: z.string().uuid(),
|
|
35
|
+
schema_version: z.literal(SCHEMA_VERSION),
|
|
36
|
+
occurred_at: z.string(),
|
|
37
|
+
collector_id: z.string().uuid(),
|
|
38
|
+
session_id: z.string().min(1).max(200),
|
|
39
|
+
agent: z.enum(AGENTS),
|
|
40
|
+
agent_version: z.string().max(50).optional(),
|
|
41
|
+
event_type: z.enum(EVENT_TYPES),
|
|
42
|
+
// zod 4 requires an explicit key schema for z.record(); string keys is the
|
|
43
|
+
// same runtime behaviour z.record(z.unknown()) had under zod 3.
|
|
44
|
+
payload: z.record(z.string(), z.unknown()).default({}),
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,CAAU,CAAC;AAG7H,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,iBAAiB,EAAE,eAAe,EAAE,eAAe;IACnD,oBAAoB,EAAE,kBAAkB;IACxC,eAAe,EAAE,gBAAgB;IACjC,cAAc,EAAE,gBAAgB,EAAE,aAAa;IAC/C,WAAW,EAAE,cAAc,EAAE,kBAAkB;IAC/C,YAAY,EAAE,oBAAoB;IAClC,gBAAgB,EAAE,OAAO,EAAE,WAAW;CAC9B,CAAC;AAGX,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,MAAM,CAAU,CAAC;AAWxE,MAAM,CAAC,MAAM,UAAU,GAAG,GAAe,EAAE,CAAC,CAAC;IAC3C,YAAY,EAAE,CAAC;IACf,mBAAmB,EAAE,CAAC;IACtB,2BAA2B,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC;IAChB,uBAAuB,EAAE,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC3B,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC;IACzC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;IACvB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC/B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACtC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;IACrB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC5C,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;IAC/B,2EAA2E;IAC3E,gEAAgE;IAChE,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CACvD,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** First meaningful line of a prompt, trimmed to a usable title length. */
|
|
2
|
+
export function deriveTitle(text, maxLength = 120) {
|
|
3
|
+
const line = text
|
|
4
|
+
.split('\n')
|
|
5
|
+
.map((l) => l.trim())
|
|
6
|
+
// Skip markdown fences, quotes and system-reminder noise.
|
|
7
|
+
.find((l) => l.length > 0 && !l.startsWith('```') && !l.startsWith('<') && !l.startsWith('>')) ?? '';
|
|
8
|
+
const cleaned = line.replace(/\s+/g, ' ').trim();
|
|
9
|
+
return cleaned.length <= maxLength ? cleaned : `${cleaned.slice(0, maxLength - 1)}…`;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=title.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"title.js","sourceRoot":"","sources":["../../src/sessions/title.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,SAAS,GAAG,GAAG;IACvD,MAAM,IAAI,GACR,IAAI;SACD,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACrB,0DAA0D;SACzD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACzG,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC;AACvF,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { EventEnvelope } from '../schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* HTTP client for the AgentsTrack API.
|
|
4
|
+
*
|
|
5
|
+
* Bodies are gzipped: a session's events compress by roughly 10x and a
|
|
6
|
+
* developer on a hotel connection should not notice the collector at all.
|
|
7
|
+
*/
|
|
8
|
+
export interface ClientOptions {
|
|
9
|
+
apiUrl: string;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BatchResult {
|
|
14
|
+
accepted: number;
|
|
15
|
+
duplicates: number;
|
|
16
|
+
rejected: {
|
|
17
|
+
index: number;
|
|
18
|
+
reason: string;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
export interface ServerConfig {
|
|
22
|
+
privacy_mode: 'metadata' | 'analytics' | 'full';
|
|
23
|
+
idle_timeout_seconds: number;
|
|
24
|
+
retention_days: number;
|
|
25
|
+
redaction_rules: {
|
|
26
|
+
pattern: string;
|
|
27
|
+
replacement: string;
|
|
28
|
+
}[];
|
|
29
|
+
max_batch_events: number;
|
|
30
|
+
batch_interval_seconds: number;
|
|
31
|
+
}
|
|
32
|
+
export declare class ApiError extends Error {
|
|
33
|
+
readonly status: number;
|
|
34
|
+
/** Whether trying again later could succeed. */
|
|
35
|
+
readonly retryable: boolean;
|
|
36
|
+
constructor(message: string, status: number,
|
|
37
|
+
/** Whether trying again later could succeed. */
|
|
38
|
+
retryable: boolean);
|
|
39
|
+
}
|
|
40
|
+
export declare class ApiClient {
|
|
41
|
+
private readonly options;
|
|
42
|
+
constructor(options: ClientOptions);
|
|
43
|
+
registerCollector(input: {
|
|
44
|
+
hostname: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
os?: string;
|
|
47
|
+
arch?: string;
|
|
48
|
+
version?: string;
|
|
49
|
+
/** The mode this device will actually enforce, which may be stricter than the org's. */
|
|
50
|
+
privacy_mode?: string;
|
|
51
|
+
agents: {
|
|
52
|
+
agent: string;
|
|
53
|
+
version?: string;
|
|
54
|
+
}[];
|
|
55
|
+
}): Promise<{
|
|
56
|
+
collector_id: string;
|
|
57
|
+
privacy_mode: string;
|
|
58
|
+
}>;
|
|
59
|
+
getConfig(): Promise<ServerConfig>;
|
|
60
|
+
sendBatch(events: EventEnvelope[]): Promise<BatchResult>;
|
|
61
|
+
health(input: {
|
|
62
|
+
collector_id: string;
|
|
63
|
+
queue_depth: number;
|
|
64
|
+
version?: string;
|
|
65
|
+
privacy_mode?: string;
|
|
66
|
+
agents: {
|
|
67
|
+
agent: string;
|
|
68
|
+
version?: string;
|
|
69
|
+
}[];
|
|
70
|
+
}): Promise<{
|
|
71
|
+
ok: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
private request;
|
|
74
|
+
}
|
|
75
|
+
/** Exponential backoff with jitter, capped. */
|
|
76
|
+
export declare function backoffMs(attempt: number, baseMs?: number, capMs?: number): number;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { gzipSync } from 'node:zlib';
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
retryable;
|
|
5
|
+
constructor(message, status,
|
|
6
|
+
/** Whether trying again later could succeed. */
|
|
7
|
+
retryable) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.retryable = retryable;
|
|
11
|
+
this.name = 'ApiError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class ApiClient {
|
|
15
|
+
options;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
async registerCollector(input) {
|
|
20
|
+
return this.request('POST', '/v1/collector/register', input);
|
|
21
|
+
}
|
|
22
|
+
async getConfig() {
|
|
23
|
+
return this.request('GET', '/v1/collector/config');
|
|
24
|
+
}
|
|
25
|
+
async sendBatch(events) {
|
|
26
|
+
return this.request('POST', '/v1/events/batch', { events }, true);
|
|
27
|
+
}
|
|
28
|
+
async health(input) {
|
|
29
|
+
return this.request('POST', '/v1/collector/health', input);
|
|
30
|
+
}
|
|
31
|
+
async request(method, path, body, compress = false) {
|
|
32
|
+
const url = `${this.options.apiUrl.replace(/\/$/, '')}${path}`;
|
|
33
|
+
const headers = {
|
|
34
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
35
|
+
accept: 'application/json',
|
|
36
|
+
};
|
|
37
|
+
let payload;
|
|
38
|
+
if (body !== undefined) {
|
|
39
|
+
headers['content-type'] = 'application/json';
|
|
40
|
+
const json = JSON.stringify(body);
|
|
41
|
+
if (compress && json.length > 1024) {
|
|
42
|
+
payload = gzipSync(json);
|
|
43
|
+
headers['content-encoding'] = 'gzip';
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
payload = json;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 30_000);
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url, { method, headers, body: payload, signal: controller.signal });
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const text = await response.text().catch(() => '');
|
|
55
|
+
// 4xx means we sent something wrong — retrying will not fix it, except
|
|
56
|
+
// 408/429 which are explicitly "try again".
|
|
57
|
+
const retryable = response.status >= 500 || response.status === 408 || response.status === 429;
|
|
58
|
+
throw new ApiError(`${method} ${path} failed: ${response.status} ${text.slice(0, 200)}`, response.status, retryable);
|
|
59
|
+
}
|
|
60
|
+
return (await response.json());
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof ApiError)
|
|
64
|
+
throw error;
|
|
65
|
+
// Network failure, DNS, timeout: always worth retrying.
|
|
66
|
+
throw new ApiError(error instanceof Error ? error.message : String(error), 0, true);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Exponential backoff with jitter, capped. */
|
|
74
|
+
export function backoffMs(attempt, baseMs = 1000, capMs = 300_000) {
|
|
75
|
+
const exponential = Math.min(capMs, baseMs * 2 ** attempt);
|
|
76
|
+
// Jitter prevents a fleet of collectors reconnecting in lockstep after an
|
|
77
|
+
// outage and immediately knocking the API over again.
|
|
78
|
+
return Math.round(exponential / 2 + Math.random() * (exponential / 2));
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/transport/client.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentstrack/collector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Privacy-first telemetry collector for AI coding agents — Claude Code, Codex, and more. Local-first, open source.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "agentstrack": "./dist/cli.js" },
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": ["dist", "README.md", "LICENSE", "CHANGELOG.md"],
|
|
11
|
+
"engines": { "node": ">=24" },
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude-code", "codex", "ai-coding", "telemetry", "analytics", "developer-tools",
|
|
14
|
+
"observability", "token-usage", "ai-cost-tracking", "openai", "anthropic"
|
|
15
|
+
],
|
|
16
|
+
"repository": { "type": "git", "url": "git+https://github.com/agentstrack/collector.git" },
|
|
17
|
+
"homepage": "https://agentstrack.ai",
|
|
18
|
+
"bugs": { "url": "https://github.com/agentstrack/collector/issues" },
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
21
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
26
|
+
"prepublishOnly": "npm run build && npm test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"better-sqlite3": "^13.0.3",
|
|
30
|
+
"chokidar": "^4.0.3",
|
|
31
|
+
"commander": "^12.1.0",
|
|
32
|
+
"picocolors": "^1.1.1",
|
|
33
|
+
"yaml": "^2.6.1",
|
|
34
|
+
"zod": "^4.4.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
38
|
+
"@types/node": "^24.0.0",
|
|
39
|
+
"typescript": "^5.7.2",
|
|
40
|
+
"vitest": "^2.1.8"
|
|
41
|
+
}
|
|
42
|
+
}
|