@klars/agentobs 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.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Process-wrap adapter: `agentobs run -- <command...>`.
3
+ *
4
+ * The universal fallback. It works with any agent CLI - present or future,
5
+ * instrumented or not - because it observes the process rather than the
6
+ * agent. What it gets in exchange for that generality is coarse data:
7
+ * start, end, duration, exit code, and nothing about individual tool calls.
8
+ *
9
+ * That limit is recorded as fidelity='coarse' and shown in the dashboard, so
10
+ * a coarse session is never mistaken for a detailed one.
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { execSync } from 'node:child_process';
15
+ import { existsSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { createSink } from './sink.js';
18
+ /** Best-effort current git branch; null outside a repo. */
19
+ function currentBranch(cwd) {
20
+ try {
21
+ return execSync('git rev-parse --abbrev-ref HEAD', {
22
+ cwd,
23
+ stdio: ['ignore', 'pipe', 'ignore'],
24
+ encoding: 'utf8',
25
+ timeout: 1000,
26
+ }).trim();
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ /**
33
+ * Runs a command under observation, returning its exit code.
34
+ *
35
+ * stdio is inherited so the wrapped agent stays fully interactive - the user
36
+ * should not be able to tell AgentObs is in the middle. That is also why we
37
+ * never buffer or parse the output here.
38
+ */
39
+ export async function runWrapped(command, opts = {}) {
40
+ if (command.length === 0)
41
+ throw new Error('no command given to run');
42
+ const cwd = opts.cwd ?? process.cwd();
43
+ const agentName = opts.agentName ?? command[0].replace(/\.(exe|cmd|bat)$/i, '');
44
+ const sessionId = randomUUID();
45
+ const sink = createSink(agentName);
46
+ sink({
47
+ type: 'session_start',
48
+ sessionId,
49
+ agentName,
50
+ cwd,
51
+ gitBranch: currentBranch(cwd),
52
+ fidelity: 'coarse',
53
+ });
54
+ return new Promise((resolve) => {
55
+ // On Windows, resolve the executable ourselves rather than reaching for
56
+ // `shell: true`. Under a shell, arguments are concatenated instead of
57
+ // escaped (Node DEP0190), so any argument containing a space or a quote
58
+ // is silently mangled before the wrapped agent ever sees it - which for
59
+ // an agent CLI means a corrupted prompt.
60
+ //
61
+ // Resolving the .cmd/.bat shim directly keeps npm-installed CLIs working
62
+ // (`agentobs run -- claude`) while spawn still escapes each argument.
63
+ const executable = process.platform === 'win32' ? resolveWindowsExecutable(command[0]) : command[0];
64
+ const child = spawn(executable, command.slice(1), {
65
+ cwd,
66
+ stdio: 'inherit',
67
+ // A .cmd shim is a batch script, so it does need a shell interpreter -
68
+ // but only for the shim itself, and cmd.exe applies its own escaping.
69
+ shell: /\.(cmd|bat)$/i.test(executable),
70
+ windowsHide: true,
71
+ });
72
+ const finish = (code) => {
73
+ sink({ type: 'session_end', sessionId, exitCode: code });
74
+ resolve(code);
75
+ };
76
+ child.on('error', (err) => {
77
+ // Spawn failure (command not found) - record it as a failed session
78
+ // rather than losing the attempt entirely.
79
+ console.error(`[agentobs] failed to start ${command[0]}: ${err.message}`);
80
+ finish(127);
81
+ });
82
+ child.on('close', (code, signal) => {
83
+ // A signal-terminated process reports code=null; map it to the shell
84
+ // convention (128 + signal number) so the stored value is never null
85
+ // for a run that genuinely ended.
86
+ if (code === null && signal) {
87
+ finish(128 + (signalNumber(signal) ?? 0));
88
+ return;
89
+ }
90
+ finish(code ?? 0);
91
+ });
92
+ // Forward interrupts so Ctrl-C reaches the agent rather than only
93
+ // killing the wrapper and orphaning it.
94
+ for (const sig of ['SIGINT', 'SIGTERM']) {
95
+ process.on(sig, () => {
96
+ if (!child.killed)
97
+ child.kill(sig);
98
+ });
99
+ }
100
+ });
101
+ }
102
+ /**
103
+ * Finds the real file behind a bare command name on Windows.
104
+ *
105
+ * Windows has no execvp: `spawn("npm")` fails because the thing on PATH is
106
+ * `npm.cmd`, not `npm`. The usual workaround is `shell: true`, but that
107
+ * disables argument escaping (see the call site). Walking PATH + PATHEXT
108
+ * ourselves keeps both properties: bare names resolve, and arguments stay
109
+ * escaped.
110
+ *
111
+ * Falls back to the original name when nothing matches, so spawn produces its
112
+ * normal ENOENT rather than this function inventing a different failure.
113
+ */
114
+ function resolveWindowsExecutable(command) {
115
+ if (command.includes('/') || command.includes('\\'))
116
+ return command;
117
+ const exts = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
118
+ const dirs = (process.env.PATH ?? '').split(';').filter(Boolean);
119
+ for (const dir of dirs) {
120
+ for (const ext of ['', ...exts]) {
121
+ const candidate = join(dir, command + ext.toLowerCase());
122
+ if (existsSync(candidate))
123
+ return candidate;
124
+ const upper = join(dir, command + ext);
125
+ if (existsSync(upper))
126
+ return upper;
127
+ }
128
+ }
129
+ return command;
130
+ }
131
+ function signalNumber(signal) {
132
+ const table = {
133
+ SIGHUP: 1,
134
+ SIGINT: 2,
135
+ SIGQUIT: 3,
136
+ SIGKILL: 9,
137
+ SIGTERM: 15,
138
+ };
139
+ return table[signal] ?? null;
140
+ }
141
+ //# sourceMappingURL=process-wrap.js.map
@@ -0,0 +1,65 @@
1
+ import { openDb } from '../core/db.js';
2
+ import { beginToolCall, completeToolCall, endSession, ensureSession, startSession, } from '../core/repo.js';
3
+ export function applyEvent(db, event, agentName) {
4
+ switch (event.type) {
5
+ case 'session_start':
6
+ startSession(db, {
7
+ id: event.sessionId,
8
+ agentName: event.agentName ?? agentName,
9
+ cwd: event.cwd,
10
+ gitBranch: event.gitBranch,
11
+ fidelity: event.fidelity ?? 'rich',
12
+ startedAt: event.timestamp,
13
+ });
14
+ break;
15
+ case 'session_end':
16
+ endSession(db, event.sessionId, { exitCode: event.exitCode, endedAt: event.timestamp });
17
+ break;
18
+ case 'tool_call_start':
19
+ // The parent session may never have been announced (agent started
20
+ // before AgentObs was installed, or no SessionStart hook configured).
21
+ // Synthesising it beats dropping the tool call.
22
+ ensureSession(db, event.sessionId, agentName);
23
+ beginToolCall(db, {
24
+ id: event.toolCallId,
25
+ sessionId: event.sessionId,
26
+ toolName: event.toolName,
27
+ input: event.input,
28
+ model: event.model,
29
+ startedAt: event.timestamp,
30
+ });
31
+ break;
32
+ case 'tool_call_end':
33
+ completeToolCall(db, event.toolCallId, {
34
+ status: event.status,
35
+ output: event.output,
36
+ tokensIn: event.tokensIn,
37
+ tokensOut: event.tokensOut,
38
+ model: event.model,
39
+ errorMessage: event.errorMessage,
40
+ endedAt: event.timestamp,
41
+ });
42
+ break;
43
+ }
44
+ }
45
+ /**
46
+ * Creates a sink bound to the local database.
47
+ *
48
+ * Swallows and logs errors by design: an adapter runs inside the user's
49
+ * agent, so a logging failure must degrade to missing data, never to a
50
+ * crashed agent.
51
+ */
52
+ export function createSink(agentName, db) {
53
+ const handle = db ?? openDb();
54
+ return (event) => {
55
+ try {
56
+ applyEvent(handle, event, agentName);
57
+ }
58
+ catch (err) {
59
+ if (process.env.AGENTOBS_DEBUG) {
60
+ console.error(`[agentobs] failed to record ${event.type}:`, err);
61
+ }
62
+ }
63
+ };
64
+ }
65
+ //# sourceMappingURL=sink.js.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Token usage recovery from a Claude Code transcript.
3
+ *
4
+ * Claude Code's hook payloads carry no token or cost fields - verified
5
+ * against the hooks reference, and the reason nothing in the hook path ever
6
+ * sets a token count. The transcript JSONL the agent writes for itself does
7
+ * carry per-message `usage` blocks, so that is where real numbers come from.
8
+ *
9
+ * The trade-off this accepts: usage is per assistant *message*, not per tool
10
+ * call, so it is attributed to the session rather than split across the
11
+ * individual calls inside it. Session-level cost is therefore accurate;
12
+ * per-tool-call cost stays null for hook-sourced data rather than being
13
+ * fabricated by dividing a total.
14
+ */
15
+ import { createReadStream, existsSync } from 'node:fs';
16
+ import { createInterface } from 'node:readline';
17
+ import { computeCost } from '../core/pricing.js';
18
+ /**
19
+ * Sums the usage blocks in a transcript.
20
+ *
21
+ * Streams line-by-line rather than reading the file: a long session's
22
+ * transcript can be tens of megabytes, and this runs inside a SessionEnd hook
23
+ * with a tight time budget.
24
+ */
25
+ export async function readTranscriptUsage(file) {
26
+ const total = {
27
+ tokensIn: 0,
28
+ tokensOut: 0,
29
+ cacheReadTokens: 0,
30
+ cacheCreationTokens: 0,
31
+ model: null,
32
+ messages: 0,
33
+ };
34
+ if (!existsSync(file))
35
+ return total;
36
+ const rl = createInterface({
37
+ input: createReadStream(file, { encoding: 'utf8' }),
38
+ crlfDelay: Infinity,
39
+ });
40
+ for await (const line of rl) {
41
+ if (!line.trim())
42
+ continue;
43
+ let row;
44
+ try {
45
+ row = JSON.parse(line);
46
+ }
47
+ catch {
48
+ continue; // a partially-flushed final line is normal while tailing
49
+ }
50
+ // The shape has moved between versions, so check both the top level and
51
+ // a nested `message` object rather than assuming one layout.
52
+ const message = (row.message ?? row);
53
+ const usage = message.usage;
54
+ if (!usage || typeof usage !== 'object')
55
+ continue;
56
+ total.tokensIn += usage.input_tokens ?? 0;
57
+ total.tokensOut += usage.output_tokens ?? 0;
58
+ total.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
59
+ total.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
60
+ total.messages += 1;
61
+ const model = message.model;
62
+ if (typeof model === 'string' && model)
63
+ total.model = model;
64
+ }
65
+ return total;
66
+ }
67
+ /**
68
+ * Writes recovered usage onto the session row.
69
+ *
70
+ * Cost counts cache-read tokens at the full input rate, which slightly
71
+ * over-states spend for cache-heavy sessions. That direction is deliberate:
72
+ * pricing.json has no cache tier for most models, and over-reporting is the
73
+ * safer error for a spend figure. It is also why the number is only claimed
74
+ * as session-level.
75
+ */
76
+ export async function attachTranscriptUsage(db, sessionId, transcriptPath) {
77
+ const usage = await readTranscriptUsage(transcriptPath);
78
+ if (usage.messages === 0)
79
+ return usage;
80
+ const billableIn = usage.tokensIn + usage.cacheReadTokens + usage.cacheCreationTokens;
81
+ const cost = computeCost(usage.model, billableIn, usage.tokensOut);
82
+ db.prepare(`UPDATE sessions
83
+ SET total_tokens_in = ?, total_tokens_out = ?, total_cost_usd = ?,
84
+ updated_at = ?, synced_at = NULL
85
+ WHERE id = ?`).run(billableIn, usage.tokensOut, cost, new Date().toISOString(), sessionId);
86
+ return usage;
87
+ }
88
+ //# sourceMappingURL=transcript.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The adapter plugin interface.
3
+ *
4
+ * An adapter's job is to turn one agent's native output into AgentEvents.
5
+ * Everything downstream - storage, redaction, pricing, the dashboard - is
6
+ * shared, so a new agent integration only has to answer "how do I observe
7
+ * this tool?" and never "how do I store it?".
8
+ *
9
+ * See CONTRIBUTING.md for a worked example of adding one.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=types.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * AgentObs CLI.
3
+ *
4
+ * Command bodies live in ./commands/*; this file only wires up the grammar so
5
+ * `--help` stays the single readable map of what the tool does.
6
+ */
7
+ import { Command } from 'commander';
8
+ import { createRequire } from 'node:module';
9
+ const require = createRequire(import.meta.url);
10
+ const pkg = require('../package.json');
11
+ export function buildProgram() {
12
+ const program = new Command();
13
+ program
14
+ .name('agentobs')
15
+ .description('Observability and control for AI coding agents')
16
+ .version(pkg.version);
17
+ program
18
+ .command('init')
19
+ .description('Create ~/.agentobs, the database, and print the Claude Code hook config')
20
+ .option('--force', 'overwrite an existing pricing.json / policy.json', false)
21
+ .action(async (opts) => {
22
+ const { init } = await import('./commands/init.js');
23
+ await init(opts);
24
+ });
25
+ program
26
+ .command('dashboard')
27
+ .description('Serve the local dashboard')
28
+ .option('-p, --port <number>', 'port to listen on', '4300')
29
+ .option('--host <address>', 'address to bind (non-loopback requires a token)', '127.0.0.1')
30
+ .option('--token <value>', 'shared token required when binding a non-loopback address')
31
+ .option('--no-open', 'do not open a browser window')
32
+ .action(async (opts) => {
33
+ const { dashboard } = await import('./commands/dashboard.js');
34
+ await dashboard(opts);
35
+ });
36
+ program
37
+ .command('stats')
38
+ .description('Print usage totals')
39
+ .option('--today', 'today only')
40
+ .option('--since <range>', 'one of: today, 7d, 30d, all', '7d')
41
+ .option('--session <id>', 'restrict to one session')
42
+ .option('--json', 'emit JSON instead of a table', false)
43
+ .action(async (opts) => {
44
+ const { stats } = await import('./commands/stats.js');
45
+ await stats(opts);
46
+ });
47
+ program
48
+ .command('watch')
49
+ .argument('<file>', 'JSONL file to tail')
50
+ .description('Ingest a newline-delimited JSON agent log')
51
+ .option('--agent <name>', 'agent name to record', 'generic')
52
+ .option('--no-follow', 'process existing lines then exit')
53
+ .action(async (file, opts) => {
54
+ const { watch } = await import('./commands/watch.js');
55
+ await watch(file, opts);
56
+ });
57
+ program
58
+ .command('run')
59
+ .description('Run a command under observation (coarse: duration and exit code)')
60
+ .argument('<command...>', 'command to run, after --')
61
+ .option('--agent <name>', 'agent name to record')
62
+ .allowUnknownOption()
63
+ .action(async (command, opts) => {
64
+ const { run } = await import('./commands/run.js');
65
+ await run(command, opts);
66
+ });
67
+ program
68
+ .command('export')
69
+ .description('Export recorded data')
70
+ .requiredOption('--format <format>', 'csv or json')
71
+ .option('--out <path>', 'output file (defaults to stdout)')
72
+ .option('--table <name>', 'sessions, tool-calls, or policy-decisions', 'tool-calls')
73
+ .option('--since <range>', 'today, 7d, 30d, all', 'all')
74
+ .action(async (opts) => {
75
+ const { exportData } = await import('./commands/export.js');
76
+ await exportData(opts);
77
+ });
78
+ const policy = program.command('policy').description('Guardrail policy management');
79
+ policy
80
+ .command('init')
81
+ .description('Write a starter ~/.agentobs/policy.json')
82
+ .action(async () => {
83
+ const { policyInit } = await import('./commands/policy.js');
84
+ await policyInit();
85
+ });
86
+ policy
87
+ .command('check')
88
+ .description('Validate the policy file and list active rules')
89
+ .action(async () => {
90
+ const { policyCheck } = await import('./commands/policy.js');
91
+ await policyCheck();
92
+ });
93
+ policy
94
+ .command('test')
95
+ .description('Dry-run a hypothetical tool call against the policy')
96
+ .argument('<tool>', 'tool name, e.g. Bash')
97
+ .argument('<input...>', 'the command or path to test')
98
+ .action(async (tool, input) => {
99
+ const { policyTest } = await import('./commands/policy.js');
100
+ await policyTest(tool, input.join(' '));
101
+ });
102
+ return program;
103
+ }
104
+ export async function main(argv) {
105
+ const program = buildProgram();
106
+ await program.parseAsync(argv);
107
+ }
108
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `agentobs dashboard` - serves the local UI.
3
+ */
4
+ import { randomBytes } from 'node:crypto';
5
+ import { spawn } from 'node:child_process';
6
+ import { isLoopback, startDashboard } from '../server/index.js';
7
+ export async function dashboard(opts) {
8
+ const port = Number(opts.port) || 4300;
9
+ const host = opts.host || '127.0.0.1';
10
+ const loopback = isLoopback(host);
11
+ // Binding beyond loopback exposes tool inputs and file paths to the local
12
+ // network, so it always requires a token. One is minted rather than
13
+ // refusing outright, because sharing a dashboard on a trusted LAN is a
14
+ // legitimate thing to want.
15
+ const token = loopback ? null : (opts.token ?? randomBytes(16).toString('hex'));
16
+ const { port: actual } = await startDashboard({ port, host, token });
17
+ const url = `http://${loopback ? '127.0.0.1' : host}:${actual}${token ? `?token=${token}` : ''}`;
18
+ console.log(`AgentObs dashboard: ${url}`);
19
+ if (!loopback) {
20
+ console.log(`
21
+ Bound to ${host} - reachable from your network.
22
+ A token is required; it is already in the URL above.
23
+ Never expose this to the public internet.`);
24
+ }
25
+ console.log('\nPress Ctrl-C to stop.');
26
+ if (opts.open !== false)
27
+ openBrowser(url);
28
+ }
29
+ function openBrowser(url) {
30
+ const [cmd, args] = process.platform === 'win32'
31
+ ? ['cmd', ['/c', 'start', '', url]]
32
+ : process.platform === 'darwin'
33
+ ? ['open', [url]]
34
+ : ['xdg-open', [url]];
35
+ try {
36
+ // Detached and unref'd so the browser process never keeps the server
37
+ // alive, and a headless box without a browser fails silently rather than
38
+ // taking the dashboard down with it.
39
+ spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
40
+ }
41
+ catch {
42
+ /* no browser available - the URL is printed above */
43
+ }
44
+ }
45
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `agentobs export` - CSV/JSON extraction.
3
+ *
4
+ * Exported summaries are already redacted, because redaction happens on write
5
+ * rather than on read. An export can therefore be shared or attached to a
6
+ * ticket without a separate scrubbing step.
7
+ */
8
+ import { writeFileSync } from 'node:fs';
9
+ import { openDb } from '../core/db.js';
10
+ import { getPolicyDecisions, getRecentToolCalls, getSessions } from '../core/queries.js';
11
+ function toRange(value) {
12
+ return value === 'today' || value === '7d' || value === '30d' || value === 'all' ? value : 'all';
13
+ }
14
+ /**
15
+ * RFC 4180 CSV escaping.
16
+ *
17
+ * Tool inputs routinely contain commas, quotes and newlines, so every field is
18
+ * quoted and inner quotes doubled - a naive join would silently corrupt the
19
+ * column layout of any row containing a shell command.
20
+ */
21
+ function csvCell(value) {
22
+ if (value === null || value === undefined)
23
+ return '';
24
+ const str = String(value);
25
+ return `"${str.replace(/"/g, '""')}"`;
26
+ }
27
+ function toCsv(rows) {
28
+ if (rows.length === 0)
29
+ return '';
30
+ const headers = Object.keys(rows[0]);
31
+ const lines = [headers.join(',')];
32
+ for (const row of rows) {
33
+ lines.push(headers.map((h) => csvCell(row[h])).join(','));
34
+ }
35
+ return lines.join('\n') + '\n';
36
+ }
37
+ export async function exportData(opts) {
38
+ const format = opts.format.toLowerCase();
39
+ if (format !== 'csv' && format !== 'json') {
40
+ console.error(`Unsupported format "${opts.format}". Use csv or json.`);
41
+ process.exitCode = 2;
42
+ return;
43
+ }
44
+ const db = openDb();
45
+ const range = toRange(opts.since);
46
+ const table = opts.table ?? 'tool-calls';
47
+ let rows;
48
+ switch (table) {
49
+ case 'sessions':
50
+ rows = getSessions(db, { range, limit: 500 });
51
+ break;
52
+ case 'policy-decisions':
53
+ rows = getPolicyDecisions(db, { limit: 500 });
54
+ break;
55
+ case 'tool-calls':
56
+ rows = getRecentToolCalls(db, { range, limit: 500 });
57
+ break;
58
+ default:
59
+ console.error(`Unknown table "${table}". Use sessions, tool-calls, or policy-decisions.`);
60
+ process.exitCode = 2;
61
+ return;
62
+ }
63
+ const body = format === 'json' ? `${JSON.stringify(rows, null, 2)}\n` : toCsv(rows);
64
+ if (opts.out) {
65
+ writeFileSync(opts.out, body, 'utf8');
66
+ console.log(`Wrote ${rows.length} row(s) to ${opts.out}`);
67
+ }
68
+ else {
69
+ process.stdout.write(body);
70
+ }
71
+ }
72
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Generates the Claude Code hook settings block.
3
+ *
4
+ * Shape verified against Claude Code's hooks reference: a `hooks` object keyed
5
+ * by event name, each holding matcher groups whose `hooks` array carries
6
+ * `{ type: "command", command }`. The matcher is a pipe-separated list of
7
+ * literal tool names (or a regex); an empty matcher means "every tool".
8
+ *
9
+ * One binary serves every event and branches on `hook_event_name`, which keeps
10
+ * the user's settings file short and means an upgrade never requires them to
11
+ * re-paste a different set of commands.
12
+ */
13
+ import { dirname, join, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { existsSync } from 'node:fs';
16
+ /**
17
+ * Absolute path to the agentobs-hook entrypoint.
18
+ *
19
+ * An absolute path is used rather than the bare command name because Claude
20
+ * Code runs hooks with a non-login shell whose PATH often omits the npm global
21
+ * bin directory - the most common reason a hook silently never fires.
22
+ */
23
+ export function hookCommandPath() {
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ // dist/commands -> package root
26
+ const candidate = resolve(here, '..', '..', 'bin', 'agentobs-hook');
27
+ if (existsSync(candidate))
28
+ return candidate;
29
+ return 'agentobs-hook';
30
+ }
31
+ /** The events worth hooking, with the matcher each one needs. */
32
+ const EVENTS = [
33
+ // Empty matcher: observe (and police) every tool, not a hand-listed subset
34
+ // that would silently miss MCP tools and anything added in future.
35
+ { event: 'PreToolUse', matcher: '' },
36
+ { event: 'PostToolUse', matcher: '' },
37
+ { event: 'SessionStart' },
38
+ { event: 'SessionEnd' },
39
+ ];
40
+ export function buildHookSettings(command) {
41
+ const hooks = {};
42
+ for (const { event, matcher } of EVENTS) {
43
+ const group = {};
44
+ if (matcher !== undefined)
45
+ group.matcher = matcher;
46
+ group.hooks = [{ type: 'command', command }];
47
+ hooks[event] = [group];
48
+ }
49
+ return { hooks };
50
+ }
51
+ export function renderHookSettings(command) {
52
+ // Quote the path for JSON; Windows paths carry backslashes that must escape.
53
+ return JSON.stringify(buildHookSettings(command), null, 2);
54
+ }
55
+ /** Path to the user-level Claude Code settings file, for messaging only. */
56
+ export function claudeSettingsPath(homeDir) {
57
+ return join(homeDir, '.claude', 'settings.json');
58
+ }
59
+ //# sourceMappingURL=hook-config.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `agentobs init` - first-run setup.
3
+ *
4
+ * Creates the home directory, the database, a starter pricing table, and
5
+ * prints the exact Claude Code hook block for the user to paste. The printed
6
+ * config is the whole onboarding experience, so it is copy-paste ready with
7
+ * absolute paths already filled in rather than placeholders to edit.
8
+ */
9
+ import { existsSync, writeFileSync } from 'node:fs';
10
+ import { closeDb, ensureDeviceId, openDb } from '../core/db.js';
11
+ import { ensureHome, paths } from '../core/paths.js';
12
+ import { DEFAULT_PRICING, writeDefaultPricing } from '../core/pricing.js';
13
+ import { hookCommandPath, renderHookSettings } from './hook-config.js';
14
+ export async function init(opts = {}) {
15
+ const home = ensureHome();
16
+ const db = openDb();
17
+ const deviceId = ensureDeviceId(db);
18
+ closeDb();
19
+ if (opts.force && existsSync(paths.pricing())) {
20
+ writeFileSync(paths.pricing(), `${JSON.stringify(DEFAULT_PRICING, null, 2)}\n`, 'utf8');
21
+ }
22
+ else {
23
+ writeDefaultPricing();
24
+ }
25
+ console.log(`AgentObs initialised.
26
+
27
+ Home ${home}
28
+ Database ${paths.db()}
29
+ Pricing ${paths.pricing()}
30
+ Device ${deviceId}
31
+
32
+ Next: add this to your Claude Code settings so tool calls are recorded.
33
+ File: ~/.claude/settings.json (or .claude/settings.json for one project)
34
+ `);
35
+ console.log(renderHookSettings(hookCommandPath()));
36
+ console.log(`
37
+ Then:
38
+ agentobs dashboard open the dashboard at http://127.0.0.1:4300
39
+ agentobs run -- <command> observe any other agent CLI (coarse detail)
40
+ agentobs policy init add guardrails that can block risky tool calls
41
+
42
+ Nothing leaves this machine. Tool inputs are truncated and secret-redacted
43
+ before they are written to disk.`);
44
+ }
45
+ //# sourceMappingURL=init.js.map