@alejandroroman/agent-kit 0.1.4 → 0.2.1

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.
Files changed (77) hide show
  1. package/dist/agent/loop.js +213 -111
  2. package/dist/agent/types.d.ts +2 -0
  3. package/dist/api/errors.d.ts +3 -0
  4. package/dist/api/errors.js +37 -0
  5. package/dist/api/events.d.ts +5 -0
  6. package/dist/api/events.js +28 -0
  7. package/dist/api/router.js +10 -0
  8. package/dist/api/traces.d.ts +3 -0
  9. package/dist/api/traces.js +35 -0
  10. package/dist/api/types.d.ts +2 -0
  11. package/dist/bootstrap.d.ts +3 -1
  12. package/dist/bootstrap.js +26 -7
  13. package/dist/cli/chat.js +3 -1
  14. package/dist/cli/claude-md-template.d.ts +5 -0
  15. package/dist/cli/claude-md-template.js +220 -0
  16. package/dist/cli/config-writer.js +3 -0
  17. package/dist/cli/env.d.ts +14 -0
  18. package/dist/cli/env.js +68 -0
  19. package/dist/cli/init.js +10 -0
  20. package/dist/cli/setup-agent/index.js +61 -18
  21. package/dist/cli/slack-setup.d.ts +6 -0
  22. package/dist/cli/slack-setup.js +234 -0
  23. package/dist/cli/start.js +65 -16
  24. package/dist/cli/ui.d.ts +2 -0
  25. package/dist/cli/ui.js +4 -1
  26. package/dist/cli/whats-new.d.ts +1 -0
  27. package/dist/cli/whats-new.js +69 -0
  28. package/dist/cli.js +14 -0
  29. package/dist/config/resolve.d.ts +1 -0
  30. package/dist/config/resolve.js +1 -0
  31. package/dist/config/schema.d.ts +2 -0
  32. package/dist/config/schema.js +1 -0
  33. package/dist/config/writer.d.ts +18 -0
  34. package/dist/config/writer.js +85 -0
  35. package/dist/cron/scheduler.d.ts +4 -1
  36. package/dist/cron/scheduler.js +99 -52
  37. package/dist/gateways/slack/client.d.ts +1 -0
  38. package/dist/gateways/slack/client.js +9 -0
  39. package/dist/gateways/slack/handler.js +2 -1
  40. package/dist/gateways/slack/index.js +75 -29
  41. package/dist/gateways/slack/listener.d.ts +8 -1
  42. package/dist/gateways/slack/listener.js +36 -10
  43. package/dist/heartbeat/runner.js +99 -82
  44. package/dist/llm/anthropic.d.ts +1 -0
  45. package/dist/llm/anthropic.js +11 -2
  46. package/dist/llm/fallback.js +34 -2
  47. package/dist/llm/openai.d.ts +2 -0
  48. package/dist/llm/openai.js +33 -2
  49. package/dist/llm/types.d.ts +16 -2
  50. package/dist/llm/types.js +9 -0
  51. package/dist/logger.d.ts +1 -0
  52. package/dist/logger.js +11 -0
  53. package/dist/media/sanitize.d.ts +5 -0
  54. package/dist/media/sanitize.js +53 -0
  55. package/dist/multi/spawn.js +29 -10
  56. package/dist/session/compaction.js +3 -1
  57. package/dist/session/prune-images.d.ts +9 -0
  58. package/dist/session/prune-images.js +42 -0
  59. package/dist/skills/activate.d.ts +6 -0
  60. package/dist/skills/activate.js +72 -27
  61. package/dist/skills/index.d.ts +1 -1
  62. package/dist/skills/index.js +1 -1
  63. package/dist/telemetry/db.d.ts +63 -0
  64. package/dist/telemetry/db.js +193 -0
  65. package/dist/telemetry/index.d.ts +17 -0
  66. package/dist/telemetry/index.js +82 -0
  67. package/dist/telemetry/sanitize.d.ts +6 -0
  68. package/dist/telemetry/sanitize.js +48 -0
  69. package/dist/telemetry/sqlite-processor.d.ts +11 -0
  70. package/dist/telemetry/sqlite-processor.js +108 -0
  71. package/dist/telemetry/types.d.ts +30 -0
  72. package/dist/telemetry/types.js +31 -0
  73. package/dist/tools/builtin/index.d.ts +2 -0
  74. package/dist/tools/builtin/index.js +2 -0
  75. package/dist/tools/builtin/self-config.d.ts +4 -0
  76. package/dist/tools/builtin/self-config.js +182 -0
  77. package/package.json +10 -2
@@ -1,6 +1,32 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { loadSkillManifest } from "./loader.js";
4
+ import { SpanStatusCode } from "@opentelemetry/api";
5
+ import { getTracer, ATTR } from "../telemetry/index.js";
6
+ /**
7
+ * Auto-activate all skills upfront so the LLM doesn't need to call activate_skill.
8
+ * Returns the skillsIndex string to append to the system prompt.
9
+ */
10
+ export async function preActivateSkills(ctx, activateTool, logger) {
11
+ for (const manifest of ctx.manifests) {
12
+ try {
13
+ const result = await activateTool.execute({ skill_name: manifest.name });
14
+ if (typeof result === "string" && result.startsWith("Error")) {
15
+ logger?.error({ skill: manifest.name, result }, "failed to auto-activate skill");
16
+ }
17
+ }
18
+ catch (err) {
19
+ logger?.error({ err, skill: manifest.name }, "skill auto-activation threw");
20
+ }
21
+ }
22
+ const activatedNames = [...ctx.activatedSkills];
23
+ if (activatedNames.length > 0) {
24
+ return "\n\nThe following skills are pre-activated: "
25
+ + activatedNames.join(", ")
26
+ + ".\nTheir tools are already available — you do not need to call activate_skill.";
27
+ }
28
+ return "";
29
+ }
4
30
  export function createActivateSkillTool(ctx) {
5
31
  return {
6
32
  name: "activate_skill",
@@ -24,39 +50,58 @@ export function createActivateSkillTool(ctx) {
24
50
  if (!manifest) {
25
51
  return `Skill "${skillName}" is not available. Available skills: ${ctx.manifests.map((m) => m.name).join(", ")}`;
26
52
  }
27
- const skillDir = path.join(ctx.skillsDir, skillName);
28
- const fullManifest = loadSkillManifest(skillDir);
29
- const loadedTools = [];
30
- // Load prompt fragment
31
- if (fullManifest.prompt) {
32
- const promptPath = path.join(skillDir, fullManifest.prompt);
33
- if (fs.existsSync(promptPath)) {
34
- ctx.promptFragments.push(fs.readFileSync(promptPath, "utf-8"));
53
+ const tracer = getTracer("skills");
54
+ const span = tracer.startSpan("skill.activate", {
55
+ attributes: {
56
+ [ATTR.SKILL_NAME]: skillName,
57
+ },
58
+ });
59
+ try {
60
+ const skillDir = path.join(ctx.skillsDir, skillName);
61
+ const fullManifest = loadSkillManifest(skillDir);
62
+ const loadedTools = [];
63
+ // Load prompt fragment
64
+ if (fullManifest.prompt) {
65
+ const promptPath = path.join(skillDir, fullManifest.prompt);
66
+ if (fs.existsSync(promptPath)) {
67
+ ctx.promptFragments.push(fs.readFileSync(promptPath, "utf-8"));
68
+ }
35
69
  }
36
- }
37
- // Load tools via dynamic import
38
- if (fullManifest.tools) {
39
- for (const toolName of fullManifest.tools) {
40
- const toolPath = path.join(skillDir, "tools", `${toolName}.ts`);
41
- if (fs.existsSync(toolPath)) {
42
- try {
43
- const mod = await import(toolPath);
44
- const tool = mod.default;
45
- ctx.toolRegistry.register(tool);
46
- loadedTools.push(tool.name);
47
- }
48
- catch (err) {
49
- return `Error loading tool "${toolName}" from skill "${skillName}": ${err instanceof Error ? err.message : String(err)}`;
70
+ // Load tools via dynamic import
71
+ if (fullManifest.tools) {
72
+ for (const toolName of fullManifest.tools) {
73
+ const toolPath = path.join(skillDir, "tools", `${toolName}.ts`);
74
+ if (fs.existsSync(toolPath)) {
75
+ try {
76
+ const mod = await import(toolPath);
77
+ const tool = mod.default;
78
+ ctx.toolRegistry.register(tool);
79
+ loadedTools.push(tool.name);
80
+ }
81
+ catch (err) {
82
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
83
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
84
+ return `Error loading tool "${toolName}" from skill "${skillName}": ${err instanceof Error ? err.message : String(err)}`;
85
+ }
50
86
  }
51
87
  }
52
88
  }
89
+ ctx.activatedSkills.add(skillName);
90
+ span.setAttribute(ATTR.SKILL_TOOLS_LOADED, loadedTools.length);
91
+ const parts = [`Skill "${skillName}" activated.`];
92
+ if (loadedTools.length > 0) {
93
+ parts.push(`Tools now available: ${loadedTools.join(", ")}`);
94
+ }
95
+ return parts.join(" ");
96
+ }
97
+ catch (err) {
98
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
99
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
100
+ throw err;
53
101
  }
54
- ctx.activatedSkills.add(skillName);
55
- const parts = [`Skill "${skillName}" activated.`];
56
- if (loadedTools.length > 0) {
57
- parts.push(`Tools now available: ${loadedTools.join(", ")}`);
102
+ finally {
103
+ span.end();
58
104
  }
59
- return parts.join(" ");
60
105
  },
61
106
  };
62
107
  }
@@ -1,3 +1,3 @@
1
1
  export { SkillManifestSchema, type SkillManifest } from "./schema.js";
2
2
  export { loadSkillManifest, loadAllSkillManifests } from "./loader.js";
3
- export { createActivateSkillTool, type SkillActivationContext } from "./activate.js";
3
+ export { createActivateSkillTool, preActivateSkills, type SkillActivationContext } from "./activate.js";
@@ -1,3 +1,3 @@
1
1
  export { SkillManifestSchema } from "./schema.js";
2
2
  export { loadSkillManifest, loadAllSkillManifests } from "./loader.js";
3
- export { createActivateSkillTool } from "./activate.js";
3
+ export { createActivateSkillTool, preActivateSkills } from "./activate.js";
@@ -0,0 +1,63 @@
1
+ export interface SpanRow {
2
+ trace_id: string;
3
+ span_id: string;
4
+ parent_span_id?: string | null;
5
+ name: string;
6
+ kind: string;
7
+ status: string;
8
+ agent?: string | null;
9
+ source?: string | null;
10
+ start_time: number;
11
+ end_time: number;
12
+ duration_ms: number;
13
+ attributes: string;
14
+ events: string;
15
+ }
16
+ export interface ErrorRow {
17
+ trace_id: string;
18
+ span_id: string;
19
+ fingerprint: string;
20
+ error_type?: string | null;
21
+ message: string;
22
+ stack?: string | null;
23
+ agent?: string | null;
24
+ source?: string | null;
25
+ }
26
+ export interface EventRow {
27
+ trace_id?: string | null;
28
+ span_id?: string | null;
29
+ name: string;
30
+ agent?: string | null;
31
+ body?: string;
32
+ }
33
+ export interface ListTracesOptions {
34
+ agent?: string;
35
+ source?: string;
36
+ status?: string;
37
+ since?: string;
38
+ limit?: number;
39
+ }
40
+ export interface RecentErrorsOptions {
41
+ agent?: string;
42
+ source?: string;
43
+ since?: string;
44
+ limit?: number;
45
+ }
46
+ export interface ErrorStatsOptions {
47
+ since?: string;
48
+ groupBy?: "fingerprint" | "agent";
49
+ }
50
+ export declare class TelemetryDb {
51
+ private db;
52
+ constructor(dbPath: string);
53
+ insertSpan(span: SpanRow): void;
54
+ insertError(error: ErrorRow): void;
55
+ insertEvent(event: EventRow): void;
56
+ getTraceSpans(traceId: string): any[];
57
+ listTraces(options: ListTracesOptions): any[];
58
+ getRecentErrors(options: RecentErrorsOptions): any[];
59
+ private static readonly ALLOWED_GROUP_BY;
60
+ getErrorStats(options: ErrorStatsOptions): any[];
61
+ cleanup(retentionDays: number, vacuum?: boolean): void;
62
+ close(): void;
63
+ }
@@ -0,0 +1,193 @@
1
+ import Database from "better-sqlite3";
2
+ const SCHEMA_SQL = `
3
+ CREATE TABLE IF NOT EXISTS spans (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ trace_id TEXT NOT NULL,
6
+ span_id TEXT NOT NULL,
7
+ parent_span_id TEXT,
8
+ name TEXT NOT NULL,
9
+ kind TEXT NOT NULL DEFAULT 'internal',
10
+ status TEXT NOT NULL DEFAULT 'unset',
11
+ agent TEXT,
12
+ source TEXT,
13
+ start_time INTEGER NOT NULL,
14
+ end_time INTEGER NOT NULL,
15
+ duration_ms REAL NOT NULL,
16
+ attributes TEXT NOT NULL DEFAULT '{}',
17
+ events TEXT NOT NULL DEFAULT '[]',
18
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
19
+ UNIQUE(trace_id, span_id)
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS errors (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ trace_id TEXT NOT NULL,
25
+ span_id TEXT NOT NULL,
26
+ fingerprint TEXT NOT NULL,
27
+ error_type TEXT,
28
+ message TEXT NOT NULL,
29
+ stack TEXT,
30
+ agent TEXT,
31
+ source TEXT,
32
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
33
+ FOREIGN KEY (trace_id, span_id) REFERENCES spans(trace_id, span_id) ON DELETE CASCADE
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS events (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ trace_id TEXT,
39
+ span_id TEXT,
40
+ name TEXT NOT NULL,
41
+ agent TEXT,
42
+ body TEXT NOT NULL DEFAULT '{}',
43
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
44
+ );
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_spans_trace ON spans(trace_id);
47
+ CREATE INDEX IF NOT EXISTS idx_spans_parent ON spans(parent_span_id);
48
+ CREATE INDEX IF NOT EXISTS idx_spans_name_time ON spans(name, start_time);
49
+ CREATE INDEX IF NOT EXISTS idx_spans_agent_time ON spans(agent, created_at);
50
+ CREATE INDEX IF NOT EXISTS idx_spans_source_time ON spans(source, created_at);
51
+ CREATE INDEX IF NOT EXISTS idx_spans_status ON spans(status, created_at);
52
+ CREATE INDEX IF NOT EXISTS idx_errors_agent_time ON errors(agent, created_at);
53
+ CREATE INDEX IF NOT EXISTS idx_errors_fingerprint ON errors(fingerprint, created_at);
54
+ CREATE INDEX IF NOT EXISTS idx_errors_trace ON errors(trace_id);
55
+ CREATE INDEX IF NOT EXISTS idx_events_time ON events(timestamp);
56
+ CREATE INDEX IF NOT EXISTS idx_events_agent ON events(agent, timestamp);
57
+ CREATE INDEX IF NOT EXISTS idx_events_span ON events(span_id);
58
+ `;
59
+ export class TelemetryDb {
60
+ db;
61
+ constructor(dbPath) {
62
+ this.db = new Database(dbPath);
63
+ this.db.pragma("journal_mode = WAL");
64
+ this.db.pragma("foreign_keys = ON");
65
+ this.db.exec(SCHEMA_SQL);
66
+ }
67
+ insertSpan(span) {
68
+ this.db
69
+ .prepare(`INSERT INTO spans (trace_id, span_id, parent_span_id, name, kind, status, agent, source, start_time, end_time, duration_ms, attributes, events)
70
+ VALUES (@trace_id, @span_id, @parent_span_id, @name, @kind, @status, @agent, @source, @start_time, @end_time, @duration_ms, @attributes, @events)`)
71
+ .run({
72
+ trace_id: span.trace_id,
73
+ span_id: span.span_id,
74
+ parent_span_id: span.parent_span_id ?? null,
75
+ name: span.name,
76
+ kind: span.kind,
77
+ status: span.status,
78
+ agent: span.agent ?? null,
79
+ source: span.source ?? null,
80
+ start_time: span.start_time,
81
+ end_time: span.end_time,
82
+ duration_ms: span.duration_ms,
83
+ attributes: span.attributes,
84
+ events: span.events,
85
+ });
86
+ }
87
+ insertError(error) {
88
+ this.db
89
+ .prepare(`INSERT INTO errors (trace_id, span_id, fingerprint, error_type, message, stack, agent, source)
90
+ VALUES (@trace_id, @span_id, @fingerprint, @error_type, @message, @stack, @agent, @source)`)
91
+ .run({
92
+ trace_id: error.trace_id,
93
+ span_id: error.span_id,
94
+ fingerprint: error.fingerprint,
95
+ error_type: error.error_type ?? null,
96
+ message: error.message,
97
+ stack: error.stack ?? null,
98
+ agent: error.agent ?? null,
99
+ source: error.source ?? null,
100
+ });
101
+ }
102
+ insertEvent(event) {
103
+ this.db
104
+ .prepare(`INSERT INTO events (trace_id, span_id, name, agent, body)
105
+ VALUES (@trace_id, @span_id, @name, @agent, @body)`)
106
+ .run({
107
+ trace_id: event.trace_id ?? null,
108
+ span_id: event.span_id ?? null,
109
+ name: event.name,
110
+ agent: event.agent ?? null,
111
+ body: event.body ?? "{}",
112
+ });
113
+ }
114
+ getTraceSpans(traceId) {
115
+ return this.db
116
+ .prepare("SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time")
117
+ .all(traceId);
118
+ }
119
+ listTraces(options) {
120
+ const conditions = ["parent_span_id IS NULL"];
121
+ const params = [];
122
+ if (options.agent) {
123
+ conditions.push("agent = ?");
124
+ params.push(options.agent);
125
+ }
126
+ if (options.source) {
127
+ conditions.push("source = ?");
128
+ params.push(options.source);
129
+ }
130
+ if (options.status) {
131
+ conditions.push("status = ?");
132
+ params.push(options.status);
133
+ }
134
+ if (options.since) {
135
+ conditions.push("created_at >= ?");
136
+ params.push(options.since);
137
+ }
138
+ const limit = options.limit ?? 100;
139
+ const sql = `SELECT * FROM spans WHERE ${conditions.join(" AND ")} ORDER BY start_time DESC LIMIT ?`;
140
+ params.push(limit);
141
+ return this.db.prepare(sql).all(...params);
142
+ }
143
+ getRecentErrors(options) {
144
+ const conditions = [];
145
+ const params = [];
146
+ if (options.agent) {
147
+ conditions.push("agent = ?");
148
+ params.push(options.agent);
149
+ }
150
+ if (options.source) {
151
+ conditions.push("source = ?");
152
+ params.push(options.source);
153
+ }
154
+ if (options.since) {
155
+ conditions.push("created_at >= ?");
156
+ params.push(options.since);
157
+ }
158
+ const limit = options.limit ?? 100;
159
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
160
+ const sql = `SELECT * FROM errors ${where} ORDER BY created_at DESC LIMIT ?`;
161
+ params.push(limit);
162
+ return this.db.prepare(sql).all(...params);
163
+ }
164
+ static ALLOWED_GROUP_BY = new Set(["fingerprint", "agent"]);
165
+ getErrorStats(options) {
166
+ const conditions = [];
167
+ const params = [];
168
+ if (options.since) {
169
+ conditions.push("created_at >= ?");
170
+ params.push(options.since);
171
+ }
172
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
173
+ const groupBy = options.groupBy ?? "fingerprint";
174
+ if (!TelemetryDb.ALLOWED_GROUP_BY.has(groupBy)) {
175
+ throw new Error(`Invalid groupBy: ${groupBy}`);
176
+ }
177
+ const sql = `SELECT ${groupBy}, COUNT(*) as count FROM errors ${where} GROUP BY ${groupBy} ORDER BY count DESC`;
178
+ return this.db.prepare(sql).all(...params);
179
+ }
180
+ cleanup(retentionDays, vacuum = false) {
181
+ const cutoff = `-${retentionDays} days`;
182
+ this.db.transaction(() => {
183
+ this.db.prepare("DELETE FROM events WHERE timestamp < datetime('now', ?)").run(cutoff);
184
+ this.db.prepare("DELETE FROM spans WHERE created_at < datetime('now', ?)").run(cutoff);
185
+ })();
186
+ if (vacuum) {
187
+ this.db.exec("VACUUM");
188
+ }
189
+ }
190
+ close() {
191
+ this.db.close();
192
+ }
193
+ }
@@ -0,0 +1,17 @@
1
+ import { type Tracer } from "@opentelemetry/api";
2
+ import { TelemetryDb } from "./db.js";
3
+ export interface TelemetryConfig {
4
+ enabled?: boolean;
5
+ serviceName?: string;
6
+ otlpEndpoint?: string;
7
+ sentryDsn?: string;
8
+ dbPath?: string;
9
+ sampleRate?: number;
10
+ }
11
+ /** Get the TelemetryDb instance (available after initTelemetry with a dbPath) */
12
+ export declare function getTelemetryDb(): TelemetryDb | undefined;
13
+ export declare function initTelemetry(config?: TelemetryConfig): () => Promise<void>;
14
+ export declare function getTracer(name: string): Tracer;
15
+ export { ATTR } from "./types.js";
16
+ export { TelemetryDb } from "./db.js";
17
+ export { SQLiteSpanProcessor } from "./sqlite-processor.js";
@@ -0,0 +1,82 @@
1
+ import * as path from "path";
2
+ import * as os from "os";
3
+ import { trace } from "@opentelemetry/api";
4
+ import { NodeSDK } from "@opentelemetry/sdk-node";
5
+ import { SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
6
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7
+ import * as Sentry from "@sentry/node";
8
+ import { createLogger } from "../logger.js";
9
+ import { TelemetryDb } from "./db.js";
10
+ import { SQLiteSpanProcessor } from "./sqlite-processor.js";
11
+ const log = createLogger("telemetry");
12
+ const DEFAULT_DB_PATH = path.join(os.homedir(), ".agent-kit", "telemetry.db");
13
+ let sdk;
14
+ let telemetryDb;
15
+ /** Get the TelemetryDb instance (available after initTelemetry with a dbPath) */
16
+ export function getTelemetryDb() {
17
+ return telemetryDb;
18
+ }
19
+ export function initTelemetry(config = {}) {
20
+ const { enabled = process.env.OTEL_ENABLED !== "false", serviceName = process.env.OTEL_SERVICE_NAME ?? "agent-kit", otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT, sentryDsn, dbPath = DEFAULT_DB_PATH, sampleRate, } = config;
21
+ if (!enabled) {
22
+ log.info("telemetry disabled");
23
+ return async () => { };
24
+ }
25
+ // Guard against double initialization
26
+ if (sdk) {
27
+ log.warn("telemetry already initialized, shutting down previous instance");
28
+ sdk.shutdown().catch(() => { });
29
+ sdk = undefined;
30
+ telemetryDb = undefined;
31
+ }
32
+ const spanProcessors = [];
33
+ // SQLite span processor (local persistence)
34
+ if (dbPath) {
35
+ try {
36
+ telemetryDb = new TelemetryDb(dbPath);
37
+ spanProcessors.push(new SQLiteSpanProcessor(telemetryDb));
38
+ log.info({ dbPath }, "SQLite span processor enabled");
39
+ }
40
+ catch (err) {
41
+ log.error({ err, dbPath }, "failed to initialize SQLite span processor");
42
+ }
43
+ }
44
+ // OTLP exporter (optional — for Grafana/Jaeger)
45
+ if (otlpEndpoint) {
46
+ spanProcessors.push(new SimpleSpanProcessor(new OTLPTraceExporter({ url: otlpEndpoint })));
47
+ log.info({ endpoint: otlpEndpoint }, "OTLP exporter enabled");
48
+ }
49
+ // Console exporter for dev (when no OTLP endpoint)
50
+ if (!otlpEndpoint && process.env.OTEL_DEBUG === "true") {
51
+ spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
52
+ }
53
+ // Sentry error tracking (optional)
54
+ if (sentryDsn) {
55
+ Sentry.init({
56
+ dsn: sentryDsn,
57
+ tracesSampleRate: sampleRate ?? 1.0,
58
+ release: process.env.npm_package_version,
59
+ environment: process.env.NODE_ENV ?? "development",
60
+ });
61
+ log.info("Sentry initialized");
62
+ }
63
+ sdk = new NodeSDK({
64
+ serviceName,
65
+ spanProcessors,
66
+ });
67
+ sdk.start();
68
+ log.info({ serviceName }, "telemetry initialized");
69
+ return async () => {
70
+ if (sdk) {
71
+ await sdk.shutdown();
72
+ log.info("telemetry shut down");
73
+ }
74
+ await Sentry.close(2000);
75
+ };
76
+ }
77
+ export function getTracer(name) {
78
+ return trace.getTracer(name);
79
+ }
80
+ export { ATTR } from "./types.js";
81
+ export { TelemetryDb } from "./db.js";
82
+ export { SQLiteSpanProcessor } from "./sqlite-processor.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sanitize span attributes before persisting:
3
+ * 1. Redact values matching known secret patterns
4
+ * 2. Truncate long string values (unless key is in the safe list)
5
+ */
6
+ export declare function sanitizeAttributes(attrs: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,48 @@
1
+ import { ATTR } from "./types.js";
2
+ /**
3
+ * Patterns that match known secret/credential formats.
4
+ * If any pattern matches within a string value, the entire value is redacted.
5
+ */
6
+ const SECRET_PATTERNS = [
7
+ /sk-[a-zA-Z0-9\-]{20,}/, // OpenAI/Anthropic API keys
8
+ /xoxb-[a-zA-Z0-9\-]+/, // Slack bot tokens
9
+ /xoxp-[a-zA-Z0-9\-]+/, // Slack user tokens
10
+ /xapp-[a-zA-Z0-9\-]+/, // Slack app tokens
11
+ /Bearer\s+[a-zA-Z0-9\-_.]+/, // Bearer tokens
12
+ /https?:\/\/[^@\s]+@[^\s]*sentry\.io[^\s]*/, // Sentry DSNs
13
+ /ghp_[a-zA-Z0-9]{36,}/, // GitHub personal access tokens
14
+ /gho_[a-zA-Z0-9]{36,}/, // GitHub OAuth tokens
15
+ ];
16
+ const MAX_STRING_LENGTH = 500;
17
+ /**
18
+ * ATTR keys that are controlled by our code and should never be truncated.
19
+ * They are still checked for secret patterns (in case a user accidentally
20
+ * passes a credential as e.g. an agent name).
21
+ */
22
+ const SAFE_KEYS = new Set(Object.values(ATTR));
23
+ /**
24
+ * Sanitize span attributes before persisting:
25
+ * 1. Redact values matching known secret patterns
26
+ * 2. Truncate long string values (unless key is in the safe list)
27
+ */
28
+ export function sanitizeAttributes(attrs) {
29
+ const result = {};
30
+ for (const [key, value] of Object.entries(attrs)) {
31
+ result[key] = sanitizeValue(key, value);
32
+ }
33
+ return result;
34
+ }
35
+ function sanitizeValue(key, value) {
36
+ if (typeof value !== "string")
37
+ return value;
38
+ // Check for secret patterns — always, even for safe keys
39
+ for (const pattern of SECRET_PATTERNS) {
40
+ if (pattern.test(value))
41
+ return "[REDACTED]";
42
+ }
43
+ // Truncate long strings, but skip safe (ATTR) keys
44
+ if (!SAFE_KEYS.has(key) && value.length > MAX_STRING_LENGTH) {
45
+ return value.slice(0, MAX_STRING_LENGTH) + "...[truncated]";
46
+ }
47
+ return value;
48
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context } from "@opentelemetry/api";
2
+ import type { SpanProcessor, Span, ReadableSpan } from "@opentelemetry/sdk-trace-base";
3
+ import type { TelemetryDb } from "./db.js";
4
+ export declare class SQLiteSpanProcessor implements SpanProcessor {
5
+ private db;
6
+ constructor(db: TelemetryDb);
7
+ onStart(_span: Span, _parentContext: Context): void;
8
+ onEnd(span: ReadableSpan): void;
9
+ shutdown(): Promise<void>;
10
+ forceFlush(): Promise<void>;
11
+ }
@@ -0,0 +1,108 @@
1
+ import { SpanStatusCode } from "@opentelemetry/api";
2
+ import { sanitizeAttributes } from "./sanitize.js";
3
+ import { ATTR } from "./types.js";
4
+ const SPAN_KIND_MAP = {
5
+ 0: "internal",
6
+ 1: "server",
7
+ 2: "client",
8
+ 3: "producer",
9
+ 4: "consumer",
10
+ };
11
+ const STATUS_MAP = {
12
+ [SpanStatusCode.UNSET]: "unset",
13
+ [SpanStatusCode.OK]: "ok",
14
+ [SpanStatusCode.ERROR]: "error",
15
+ };
16
+ /** Convert OTel HrTime [seconds, nanoseconds] to milliseconds */
17
+ function hrToMs(hr) {
18
+ return hr[0] * 1000 + hr[1] / 1e6;
19
+ }
20
+ /**
21
+ * Generate an error fingerprint from exception info.
22
+ * Prefers: error_type + first file + line from stack trace.
23
+ * Fallback: error_type + message.
24
+ */
25
+ function generateFingerprint(errorType, message, stack) {
26
+ const type = errorType ?? "Error";
27
+ if (stack) {
28
+ // Try to extract file:line from first stack frame
29
+ // Match patterns like: at Object.<anonymous> (src/agent/loop.ts:42:10)
30
+ // or: at src/agent/loop.ts:42:10
31
+ const frameMatch = stack.match(/(?:at\s+(?:.*?\s+)?(?:\()?)((?:[a-zA-Z]:)?[^:(\s]+):(\d+)/);
32
+ if (frameMatch) {
33
+ const file = frameMatch[1];
34
+ const line = frameMatch[2];
35
+ return `${type}:${file}:${line}`;
36
+ }
37
+ }
38
+ return `${type}:${message}`;
39
+ }
40
+ export class SQLiteSpanProcessor {
41
+ db;
42
+ constructor(db) {
43
+ this.db = db;
44
+ }
45
+ onStart(_span, _parentContext) {
46
+ // no-op
47
+ }
48
+ onEnd(span) {
49
+ const ctx = span.spanContext();
50
+ const traceId = ctx.traceId;
51
+ const spanId = ctx.spanId;
52
+ const parentSpanId = span.parentSpanContext?.spanId ?? null;
53
+ const agent = span.attributes[ATTR.AGENT];
54
+ const source = span.attributes[ATTR.SOURCE];
55
+ const startTime = hrToMs(span.startTime);
56
+ const endTime = hrToMs(span.endTime);
57
+ const durationMs = hrToMs(span.duration);
58
+ const kind = SPAN_KIND_MAP[span.kind] ?? "internal";
59
+ const status = STATUS_MAP[span.status.code] ?? "unset";
60
+ // Serialize all span events for storage
61
+ const serializedEvents = span.events.map((e) => ({
62
+ name: e.name,
63
+ time: hrToMs(e.time),
64
+ attributes: e.attributes ?? {},
65
+ }));
66
+ // Insert span
67
+ this.db.insertSpan({
68
+ trace_id: traceId,
69
+ span_id: spanId,
70
+ parent_span_id: parentSpanId,
71
+ name: span.name,
72
+ kind,
73
+ status,
74
+ agent: agent ?? null,
75
+ source: source ?? null,
76
+ start_time: startTime,
77
+ end_time: endTime,
78
+ duration_ms: durationMs,
79
+ attributes: JSON.stringify(sanitizeAttributes(span.attributes)),
80
+ events: JSON.stringify(serializedEvents),
81
+ });
82
+ // Extract exception events and write to errors table
83
+ for (const event of span.events) {
84
+ if (event.name === "exception") {
85
+ const errorType = event.attributes?.["exception.type"];
86
+ const message = event.attributes?.["exception.message"] ?? "Unknown error";
87
+ const stack = event.attributes?.["exception.stacktrace"];
88
+ const fingerprint = generateFingerprint(errorType, message, stack);
89
+ this.db.insertError({
90
+ trace_id: traceId,
91
+ span_id: spanId,
92
+ fingerprint,
93
+ error_type: errorType ?? null,
94
+ message,
95
+ stack: stack ?? null,
96
+ agent: agent ?? null,
97
+ source: source ?? null,
98
+ });
99
+ }
100
+ }
101
+ }
102
+ async shutdown() {
103
+ this.db.close();
104
+ }
105
+ async forceFlush() {
106
+ // Writes are synchronous with better-sqlite3, nothing to flush
107
+ }
108
+ }