@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
package/dist/config.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parse, stringify } from 'yaml';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { PRIVACY_MODES } from './schema.js';
|
|
7
|
+
export const CONFIG_DIR = process.env['AGENTSTRACK_HOME'] ?? join(homedir(), '.agentstrack');
|
|
8
|
+
export const CONFIG_PATH = join(CONFIG_DIR, 'config.yaml');
|
|
9
|
+
export const SPOOL_PATH = join(CONFIG_DIR, 'spool.db');
|
|
10
|
+
export const LOG_PATH = join(CONFIG_DIR, 'collector.log');
|
|
11
|
+
export const PID_PATH = join(CONFIG_DIR, 'collector.pid');
|
|
12
|
+
/**
|
|
13
|
+
* On-disk config. Shape follows BLUEPRINT §9.4 so the documented example is
|
|
14
|
+
* literally valid.
|
|
15
|
+
*/
|
|
16
|
+
export const Config = z.object({
|
|
17
|
+
api_url: z.string().url().default('https://api.agentstrack.ai'),
|
|
18
|
+
/** Written by `agentstrack login`. File is chmod 600. */
|
|
19
|
+
api_key: z.string().optional(),
|
|
20
|
+
collector_id: z.string().uuid().optional(),
|
|
21
|
+
privacy: z
|
|
22
|
+
.object({
|
|
23
|
+
mode: z.enum(PRIVACY_MODES).default('analytics'),
|
|
24
|
+
prompts: z.enum(['never', 'local_summary_only', 'full']).default('local_summary_only'),
|
|
25
|
+
code_content: z.enum(['never', 'full']).default('never'),
|
|
26
|
+
file_paths: z.enum(['never', 'relative', 'absolute']).default('relative'),
|
|
27
|
+
shell_arguments: z.enum(['never', 'redact_secrets', 'full']).default('redact_secrets'),
|
|
28
|
+
excluded_projects: z.array(z.string()).default([]),
|
|
29
|
+
})
|
|
30
|
+
// zod 4: every key here already has its own .default(), so the object
|
|
31
|
+
// schema's *input* type is `{}` (all-optional) but its parsed type is
|
|
32
|
+
// fully populated — .default() now types against the input, .prefault()
|
|
33
|
+
// (pre-parse default) is the zod4 replacement for this shape.
|
|
34
|
+
.prefault({}),
|
|
35
|
+
tracking: z
|
|
36
|
+
.object({
|
|
37
|
+
idle_timeout_seconds: z.number().int().min(30).max(3600).default(120),
|
|
38
|
+
git_metadata: z.boolean().default(true),
|
|
39
|
+
process_metrics: z.boolean().default(true),
|
|
40
|
+
agents: z.array(z.string()).default(['claude_code', 'codex', 'opencode']),
|
|
41
|
+
})
|
|
42
|
+
.prefault({}),
|
|
43
|
+
upload: z
|
|
44
|
+
.object({
|
|
45
|
+
batch_size: z.number().int().min(1).max(500).default(100),
|
|
46
|
+
interval_seconds: z.number().int().min(5).max(600).default(30),
|
|
47
|
+
max_retries: z.number().int().min(0).max(20).default(8),
|
|
48
|
+
})
|
|
49
|
+
.prefault({}),
|
|
50
|
+
});
|
|
51
|
+
export function loadConfig() {
|
|
52
|
+
if (!existsSync(CONFIG_PATH))
|
|
53
|
+
return Config.parse({});
|
|
54
|
+
try {
|
|
55
|
+
return Config.parse(parse(readFileSync(CONFIG_PATH, 'utf8')) ?? {});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// A hand-edited config that no longer parses must not silently revert to
|
|
59
|
+
// defaults — that could quietly widen the privacy mode.
|
|
60
|
+
throw new Error(`Config at ${CONFIG_PATH} is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function saveConfig(config) {
|
|
64
|
+
ensureConfigDir();
|
|
65
|
+
writeFileSync(CONFIG_PATH, stringify(config), 'utf8');
|
|
66
|
+
// The file holds an API key.
|
|
67
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Creates ~/.agentstrack as owner-only.
|
|
71
|
+
*
|
|
72
|
+
* The directory holds an API key, a spool of telemetry and a log. At the
|
|
73
|
+
* default umask those are world-readable, which on a shared host hands every
|
|
74
|
+
* other user a developer's session history.
|
|
75
|
+
*/
|
|
76
|
+
export function ensureConfigDir() {
|
|
77
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
78
|
+
try {
|
|
79
|
+
chmodSync(CONFIG_DIR, 0o700);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Pre-existing directory owned by someone else — nothing useful to do.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function configExists() {
|
|
86
|
+
return existsSync(CONFIG_PATH);
|
|
87
|
+
}
|
|
88
|
+
/** Documented starting point, written on `agentstrack login`. */
|
|
89
|
+
export const DEFAULT_CONFIG_COMMENT = `# AgentsTrack collector configuration
|
|
90
|
+
# Docs: https://github.com/agentstrack/collector#configuration
|
|
91
|
+
#
|
|
92
|
+
# privacy.mode:
|
|
93
|
+
# metadata - counts and timings only; no titles, no prompts, no code
|
|
94
|
+
# analytics - adds locally generated session titles; raw content discarded
|
|
95
|
+
# full - uploads prompt text (opt-in, off by default)
|
|
96
|
+
`;
|
|
97
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { loadConfig, type Config } from './config.js';
|
|
2
|
+
import type { AgentAdapter } from './adapters/types.js';
|
|
3
|
+
export declare function log(message: string): void;
|
|
4
|
+
export declare function buildAdapters(config: Config): AgentAdapter[];
|
|
5
|
+
/**
|
|
6
|
+
* Whether this event may carry account attribution.
|
|
7
|
+
*
|
|
8
|
+
* The honest half is the timestamp. ~/.claude.json and account.json record who
|
|
9
|
+
* is signed in NOW and keep no history, so an event written before this
|
|
10
|
+
* collector started — a transcript already on disk at first run, or anything
|
|
11
|
+
* from a window when the daemon was down — cannot be attributed. It gets NO
|
|
12
|
+
* account rather than today's account, which would be a plausible-looking lie.
|
|
13
|
+
*/
|
|
14
|
+
export declare function attributable(eventType: string, occurredAt: string, liveSinceMs: number): boolean;
|
|
15
|
+
/** Recursively lists .jsonl files under a directory, newest first. */
|
|
16
|
+
export declare function listTranscripts(dir: string, maxAgeDays?: number): string[];
|
|
17
|
+
export declare class Collector {
|
|
18
|
+
private config;
|
|
19
|
+
private readonly spool;
|
|
20
|
+
private readonly client;
|
|
21
|
+
private readonly adapters;
|
|
22
|
+
/** Null when git metadata is switched off — then we never shell out to git. */
|
|
23
|
+
private readonly commitWatcher;
|
|
24
|
+
private serverConfig;
|
|
25
|
+
/**
|
|
26
|
+
* When this collector started, and therefore the earliest event it can
|
|
27
|
+
* honestly attribute to an account.
|
|
28
|
+
*
|
|
29
|
+
* Account files record only who is signed in NOW. A transcript that was
|
|
30
|
+
* already on disk when the collector first ran was written by whoever was
|
|
31
|
+
* signed in at the time, which we cannot know — so backfilled events carry
|
|
32
|
+
* NO account rather than today's account. Live events, written while we were
|
|
33
|
+
* watching, do carry one. The same rule covers a restart: the gap while the
|
|
34
|
+
* collector was down is backfill.
|
|
35
|
+
*/
|
|
36
|
+
private readonly liveSinceMs;
|
|
37
|
+
private uploadFailures;
|
|
38
|
+
/** Shrinks on 413, recovers on success. Never below 1. */
|
|
39
|
+
private batchSize;
|
|
40
|
+
private running;
|
|
41
|
+
constructor(config: Config);
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
stop(): void;
|
|
44
|
+
/** Registers this device once and remembers the id. */
|
|
45
|
+
private ensureRegistered;
|
|
46
|
+
private refreshServerConfig;
|
|
47
|
+
/** One pass over every tracked transcript file. */
|
|
48
|
+
private scan;
|
|
49
|
+
private enqueue;
|
|
50
|
+
/** Drains the spool, oldest first, until it is empty or the server pushes back. */
|
|
51
|
+
flush(): Promise<void>;
|
|
52
|
+
private reportHealth;
|
|
53
|
+
queueDepth(): number;
|
|
54
|
+
}
|
|
55
|
+
export declare const VERSION = "0.1.0";
|
|
56
|
+
export declare function errorMessage(error: unknown): string;
|
|
57
|
+
export { loadConfig };
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { appendFileSync } from 'node:fs';
|
|
3
|
+
import { hostname, arch, platform } from 'node:os';
|
|
4
|
+
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { loadConfig, saveConfig, SPOOL_PATH, LOG_PATH } from './config.js';
|
|
7
|
+
import { Spool } from './queue/spool.js';
|
|
8
|
+
import { tailFile } from './queue/tailer.js';
|
|
9
|
+
import { ApiClient, backoffMs } from './transport/client.js';
|
|
10
|
+
import { ClaudeCodeAdapter } from './adapters/claude.js';
|
|
11
|
+
import { CodexAdapter } from './adapters/codex.js';
|
|
12
|
+
import { OpenCodeAdapter } from './adapters/opencode.js';
|
|
13
|
+
import { applyPrivacy } from './privacy/pipeline.js';
|
|
14
|
+
import { clampPrivacyMode } from './privacy/mode.js';
|
|
15
|
+
import { isExcluded } from './privacy/paths.js';
|
|
16
|
+
import { describeRepo } from './git/repo.js';
|
|
17
|
+
import { GitCommitWatcher } from './git/commits.js';
|
|
18
|
+
import { SCHEMA_VERSION } from './schema.js';
|
|
19
|
+
export function log(message) {
|
|
20
|
+
const line = `${new Date().toISOString()} ${message}\n`;
|
|
21
|
+
try {
|
|
22
|
+
appendFileSync(LOG_PATH, line);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Logging must never take the collector down.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function buildAdapters(config) {
|
|
29
|
+
const all = [new ClaudeCodeAdapter(), new CodexAdapter(), new OpenCodeAdapter()];
|
|
30
|
+
return all.filter((a) => config.tracking.agents.includes(a.id));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Event types that carry account attribution.
|
|
34
|
+
*
|
|
35
|
+
* Session start pins the whole session to an account; a prompt pins the turn.
|
|
36
|
+
* Every other event inherits that on the server, so repeating it on each of
|
|
37
|
+
* them would only add bytes and more places for the identity to leak from.
|
|
38
|
+
*/
|
|
39
|
+
const ACCOUNT_EVENTS = new Set(['session.started', 'user.prompted']);
|
|
40
|
+
/**
|
|
41
|
+
* Whether this event may carry account attribution.
|
|
42
|
+
*
|
|
43
|
+
* The honest half is the timestamp. ~/.claude.json and account.json record who
|
|
44
|
+
* is signed in NOW and keep no history, so an event written before this
|
|
45
|
+
* collector started — a transcript already on disk at first run, or anything
|
|
46
|
+
* from a window when the daemon was down — cannot be attributed. It gets NO
|
|
47
|
+
* account rather than today's account, which would be a plausible-looking lie.
|
|
48
|
+
*/
|
|
49
|
+
export function attributable(eventType, occurredAt, liveSinceMs) {
|
|
50
|
+
if (!ACCOUNT_EVENTS.has(eventType))
|
|
51
|
+
return false;
|
|
52
|
+
const at = Date.parse(occurredAt);
|
|
53
|
+
return Number.isFinite(at) && at >= liveSinceMs;
|
|
54
|
+
}
|
|
55
|
+
/** Recursively lists .jsonl files under a directory, newest first. */
|
|
56
|
+
export function listTranscripts(dir, maxAgeDays = 7) {
|
|
57
|
+
const cutoff = Date.now() - maxAgeDays * 86_400_000;
|
|
58
|
+
const found = [];
|
|
59
|
+
const walk = (current, depth) => {
|
|
60
|
+
if (depth > 5 || !existsSync(current))
|
|
61
|
+
return;
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = readdirSync(current);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return; // unreadable directory is skipped, not fatal
|
|
68
|
+
}
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const full = join(current, entry);
|
|
71
|
+
let stats;
|
|
72
|
+
try {
|
|
73
|
+
stats = statSync(full);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (stats.isDirectory())
|
|
79
|
+
walk(full, depth + 1);
|
|
80
|
+
else if (entry.endsWith('.jsonl') && stats.mtimeMs >= cutoff) {
|
|
81
|
+
found.push({ path: full, mtime: stats.mtimeMs });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
walk(dir, 0);
|
|
86
|
+
return found.sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
|
|
87
|
+
}
|
|
88
|
+
export class Collector {
|
|
89
|
+
config;
|
|
90
|
+
spool;
|
|
91
|
+
client;
|
|
92
|
+
adapters;
|
|
93
|
+
/** Null when git metadata is switched off — then we never shell out to git. */
|
|
94
|
+
commitWatcher;
|
|
95
|
+
serverConfig = null;
|
|
96
|
+
/**
|
|
97
|
+
* When this collector started, and therefore the earliest event it can
|
|
98
|
+
* honestly attribute to an account.
|
|
99
|
+
*
|
|
100
|
+
* Account files record only who is signed in NOW. A transcript that was
|
|
101
|
+
* already on disk when the collector first ran was written by whoever was
|
|
102
|
+
* signed in at the time, which we cannot know — so backfilled events carry
|
|
103
|
+
* NO account rather than today's account. Live events, written while we were
|
|
104
|
+
* watching, do carry one. The same rule covers a restart: the gap while the
|
|
105
|
+
* collector was down is backfill.
|
|
106
|
+
*/
|
|
107
|
+
liveSinceMs = Date.now();
|
|
108
|
+
uploadFailures = 0;
|
|
109
|
+
/** Shrinks on 413, recovers on success. Never below 1. */
|
|
110
|
+
batchSize;
|
|
111
|
+
running = false;
|
|
112
|
+
constructor(config) {
|
|
113
|
+
this.config = config;
|
|
114
|
+
if (!config.api_key)
|
|
115
|
+
throw new Error('Not logged in. Run: agentstrack login <api-key>');
|
|
116
|
+
this.spool = new Spool(SPOOL_PATH);
|
|
117
|
+
this.client = new ApiClient({ apiUrl: config.api_url, apiKey: config.api_key });
|
|
118
|
+
this.adapters = buildAdapters(config);
|
|
119
|
+
this.commitWatcher = config.tracking.git_metadata ? new GitCommitWatcher() : null;
|
|
120
|
+
this.batchSize = config.upload.batch_size;
|
|
121
|
+
}
|
|
122
|
+
async start() {
|
|
123
|
+
this.running = true;
|
|
124
|
+
await this.ensureRegistered();
|
|
125
|
+
await this.refreshServerConfig();
|
|
126
|
+
log(`Collector started — agents: ${this.adapters.map((a) => a.id).join(', ')}`);
|
|
127
|
+
const scanInterval = 5_000;
|
|
128
|
+
const uploadInterval = this.config.upload.interval_seconds * 1000;
|
|
129
|
+
let lastUpload = 0;
|
|
130
|
+
let lastHealth = 0;
|
|
131
|
+
while (this.running) {
|
|
132
|
+
try {
|
|
133
|
+
await this.scan();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
log(`Scan error: ${errorMessage(error)}`);
|
|
137
|
+
}
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (now - lastUpload >= uploadInterval || this.spool.depth() >= this.batchSize) {
|
|
140
|
+
lastUpload = now;
|
|
141
|
+
await this.flush();
|
|
142
|
+
}
|
|
143
|
+
if (now - lastHealth >= 60_000) {
|
|
144
|
+
lastHealth = now;
|
|
145
|
+
await this.reportHealth();
|
|
146
|
+
}
|
|
147
|
+
await sleep(scanInterval);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
stop() {
|
|
151
|
+
this.running = false;
|
|
152
|
+
this.spool.close();
|
|
153
|
+
}
|
|
154
|
+
/** Registers this device once and remembers the id. */
|
|
155
|
+
async ensureRegistered() {
|
|
156
|
+
if (this.config.collector_id)
|
|
157
|
+
return;
|
|
158
|
+
const agents = await Promise.all(this.adapters.map(async (a) => ({ agent: a.id, version: (await a.detect()).version })));
|
|
159
|
+
const result = await this.client.registerCollector({
|
|
160
|
+
hostname: hostname(),
|
|
161
|
+
os: platform(),
|
|
162
|
+
arch: arch(),
|
|
163
|
+
version: VERSION,
|
|
164
|
+
// Report what this device enforces, so a session records the mode that
|
|
165
|
+
// actually applied rather than the org default.
|
|
166
|
+
privacy_mode: this.config.privacy.mode,
|
|
167
|
+
agents,
|
|
168
|
+
});
|
|
169
|
+
this.config = { ...this.config, collector_id: result.collector_id };
|
|
170
|
+
saveConfig(this.config);
|
|
171
|
+
log(`Registered collector ${result.collector_id}`);
|
|
172
|
+
}
|
|
173
|
+
async refreshServerConfig() {
|
|
174
|
+
try {
|
|
175
|
+
this.serverConfig = await this.client.getConfig();
|
|
176
|
+
// The org sets a ceiling; a stricter local mode is honoured, a looser one
|
|
177
|
+
// is not. Same rule as `login`, shared so the two cannot drift.
|
|
178
|
+
const effective = clampPrivacyMode(this.config.privacy.mode, this.serverConfig.privacy_mode);
|
|
179
|
+
if (effective !== this.config.privacy.mode) {
|
|
180
|
+
log(`Local privacy mode '${this.config.privacy.mode}' exceeds org policy '${this.serverConfig.privacy_mode}' — using org policy`);
|
|
181
|
+
this.config = { ...this.config, privacy: { ...this.config.privacy, mode: effective } };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
log(`Could not fetch server config, using local defaults: ${errorMessage(error)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** One pass over every tracked transcript file. */
|
|
189
|
+
async scan() {
|
|
190
|
+
const collectorId = this.config.collector_id;
|
|
191
|
+
if (!collectorId)
|
|
192
|
+
return;
|
|
193
|
+
for (const adapter of this.adapters) {
|
|
194
|
+
const detection = await adapter.detect();
|
|
195
|
+
if (!detection.installed)
|
|
196
|
+
continue;
|
|
197
|
+
// Read once per cycle, not once at boot: the user may have switched
|
|
198
|
+
// accounts since the last scan.
|
|
199
|
+
const account = adapter.account?.();
|
|
200
|
+
for (const watchPath of detection.watchPaths) {
|
|
201
|
+
for (const file of listTranscripts(watchPath)) {
|
|
202
|
+
const { lines } = await tailFile(file, this.spool);
|
|
203
|
+
if (lines.length === 0)
|
|
204
|
+
continue;
|
|
205
|
+
const normalized = [];
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
try {
|
|
208
|
+
normalized.push(...adapter.normalize(line, { collectorId, sourceFile: file }));
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// A parser bug on one line must not stop the whole file.
|
|
212
|
+
log(`normalize error in ${file}: ${errorMessage(error)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.commitWatcher?.observe(normalized);
|
|
216
|
+
this.enqueue(normalized, collectorId, account);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Database-backed agents have no lines to tail; they hand us events on
|
|
220
|
+
// the same cycle, under the same gating and the same privacy pipeline.
|
|
221
|
+
if (adapter.poll) {
|
|
222
|
+
try {
|
|
223
|
+
const polled = await adapter.poll({
|
|
224
|
+
collectorId,
|
|
225
|
+
getMeta: (key) => this.spool.getMeta(key),
|
|
226
|
+
setMeta: (key, value) => this.spool.setMeta(key, value),
|
|
227
|
+
});
|
|
228
|
+
this.commitWatcher?.observe(polled);
|
|
229
|
+
this.enqueue(polled, collectorId, account);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
log(`poll error in ${adapter.id}: ${errorMessage(error)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Commits are the strongest session -> PR signal (§13) and no agent logs
|
|
237
|
+
// them, so they are read from git itself — one `git log` per repo a session
|
|
238
|
+
// actually touched, once per scan cycle.
|
|
239
|
+
if (this.commitWatcher) {
|
|
240
|
+
try {
|
|
241
|
+
this.enqueue(await this.commitWatcher.poll(), collectorId);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
log(`Commit scan error: ${errorMessage(error)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
enqueue(normalized, collectorId, adapterAccount) {
|
|
249
|
+
const envelopes = [];
|
|
250
|
+
for (const item of normalized) {
|
|
251
|
+
const cwd = item.cwd;
|
|
252
|
+
// An excluded project never produces an event at all.
|
|
253
|
+
if (cwd && isExcluded(cwd, this.config.privacy.excluded_projects))
|
|
254
|
+
continue;
|
|
255
|
+
const repo = cwd && this.config.tracking.git_metadata ? describeRepo(cwd) : item.repo;
|
|
256
|
+
const payload = repo
|
|
257
|
+
? { ...item.event.payload, repo: { ...repo, ...(item.event.payload['repo'] ?? {}) } }
|
|
258
|
+
: { ...item.event.payload };
|
|
259
|
+
// The adapter's per-event identity wins: OpenCode knows which provider
|
|
260
|
+
// account a given session ran on, which adapter.account() cannot.
|
|
261
|
+
const account = item.account ?? adapterAccount;
|
|
262
|
+
// Plan type rides with the usage, not with the account block: the server
|
|
263
|
+
// reads it off the token-bearing event to decide cost basis. Without it a
|
|
264
|
+
// flat-rate subscriber's spend is labelled "estimated at API list rates",
|
|
265
|
+
// which reads as a bill they never received.
|
|
266
|
+
if (account?.planType &&
|
|
267
|
+
(item.event.event_type === 'usage.reported' || item.event.event_type === 'model.response')) {
|
|
268
|
+
payload['plan_type'] = account.planType;
|
|
269
|
+
}
|
|
270
|
+
if (account && attributable(item.event.event_type, item.event.occurred_at, this.liveSinceMs)) {
|
|
271
|
+
payload['account'] = account;
|
|
272
|
+
}
|
|
273
|
+
const envelope = {
|
|
274
|
+
...item.event,
|
|
275
|
+
payload,
|
|
276
|
+
event_id: randomUUID(),
|
|
277
|
+
schema_version: SCHEMA_VERSION,
|
|
278
|
+
collector_id: collectorId,
|
|
279
|
+
};
|
|
280
|
+
const { event } = applyPrivacy(envelope, {
|
|
281
|
+
config: this.config,
|
|
282
|
+
projectRoot: repo?.project_path,
|
|
283
|
+
orgRules: this.serverConfig?.redaction_rules,
|
|
284
|
+
});
|
|
285
|
+
envelopes.push(event);
|
|
286
|
+
}
|
|
287
|
+
const written = this.spool.enqueue(envelopes);
|
|
288
|
+
if (written > 0)
|
|
289
|
+
log(`Queued ${written} events (depth ${this.spool.depth()})`);
|
|
290
|
+
}
|
|
291
|
+
/** Drains the spool, oldest first, until it is empty or the server pushes back. */
|
|
292
|
+
async flush() {
|
|
293
|
+
for (;;) {
|
|
294
|
+
const batch = this.spool.peek(this.batchSize);
|
|
295
|
+
if (batch.length === 0) {
|
|
296
|
+
this.uploadFailures = 0;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const result = await this.client.sendBatch(batch.map((b) => b.event));
|
|
301
|
+
// Duplicates are acknowledged too — the server already has them.
|
|
302
|
+
this.spool.ack(batch.map((b) => b.eventId));
|
|
303
|
+
this.uploadFailures = 0;
|
|
304
|
+
// Creep back up after a shrink so one huge session does not permanently
|
|
305
|
+
// halve throughput.
|
|
306
|
+
if (this.batchSize < this.config.upload.batch_size) {
|
|
307
|
+
this.batchSize = Math.min(this.config.upload.batch_size, this.batchSize * 2);
|
|
308
|
+
}
|
|
309
|
+
if (result.rejected.length > 0) {
|
|
310
|
+
log(`Server rejected ${result.rejected.length} events: ${result.rejected[0]?.reason ?? ''}`);
|
|
311
|
+
}
|
|
312
|
+
log(`Uploaded ${result.accepted} events (${result.duplicates} duplicates)`);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
const status = error instanceof Error && 'status' in error ? Number(error.status) : 0;
|
|
316
|
+
// 413: the batch is too big for the server, but the events are fine.
|
|
317
|
+
// Halve and retry rather than treat real telemetry as poison.
|
|
318
|
+
if (status === 413 && this.batchSize > 1) {
|
|
319
|
+
this.batchSize = Math.max(1, Math.floor(this.batchSize / 2));
|
|
320
|
+
log(`Server rejected the batch as too large — reducing batch size to ${this.batchSize}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const retryable = error instanceof Error && 'retryable' in error ? Boolean(error.retryable) : true;
|
|
324
|
+
if (!retryable) {
|
|
325
|
+
// The server will never accept these; count strikes so a poison
|
|
326
|
+
// batch cannot block the queue indefinitely.
|
|
327
|
+
const dropped = this.spool.fail(batch.map((b) => b.eventId), this.config.upload.max_retries);
|
|
328
|
+
log(`Batch permanently rejected: ${errorMessage(error)}${dropped ? ` (dropped ${dropped})` : ''}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.uploadFailures += 1;
|
|
332
|
+
const wait = backoffMs(this.uploadFailures);
|
|
333
|
+
log(`Upload failed (attempt ${this.uploadFailures}), retrying in ${Math.round(wait / 1000)}s: ${errorMessage(error)}`);
|
|
334
|
+
await sleep(wait);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async reportHealth() {
|
|
340
|
+
if (!this.config.collector_id)
|
|
341
|
+
return;
|
|
342
|
+
try {
|
|
343
|
+
const agents = await Promise.all(this.adapters.map(async (a) => ({ agent: a.id, version: (await a.detect()).version })));
|
|
344
|
+
await this.client.health({
|
|
345
|
+
collector_id: this.config.collector_id,
|
|
346
|
+
queue_depth: this.spool.depth(),
|
|
347
|
+
version: VERSION,
|
|
348
|
+
privacy_mode: this.config.privacy.mode,
|
|
349
|
+
agents,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
log(`Health report failed: ${errorMessage(error)}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
queueDepth() {
|
|
357
|
+
return this.spool.depth();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
export const VERSION = '0.1.0';
|
|
361
|
+
function sleep(ms) {
|
|
362
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
363
|
+
}
|
|
364
|
+
export function errorMessage(error) {
|
|
365
|
+
return error instanceof Error ? error.message : String(error);
|
|
366
|
+
}
|
|
367
|
+
export { loadConfig };
|
|
368
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.js","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { NormalizedEvent } from '../adapters/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Emits `git.commit` for commits that land while a session is running.
|
|
4
|
+
*
|
|
5
|
+
* This is the only EXACT signal the server has for tying a session to a pull
|
|
6
|
+
* request (BLUEPRINT §13), and no agent writes it to its transcript — the
|
|
7
|
+
* commit is made by a shell command whose output we never see. So the
|
|
8
|
+
* collector watches the repositories a session actually touched and reports
|
|
9
|
+
* the SHAs that appear inside the session window.
|
|
10
|
+
*/
|
|
11
|
+
export interface WatchedRepo {
|
|
12
|
+
gitRoot: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
agent: NormalizedEvent['event']['agent'];
|
|
15
|
+
agentVersion?: string;
|
|
16
|
+
/** Earliest event seen for this session in this repo. */
|
|
17
|
+
since: Date;
|
|
18
|
+
}
|
|
19
|
+
export declare class GitCommitWatcher {
|
|
20
|
+
/** One entry per repo: the session most recently active in it. */
|
|
21
|
+
private readonly repos;
|
|
22
|
+
/** SHAs already emitted, so a commit is reported once and not once per scan. */
|
|
23
|
+
private readonly emitted;
|
|
24
|
+
/** cwd -> git root, so a repeated cwd costs no filesystem walk. */
|
|
25
|
+
private readonly roots;
|
|
26
|
+
/** Records which repo each session is working in. Cheap: no git process. */
|
|
27
|
+
observe(events: NormalizedEvent[]): void;
|
|
28
|
+
/**
|
|
29
|
+
* One `git log` per watched repo, at most once per call. Repos are dropped
|
|
30
|
+
* afterwards: a repo is re-armed by the next event from it, so an idle
|
|
31
|
+
* project costs nothing.
|
|
32
|
+
*/
|
|
33
|
+
poll(now?: Date): Promise<NormalizedEvent[]>;
|
|
34
|
+
}
|