@routecraft/cli 0.6.0-canary.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui/db.d.ts CHANGED
@@ -17,6 +17,60 @@ interface ExchangeSnapshot {
17
17
  body: string | null;
18
18
  truncated: boolean;
19
19
  }
20
+ /**
21
+ * A summary row for the Agents tab. `key` is the registered agent id for
22
+ * by-name agents, or the dispatching route id for inline agents.
23
+ */
24
+ interface AgentSummary {
25
+ key: string;
26
+ source: "registered" | "inline";
27
+ model: string | null;
28
+ description: string | null;
29
+ runCount: number;
30
+ errorCount: number;
31
+ totalTokens: number;
32
+ lastRunAt: string | null;
33
+ }
34
+ /** A summary row for the Tools tab. */
35
+ interface ToolSummary {
36
+ name: string;
37
+ source: "registered" | "observed";
38
+ callCount: number;
39
+ errorCount: number;
40
+ lastCalledAt: string | null;
41
+ }
42
+ /** Per-run agent detail, keyed by the dispatching exchange. */
43
+ interface AgentRunInfo {
44
+ exchangeId: string;
45
+ model: string | null;
46
+ finishReason: string | null;
47
+ inputTokens: number | null;
48
+ outputTokens: number | null;
49
+ totalTokens: number | null;
50
+ status: "running" | "finished" | "error";
51
+ }
52
+ /**
53
+ * A single tool invocation, correlating the invoked/result/error events
54
+ * for one `toolCallId`. `input`/`output`/`error` are only populated when
55
+ * telemetry snapshot capture was enabled; `errorName` is the always
56
+ * persisted, non-sensitive error class.
57
+ */
58
+ interface ToolCallRow {
59
+ toolCallId: string;
60
+ toolName: string;
61
+ routeId: string;
62
+ exchangeId: string;
63
+ agentName: string | null;
64
+ status: "invoked" | "result" | "error";
65
+ durationMs: number | null;
66
+ timestamp: string;
67
+ hasInput: boolean;
68
+ hasOutput: boolean;
69
+ input: string | null;
70
+ output: string | null;
71
+ error: string | null;
72
+ errorName: string | null;
73
+ }
20
74
 
21
75
  /** Exchange row shape from the telemetry SQLite database. */
22
76
  interface TelemetryExchange {
@@ -33,6 +87,10 @@ interface TelemetryExchange {
33
87
  declare class TelemetryDb {
34
88
  private db;
35
89
  private stmtCache;
90
+ /** Incremental Agents-tab aggregation: only events with id > lastId are parsed per poll. */
91
+ private agentAgg;
92
+ /** Incremental Tools-tab aggregation: only events with id > lastId are parsed per poll. */
93
+ private toolAgg;
36
94
  private constructor();
37
95
  /** Prepare and cache a statement by key. Avoids re-parsing on every call. */
38
96
  private stmt;
@@ -72,6 +130,12 @@ declare class TelemetryDb {
72
130
  * without deduplication by correlation chain.
73
131
  */
74
132
  getFailedExchanges(limit?: number): TelemetryExchange[];
133
+ /**
134
+ * Get one exchange by id, or null when it is not (yet) recorded.
135
+ * Used by the live detail views to refresh status/duration while the
136
+ * exchange is open.
137
+ */
138
+ getExchangeById(id: string): TelemetryExchange | null;
75
139
  /**
76
140
  * Get events for an exchange and all related exchanges sharing the
77
141
  * same correlation ID. Uses indexed exchange_id/correlation_id columns
@@ -118,6 +182,53 @@ declare class TelemetryDb {
118
182
  * Returns null if no snapshot was captured.
119
183
  */
120
184
  getExchangeSnapshot(exchangeId: string): ExchangeSnapshot | null;
185
+ /**
186
+ * List agents derived from registration (`agent:registered`) and
187
+ * lifecycle (`route:agent:started|finished|error`) events. By-name
188
+ * agents are keyed by their registered id; inline agents by their
189
+ * dispatching route id.
190
+ *
191
+ * Aggregation is incremental: each call only reads and parses events
192
+ * newer than the previous call's high-water mark, so the 2s TUI poll
193
+ * stays cheap regardless of how large the events table has grown.
194
+ */
195
+ getAgents(): AgentSummary[];
196
+ /**
197
+ * Get the exchanges in which a given agent ran, most recent first.
198
+ * Resolves run exchange ids from `agent:started` events, then joins the
199
+ * `exchanges` table (synthesising a minimal row when the dispatching
200
+ * exchange was not separately recorded).
201
+ */
202
+ getAgentRuns(agentKey: string, source: "registered" | "inline", limit?: number): TelemetryExchange[];
203
+ /**
204
+ * Batch variant of {@link getAgentRunInfo}: per-run model, finish reason
205
+ * and token usage for a set of run exchanges in one query, keyed by
206
+ * exchange id. Used by the agent runs list to render model/token
207
+ * columns without one query per visible row.
208
+ */
209
+ getAgentRunInfos(exchangeIds: string[]): Map<string, AgentRunInfo>;
210
+ /** Per-run agent detail (model, finish reason, tokens) for an exchange. */
211
+ getAgentRunInfo(exchangeId: string): AgentRunInfo | null;
212
+ /** Ordered tool calls made during a single agent run (exchange). */
213
+ getAgentRunToolCalls(exchangeId: string): ToolCallRow[];
214
+ /**
215
+ * List tools derived from registration (`agent:tool:registered`) and
216
+ * invocation (`route:agent:tool:invoked|error`) events.
217
+ *
218
+ * Aggregation is incremental (see {@link getAgents}): each call only
219
+ * parses events newer than the previous call's high-water mark.
220
+ */
221
+ getTools(): ToolSummary[];
222
+ /**
223
+ * Invocation history for a single tool, most recent call first.
224
+ *
225
+ * The tool filter runs in SQL (json_extract) and the scan walks the
226
+ * primary key from the newest row, stopping at the LIMIT, so cost
227
+ * tracks the tool's recent activity rather than table size. The LIMIT
228
+ * is tripled because each call spans up to three events (invoked,
229
+ * result, error) that the correlation step merges into one row.
230
+ */
231
+ getToolCalls(toolName: string, limit?: number): ToolCallRow[];
121
232
  /**
122
233
  * Close the database connection.
123
234
  */
package/dist/tui/db.js CHANGED
@@ -1,2 +1,2 @@
1
- export{a as TelemetryDb}from'../chunk-5UHEZNN5.js';//# sourceMappingURL=db.js.map
1
+ export{a as TelemetryDb}from'../chunk-R3PVIDDT.js';//# sourceMappingURL=db.js.map
2
2
  //# sourceMappingURL=db.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routecraft/cli",
3
- "version": "0.6.0-canary.8",
3
+ "version": "0.6.0",
4
4
  "description": "CLI for running Routecraft capabilities",
5
5
  "private": false,
6
6
  "type": "module",
@@ -22,25 +22,27 @@
22
22
  "prepublishOnly": "bun run build"
23
23
  },
24
24
  "dependencies": {
25
- "@opentelemetry/sdk-trace-base": "^2.7.1",
26
- "@routecraft/routecraft": "^0.6.0-canary.8",
25
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
26
+ "@routecraft/routecraft": "^0.6.0",
27
27
  "agent-browser": "^0.17.1",
28
28
  "cheerio": "^1.2.0",
29
29
  "commander": "^14.0.3",
30
30
  "croner": "^10.0.1",
31
31
  "dotenv": "^17.4.2",
32
- "imapflow": "^1.3.3",
32
+ "fast-xml-parser": "^5.10.1",
33
+ "imapflow": "^1.4.7",
33
34
  "ink": "^5.2.1",
34
- "jose": "^6.2.3",
35
- "mailparser": "^3.9.8",
35
+ "jose": "^6.2.8",
36
+ "mailparser": "^3.9.15",
36
37
  "nodemailer": "^6.10.1",
37
- "papaparse": "^5.5.3",
38
+ "papaparse": "^5.5.4",
38
39
  "pino-pretty": "^13.1.3",
39
- "react": "^18.3.1"
40
+ "react": "^18.3.1",
41
+ "tsdav": "^2.3.1"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@types/mailparser": "^3.4.6",
43
- "@types/nodemailer": "^6.4.23",
45
+ "@types/nodemailer": "^6.4.24",
44
46
  "@types/react": "^18.3.28",
45
47
  "ink-testing-library": "^4.0.0"
46
48
  },
@@ -1,140 +0,0 @@
1
- function u(a){return a.replace(/[%_\\]/g,"\\$&")}var g=class a{db;stmtCache=new Map;constructor(e){this.db=e;}stmt(e,t){let n=this.stmtCache.get(e);return n||(n=this.db.prepare(t),this.stmtCache.set(e,n)),n}static async open(e){let t;try{t=(await import('bun:sqlite')).Database;}catch{throw new Error("bun:sqlite is not available. The craft CLI requires Bun >= 1.1.0.")}let n=new t(e,{readonly:true});try{n.exec("PRAGMA journal_mode = WAL");}catch{}{let r=new t(e);try{r.prepare("CREATE INDEX IF NOT EXISTS idx_exchanges_started_at ON exchanges(started_at)").run(),r.prepare("CREATE INDEX IF NOT EXISTS idx_exchanges_route_started ON exchanges(route_id, started_at)").run(),r.prepare("CREATE INDEX IF NOT EXISTS idx_exchanges_correlation_id ON exchanges(correlation_id)").run(),r.prepare("PRAGMA table_info(events)").all().some(o=>o.name==="exchange_id")||(r.prepare("ALTER TABLE events ADD COLUMN exchange_id TEXT").run(),r.prepare("ALTER TABLE events ADD COLUMN correlation_id TEXT").run()),r.prepare("CREATE INDEX IF NOT EXISTS idx_events_exchange_id ON events(exchange_id)").run(),r.prepare("CREATE INDEX IF NOT EXISTS idx_events_correlation_id ON events(correlation_id)").run(),r.prepare(`CREATE TABLE IF NOT EXISTS exchange_snapshots (
2
- exchange_id TEXT NOT NULL,
3
- context_id TEXT NOT NULL,
4
- headers TEXT NOT NULL,
5
- body TEXT,
6
- truncated INTEGER NOT NULL DEFAULT 0,
7
- captured_at TEXT NOT NULL DEFAULT (datetime('now')),
8
- PRIMARY KEY (exchange_id, context_id)
9
- )`).run();}catch{}finally{r.close();}}return new a(n)}getRouteSummary(){return this.stmt("routeSummary",`WITH unique_routes AS (
10
- SELECT
11
- id,
12
- MAX(registered_at) AS registered_at,
13
- (SELECT r2.status FROM routes r2
14
- WHERE r2.id = r.id
15
- ORDER BY r2.registered_at DESC LIMIT 1) AS status
16
- FROM routes r
17
- GROUP BY id
18
- ),
19
- exchange_counts AS (
20
- SELECT
21
- route_id,
22
- COUNT(*) AS total,
23
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
24
- SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
25
- SUM(CASE WHEN status = 'dropped' THEN 1 ELSE 0 END) AS dropped
26
- FROM exchanges
27
- GROUP BY route_id
28
- ),
29
- recent_avg AS (
30
- SELECT route_id, AVG(duration_ms) AS avgDur
31
- FROM exchanges
32
- WHERE started_at >= datetime('now', '-5 minutes')
33
- GROUP BY route_id
34
- )
35
- SELECT
36
- ur.id,
37
- ur.status,
38
- COALESCE(ec.total, 0) AS totalExchanges,
39
- COALESCE(ec.completed, 0) AS completedExchanges,
40
- COALESCE(ec.failed, 0) AS failedExchanges,
41
- COALESCE(ec.dropped, 0) AS droppedExchanges,
42
- ra.avgDur AS avgDurationMs
43
- FROM unique_routes ur
44
- LEFT JOIN exchange_counts ec ON ur.id = ec.route_id
45
- LEFT JOIN recent_avg ra ON ur.id = ra.route_id
46
- ORDER BY ur.registered_at DESC`).all()}getExchangesByRoute(e,t=-1){return this.stmt("exchangesByRoute",`SELECT
47
- id,
48
- route_id AS routeId,
49
- context_id AS contextId,
50
- correlation_id AS correlationId,
51
- status,
52
- started_at AS startedAt,
53
- completed_at AS completedAt,
54
- duration_ms AS durationMs,
55
- error
56
- FROM exchanges
57
- WHERE route_id = ?
58
- ORDER BY started_at DESC
59
- LIMIT ?`).all(e,t)}getAllExchanges(e=-1){return this.stmt("allExchanges",`SELECT
60
- id,
61
- route_id AS routeId,
62
- context_id AS contextId,
63
- correlation_id AS correlationId,
64
- status,
65
- started_at AS startedAt,
66
- completed_at AS completedAt,
67
- duration_ms AS durationMs,
68
- error
69
- FROM exchanges
70
- ORDER BY started_at DESC
71
- LIMIT ?`).all(e)}getFailedExchanges(e=-1){return this.stmt("failedExchanges",`SELECT
72
- id,
73
- route_id AS routeId,
74
- context_id AS contextId,
75
- correlation_id AS correlationId,
76
- status,
77
- started_at AS startedAt,
78
- completed_at AS completedAt,
79
- duration_ms AS durationMs,
80
- error
81
- FROM exchanges
82
- WHERE status = 'failed'
83
- ORDER BY started_at DESC
84
- LIMIT ?`).all(e)}getEventsByExchange(e,t){let n=t||e;return this.hasEventIdColumns()?this.stmt("eventsByExchangeIndexed",`SELECT
85
- id,
86
- timestamp,
87
- context_id AS contextId,
88
- event_name AS eventName,
89
- details
90
- FROM events
91
- WHERE exchange_id = ? OR correlation_id = ?
92
- ORDER BY id ASC`).all(n,n):this.stmt("eventsByExchangeFallback",`SELECT
93
- id,
94
- timestamp,
95
- context_id AS contextId,
96
- event_name AS eventName,
97
- details
98
- FROM events
99
- WHERE details LIKE '%' || ? || '%' ESCAPE '\\'
100
- ORDER BY id ASC`).all(u(n))}hasEventIdColumns(){if(this._hasEventIdColumns!==void 0)return this._hasEventIdColumns;try{let e=this.db.prepare("PRAGMA table_info(events)").all();this._hasEventIdColumns=e.some(t=>t.name==="exchange_id");}catch{this._hasEventIdColumns=false;}return this._hasEventIdColumns}_hasEventIdColumns;getRecentEvents(e){let t=e?.limit??100,n=e?.sinceId??0,r=`
101
- SELECT
102
- id,
103
- timestamp,
104
- context_id AS contextId,
105
- event_name AS eventName,
106
- details
107
- FROM events
108
- WHERE id > ?
109
- `,s=[n];if(e?.eventNameFilter&&(r+=" AND event_name LIKE ? ESCAPE '\\'",s.push(`%${u(e.eventNameFilter)}%`)),e?.routeIdFilter){r+=" AND (event_name LIKE ? ESCAPE '\\' OR details LIKE ? ESCAPE '\\')";let i=u(e.routeIdFilter);s.push(`%${i}%`,`%${i}%`);}return r+=" ORDER BY id DESC LIMIT ?",s.push(t),this.db.prepare(r).all(...s)}getMetrics(){let t=this.stmt("metrics",`SELECT
110
- (SELECT COUNT(DISTINCT id) FROM routes) AS totalRoutes,
111
- (SELECT COUNT(*) FROM exchanges) AS totalExchanges,
112
- (SELECT COUNT(*) FROM exchanges WHERE status = 'completed') AS completedExchanges,
113
- (SELECT COUNT(*) FROM exchanges WHERE status = 'failed') AS failedExchanges,
114
- (SELECT COUNT(*) FROM exchanges WHERE status = 'dropped') AS droppedExchanges,
115
- (SELECT AVG(duration_ms) FROM exchanges
116
- WHERE started_at >= datetime('now', '-5 minutes')) AS avgDurationMs`).get(),r=this.stmt("percentiles",`SELECT duration_ms
117
- FROM exchanges
118
- WHERE duration_ms IS NOT NULL
119
- AND started_at >= datetime('now', '-5 minutes')
120
- ORDER BY duration_ms ASC`).all().map(s=>s.duration_ms);return {...t,errorRate:t.totalExchanges>0?t.failedExchanges/t.totalExchanges:0,p90DurationMs:l(r,.9),p95DurationMs:l(r,.95),p99DurationMs:l(r,.99)}}getLiveTrafficBuckets(e=60,t=5){let n=Math.floor(Date.now()/1e3),r=Math.floor(n/t)*t,s=r-(e-1)*t,i=this.stmt("liveTraffic",`SELECT
121
- CAST((CAST(strftime('%s', started_at) AS INTEGER) - ?) / ? AS INTEGER) AS bucket,
122
- COUNT(*) AS cnt
123
- FROM exchanges
124
- WHERE CAST(strftime('%s', started_at) AS INTEGER) >= ?
125
- AND CAST(strftime('%s', started_at) AS INTEGER) <= ?
126
- GROUP BY bucket`).all(s,t,s,r+t),c=new Array(e).fill(0);for(let E of i)E.bucket>=0&&E.bucket<e&&(c[E.bucket]+=E.cnt);return c}getSingleRouteActivity(e,t=12,n=5){let r=Math.floor(Date.now()/1e3),o=Math.floor(r/n)*n-(t-1)*n,c=this.stmt("singleRouteActivity",`SELECT
127
- CAST((CAST(strftime('%s', started_at) AS INTEGER) - ?) / ? AS INTEGER) AS bucket,
128
- COUNT(*) AS cnt
129
- FROM exchanges
130
- WHERE route_id = ?
131
- AND CAST(strftime('%s', started_at) AS INTEGER) >= ?
132
- GROUP BY bucket`).all(o,n,e,o),S=this.stmt("singleRouteErrors",`SELECT COUNT(*) AS cnt
133
- FROM exchanges
134
- WHERE route_id = ?
135
- AND status = 'failed'
136
- AND CAST(strftime('%s', started_at) AS INTEGER) >= ?`).get(e,o),m=new Array(t).fill(0);for(let d of c)d.bucket>=0&&d.bucket<t&&(m[d.bucket]=d.cnt);return {throughput:m,recentErrors:S?.cnt??0}}getMaxEventId(){return this.db.prepare("SELECT COALESCE(MAX(id), 0) AS maxId FROM events").get().maxId}getExchangeSnapshot(e){try{let n=this.stmt("exchangeSnapshot",`SELECT headers, body, truncated
137
- FROM exchange_snapshots
138
- WHERE exchange_id = ?
139
- LIMIT 1`).get(e);return n?{headers:n.headers,body:n.body,truncated:n.truncated!==0}:null}catch{return null}}close(){this.db.close();}};function l(a,e){if(a.length===0)return null;let t=e*(a.length-1),n=Math.floor(t),r=Math.ceil(t);return n===r?a[n]:a[n]+(a[r]-a[n])*(t-n)}export{g as a};//# sourceMappingURL=chunk-5UHEZNN5.js.map
140
- //# sourceMappingURL=chunk-5UHEZNN5.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tui/db.ts"],"names":["escapeLike","s","TelemetryDb","_TelemetryDb","db","key","sql","dbPath","Database","wdb","c","routeId","limit","exchangeId","correlationId","searchId","info","col","options","sinceId","params","escaped","row","durations","r","percentile","bucketCount","bucketWidthSec","nowSec","snapped","windowStart","rows","result","tRows","eRow","throughput","sorted","p","idx","lo","hi"],"mappings":"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","file":"chunk-5UHEZNN5.js","sourcesContent":["/// <reference types=\"bun-types\" />\nimport type { TelemetryEvent } from \"@routecraft/routecraft\";\n\n/** Exchange row shape from the telemetry SQLite database. */\ninterface TelemetryExchange {\n id: string;\n routeId: string;\n contextId: string;\n correlationId: string;\n status: string;\n startedAt: string;\n completedAt: string | null;\n durationMs: number | null;\n error: string | null;\n}\n\n/**\n * Minimal type for the bun:sqlite database to avoid pulling bun-types\n * into the CLI's public type surface.\n */\ninterface Database {\n prepare(sql: string): Statement;\n query(sql: string): Statement;\n exec(sql: string): void;\n close(): void;\n}\n\ninterface Statement {\n all(...params: unknown[]): unknown[];\n get(...params: unknown[]): unknown;\n run(...params: unknown[]): unknown;\n}\n\ntype DatabaseConstructor = new (\n filename: string,\n options?: { readonly?: boolean; create?: boolean },\n) => Database;\n\n/**\n * Read-only accessor for the telemetry SQLite database.\n * Used by the TUI to query historical data without affecting the running engine.\n *\n * Use the static `open()` factory instead of the constructor directly.\n */\n/** Escape LIKE metacharacters so they match literally. */\nfunction escapeLike(s: string): string {\n return s.replace(/[%_\\\\]/g, \"\\\\$&\");\n}\n\nexport class TelemetryDb {\n private db: Database;\n private stmtCache = new Map<string, Statement>();\n\n private constructor(db: Database) {\n this.db = db;\n }\n\n /** Prepare and cache a statement by key. Avoids re-parsing on every call. */\n private stmt(key: string, sql: string): Statement {\n let s = this.stmtCache.get(key);\n if (!s) {\n s = this.db.prepare(sql);\n this.stmtCache.set(key, s);\n }\n return s;\n }\n\n /**\n * Open a telemetry database in read-only mode.\n *\n * Uses dynamic `import(\"bun:sqlite\")` so the module resolves at runtime\n * (the CLI is Bun-only; `bun:sqlite` is a Bun built-in and is not a\n * resolvable spec under Node).\n */\n static async open(dbPath: string): Promise<TelemetryDb> {\n let Database: DatabaseConstructor;\n try {\n const mod = await import(\"bun:sqlite\");\n Database = mod.Database as unknown as DatabaseConstructor;\n } catch {\n throw new Error(\n \"bun:sqlite is not available. The craft CLI requires Bun >= 1.1.0.\",\n );\n }\n\n const db = new Database(dbPath, { readonly: true });\n try {\n db.exec(\"PRAGMA journal_mode = WAL\");\n } catch {\n // Read-only connection cannot change journal mode; safe to ignore\n }\n\n // Ensure indexes exist for TUI query performance.\n // If the DB was created before indexes were added, create them now\n // using a separate writable connection.\n {\n const wdb = new Database(dbPath);\n try {\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_exchanges_started_at ON exchanges(started_at)\",\n )\n .run();\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_exchanges_route_started ON exchanges(route_id, started_at)\",\n )\n .run();\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_exchanges_correlation_id ON exchanges(correlation_id)\",\n )\n .run();\n // Add exchange_id/correlation_id columns to events table if missing\n const cols = wdb.prepare(\"PRAGMA table_info(events)\").all() as Array<{\n name: string;\n }>;\n if (!cols.some((c) => c.name === \"exchange_id\")) {\n wdb.prepare(\"ALTER TABLE events ADD COLUMN exchange_id TEXT\").run();\n wdb\n .prepare(\"ALTER TABLE events ADD COLUMN correlation_id TEXT\")\n .run();\n }\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_events_exchange_id ON events(exchange_id)\",\n )\n .run();\n wdb\n .prepare(\n \"CREATE INDEX IF NOT EXISTS idx_events_correlation_id ON events(correlation_id)\",\n )\n .run();\n // Ensure exchange_snapshots table exists for older databases\n wdb\n .prepare(\n `CREATE TABLE IF NOT EXISTS exchange_snapshots (\n exchange_id TEXT NOT NULL,\n context_id TEXT NOT NULL,\n headers TEXT NOT NULL,\n body TEXT,\n truncated INTEGER NOT NULL DEFAULT 0,\n captured_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY (exchange_id, context_id)\n )`,\n )\n .run();\n } catch {\n // Best-effort; the writer will create them on next restart\n } finally {\n wdb.close();\n }\n }\n\n return new TelemetryDb(db);\n }\n\n /**\n * Get a summary of all routes with aggregated metrics.\n * Routes are grouped by ID across all context runs so that restarting\n * the same application does not create duplicate rows.\n */\n getRouteSummary(): Array<{\n id: string;\n status: string;\n totalExchanges: number;\n completedExchanges: number;\n failedExchanges: number;\n droppedExchanges: number;\n avgDurationMs: number | null;\n }> {\n const s = this.stmt(\n \"routeSummary\",\n `WITH unique_routes AS (\n SELECT\n id,\n MAX(registered_at) AS registered_at,\n (SELECT r2.status FROM routes r2\n WHERE r2.id = r.id\n ORDER BY r2.registered_at DESC LIMIT 1) AS status\n FROM routes r\n GROUP BY id\n ),\n exchange_counts AS (\n SELECT\n route_id,\n COUNT(*) AS total,\n SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,\n SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,\n SUM(CASE WHEN status = 'dropped' THEN 1 ELSE 0 END) AS dropped\n FROM exchanges\n GROUP BY route_id\n ),\n recent_avg AS (\n SELECT route_id, AVG(duration_ms) AS avgDur\n FROM exchanges\n WHERE started_at >= datetime('now', '-5 minutes')\n GROUP BY route_id\n )\n SELECT\n ur.id,\n ur.status,\n COALESCE(ec.total, 0) AS totalExchanges,\n COALESCE(ec.completed, 0) AS completedExchanges,\n COALESCE(ec.failed, 0) AS failedExchanges,\n COALESCE(ec.dropped, 0) AS droppedExchanges,\n ra.avgDur AS avgDurationMs\n FROM unique_routes ur\n LEFT JOIN exchange_counts ec ON ur.id = ec.route_id\n LEFT JOIN recent_avg ra ON ur.id = ra.route_id\n ORDER BY ur.registered_at DESC`,\n );\n return s.all() as Array<{\n id: string;\n status: string;\n totalExchanges: number;\n completedExchanges: number;\n failedExchanges: number;\n droppedExchanges: number;\n avgDurationMs: number | null;\n }>;\n }\n\n /**\n * Get exchanges for a specific route, ordered by most recent first.\n */\n getExchangesByRoute(routeId: string, limit = -1): TelemetryExchange[] {\n const s = this.stmt(\n \"exchangesByRoute\",\n `SELECT\n id,\n route_id AS routeId,\n context_id AS contextId,\n correlation_id AS correlationId,\n status,\n started_at AS startedAt,\n completed_at AS completedAt,\n duration_ms AS durationMs,\n error\n FROM exchanges\n WHERE route_id = ?\n ORDER BY started_at DESC\n LIMIT ?`,\n );\n return s.all(routeId, limit) as TelemetryExchange[];\n }\n\n /**\n * Get all exchanges across all routes, ordered by most recent first.\n */\n getAllExchanges(limit = -1): TelemetryExchange[] {\n const s = this.stmt(\n \"allExchanges\",\n `SELECT\n id,\n route_id AS routeId,\n context_id AS contextId,\n correlation_id AS correlationId,\n status,\n started_at AS startedAt,\n completed_at AS completedAt,\n duration_ms AS durationMs,\n error\n FROM exchanges\n ORDER BY started_at DESC\n LIMIT ?`,\n );\n return s.all(limit) as TelemetryExchange[];\n }\n\n /**\n * Get all failed exchanges across all routes, ordered by most recent first.\n * Shows every failed exchange including child exchanges from split/multicast,\n * without deduplication by correlation chain.\n */\n getFailedExchanges(limit = -1): TelemetryExchange[] {\n const s = this.stmt(\n \"failedExchanges\",\n `SELECT\n id,\n route_id AS routeId,\n context_id AS contextId,\n correlation_id AS correlationId,\n status,\n started_at AS startedAt,\n completed_at AS completedAt,\n duration_ms AS durationMs,\n error\n FROM exchanges\n WHERE status = 'failed'\n ORDER BY started_at DESC\n LIMIT ?`,\n );\n return s.all(limit) as TelemetryExchange[];\n }\n\n /**\n * Get events for an exchange and all related exchanges sharing the\n * same correlation ID. Uses indexed exchange_id/correlation_id columns\n * with a fallback to details LIKE for databases created before the\n * column migration.\n */\n getEventsByExchange(\n exchangeId: string,\n correlationId: string,\n ): TelemetryEvent[] {\n const searchId = correlationId || exchangeId;\n\n // Try indexed column lookup first (fast path)\n const hasColumns = this.hasEventIdColumns();\n if (hasColumns) {\n const s = this.stmt(\n \"eventsByExchangeIndexed\",\n `SELECT\n id,\n timestamp,\n context_id AS contextId,\n event_name AS eventName,\n details\n FROM events\n WHERE exchange_id = ? OR correlation_id = ?\n ORDER BY id ASC`,\n );\n return s.all(searchId, searchId) as TelemetryEvent[];\n }\n\n // Fallback: full-text LIKE scan for older databases\n const s = this.stmt(\n \"eventsByExchangeFallback\",\n `SELECT\n id,\n timestamp,\n context_id AS contextId,\n event_name AS eventName,\n details\n FROM events\n WHERE details LIKE '%' || ? || '%' ESCAPE '\\\\'\n ORDER BY id ASC`,\n );\n return s.all(escapeLike(searchId)) as TelemetryEvent[];\n }\n\n /** Check whether the events table has the exchange_id column. */\n private hasEventIdColumns(): boolean {\n if (this._hasEventIdColumns !== undefined) return this._hasEventIdColumns;\n try {\n const info = this.db.prepare(\"PRAGMA table_info(events)\").all() as Array<{\n name: string;\n }>;\n this._hasEventIdColumns = info.some((col) => col.name === \"exchange_id\");\n } catch {\n this._hasEventIdColumns = false;\n }\n return this._hasEventIdColumns;\n }\n private _hasEventIdColumns: boolean | undefined;\n\n /**\n * Get recent events, optionally filtered by event name pattern or route ID.\n */\n getRecentEvents(options?: {\n limit?: number;\n eventNameFilter?: string;\n routeIdFilter?: string;\n sinceId?: number;\n }): TelemetryEvent[] {\n const limit = options?.limit ?? 100;\n const sinceId = options?.sinceId ?? 0;\n\n let sql = `\n SELECT\n id,\n timestamp,\n context_id AS contextId,\n event_name AS eventName,\n details\n FROM events\n WHERE id > ?\n `;\n const params: unknown[] = [sinceId];\n\n if (options?.eventNameFilter) {\n sql += \" AND event_name LIKE ? ESCAPE '\\\\'\";\n params.push(`%${escapeLike(options.eventNameFilter)}%`);\n }\n\n if (options?.routeIdFilter) {\n sql +=\n \" AND (event_name LIKE ? ESCAPE '\\\\' OR details LIKE ? ESCAPE '\\\\')\";\n const escaped = escapeLike(options.routeIdFilter);\n params.push(`%${escaped}%`, `%${escaped}%`);\n }\n\n sql += \" ORDER BY id DESC LIMIT ?\";\n params.push(limit);\n\n const stmt = this.db.prepare(sql);\n return stmt.all(...params) as TelemetryEvent[];\n }\n\n /**\n * Get aggregated metrics for the dashboard, including duration percentiles.\n * Percentiles are computed over exchanges from the last 5 minutes.\n */\n getMetrics(): import(\"./types.js\").Metrics {\n const s = this.stmt(\n \"metrics\",\n `SELECT\n (SELECT COUNT(DISTINCT id) FROM routes) AS totalRoutes,\n (SELECT COUNT(*) FROM exchanges) AS totalExchanges,\n (SELECT COUNT(*) FROM exchanges WHERE status = 'completed') AS completedExchanges,\n (SELECT COUNT(*) FROM exchanges WHERE status = 'failed') AS failedExchanges,\n (SELECT COUNT(*) FROM exchanges WHERE status = 'dropped') AS droppedExchanges,\n (SELECT AVG(duration_ms) FROM exchanges\n WHERE started_at >= datetime('now', '-5 minutes')) AS avgDurationMs`,\n );\n const row = s.get() as {\n totalRoutes: number;\n totalExchanges: number;\n completedExchanges: number;\n failedExchanges: number;\n droppedExchanges: number;\n avgDurationMs: number | null;\n };\n\n // Compute percentiles from recent exchanges with non-null durations\n const pctStmt = this.stmt(\n \"percentiles\",\n `SELECT duration_ms\n FROM exchanges\n WHERE duration_ms IS NOT NULL\n AND started_at >= datetime('now', '-5 minutes')\n ORDER BY duration_ms ASC`,\n );\n const durations = (pctStmt.all() as Array<{ duration_ms: number }>).map(\n (r) => r.duration_ms,\n );\n\n return {\n ...row,\n errorRate:\n row.totalExchanges > 0 ? row.failedExchanges / row.totalExchanges : 0,\n p90DurationMs: percentile(durations, 0.9),\n p95DurationMs: percentile(durations, 0.95),\n p99DurationMs: percentile(durations, 0.99),\n };\n }\n\n /**\n * Get exchange counts for a rolling window using fine-grained buckets.\n * Default: 60 buckets of 5 seconds = 5-minute rolling window.\n * Returns values oldest-first for sparkline rendering.\n */\n getLiveTrafficBuckets(bucketCount = 60, bucketWidthSec = 5): number[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const snapped = Math.floor(nowSec / bucketWidthSec) * bucketWidthSec;\n const windowStart = snapped - (bucketCount - 1) * bucketWidthSec;\n\n const s = this.stmt(\n \"liveTraffic\",\n `SELECT\n CAST((CAST(strftime('%s', started_at) AS INTEGER) - ?) / ? AS INTEGER) AS bucket,\n COUNT(*) AS cnt\n FROM exchanges\n WHERE CAST(strftime('%s', started_at) AS INTEGER) >= ?\n AND CAST(strftime('%s', started_at) AS INTEGER) <= ?\n GROUP BY bucket`,\n );\n const rows = s.all(\n windowStart,\n bucketWidthSec,\n windowStart,\n snapped + bucketWidthSec,\n ) as Array<{ bucket: number; cnt: number }>;\n\n const result = new Array(bucketCount).fill(0) as number[];\n for (const row of rows) {\n if (row.bucket >= 0 && row.bucket < bucketCount) {\n result[row.bucket] += row.cnt;\n }\n }\n return result;\n }\n\n /**\n * Get activity data for a single route: throughput sparkline + recent error count.\n */\n getSingleRouteActivity(\n routeId: string,\n bucketCount = 12,\n bucketWidthSec = 5,\n ): { throughput: number[]; recentErrors: number } {\n const nowSec = Math.floor(Date.now() / 1000);\n const snapped = Math.floor(nowSec / bucketWidthSec) * bucketWidthSec;\n const windowStart = snapped - (bucketCount - 1) * bucketWidthSec;\n\n const tStmt = this.stmt(\n \"singleRouteActivity\",\n `SELECT\n CAST((CAST(strftime('%s', started_at) AS INTEGER) - ?) / ? AS INTEGER) AS bucket,\n COUNT(*) AS cnt\n FROM exchanges\n WHERE route_id = ?\n AND CAST(strftime('%s', started_at) AS INTEGER) >= ?\n GROUP BY bucket`,\n );\n const tRows = tStmt.all(\n windowStart,\n bucketWidthSec,\n routeId,\n windowStart,\n ) as Array<{ bucket: number; cnt: number }>;\n\n const eStmt = this.stmt(\n \"singleRouteErrors\",\n `SELECT COUNT(*) AS cnt\n FROM exchanges\n WHERE route_id = ?\n AND status = 'failed'\n AND CAST(strftime('%s', started_at) AS INTEGER) >= ?`,\n );\n const eRow = eStmt.get(routeId, windowStart) as { cnt: number } | undefined;\n\n const throughput = new Array(bucketCount).fill(0) as number[];\n for (const row of tRows) {\n if (row.bucket >= 0 && row.bucket < bucketCount) {\n throughput[row.bucket] = row.cnt;\n }\n }\n\n return { throughput, recentErrors: eRow?.cnt ?? 0 };\n }\n\n /**\n * Get the maximum event ID (for tailing/polling).\n */\n getMaxEventId(): number {\n const stmt = this.db.prepare(\n \"SELECT COALESCE(MAX(id), 0) AS maxId FROM events\",\n );\n const row = stmt.get() as { maxId: number };\n return row.maxId;\n }\n\n /**\n * Get the exchange snapshot (headers and body) for a given exchange.\n * Returns null if no snapshot was captured.\n */\n getExchangeSnapshot(\n exchangeId: string,\n ): import(\"./types.js\").ExchangeSnapshot | null {\n try {\n const s = this.stmt(\n \"exchangeSnapshot\",\n `SELECT headers, body, truncated\n FROM exchange_snapshots\n WHERE exchange_id = ?\n LIMIT 1`,\n );\n const row = s.get(exchangeId) as\n | { headers: string; body: string | null; truncated: number }\n | undefined;\n if (!row) return null;\n return {\n headers: row.headers,\n body: row.body,\n truncated: row.truncated !== 0,\n };\n } catch {\n // Table may not exist in very old databases\n return null;\n }\n }\n\n /**\n * Close the database connection.\n */\n close(): void {\n this.db.close();\n }\n}\n\n/**\n * Compute a percentile from a sorted array of numbers using linear interpolation.\n * Returns null if the array is empty.\n */\nfunction percentile(sorted: number[], p: number): number | null {\n if (sorted.length === 0) return null;\n const idx = p * (sorted.length - 1);\n const lo = Math.floor(idx);\n const hi = Math.ceil(idx);\n if (lo === hi) return sorted[lo]!;\n return sorted[lo]! + (sorted[hi]! - sorted[lo]!) * (idx - lo);\n}\n"]}
@@ -1,2 +0,0 @@
1
- var r="0.6.0-canary.8";export{r as a};//# sourceMappingURL=chunk-R2ZD52FM.js.map
2
- //# sourceMappingURL=chunk-R2ZD52FM.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../package.json"],"names":["version"],"mappings":"AAEE,IAAAA,CAAAA,CAAW","file":"chunk-R2ZD52FM.js","sourcesContent":["{\n \"name\": \"@routecraft/cli\",\n \"version\": \"0.6.0-canary.8\",\n \"description\": \"CLI for running Routecraft capabilities\",\n \"private\": false,\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\"\n }\n },\n \"files\": [\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"tsup src/index.ts src/tui/app.tsx src/tui/db.ts --format esm --dts --external react --external ink --external bun:sqlite\",\n \"test\": \"vitest\",\n \"test:coverage\": \"vitest --coverage\",\n \"prepublishOnly\": \"bun run build\"\n },\n \"dependencies\": {\n \"@opentelemetry/sdk-trace-base\": \"^2.7.1\",\n \"@routecraft/routecraft\": \"^0.6.0-canary.8\",\n \"agent-browser\": \"^0.17.1\",\n \"cheerio\": \"^1.2.0\",\n \"commander\": \"^14.0.3\",\n \"croner\": \"^10.0.1\",\n \"dotenv\": \"^17.4.2\",\n \"imapflow\": \"^1.3.3\",\n \"ink\": \"^5.2.1\",\n \"jose\": \"^6.2.3\",\n \"mailparser\": \"^3.9.8\",\n \"nodemailer\": \"^6.10.1\",\n \"papaparse\": \"^5.5.3\",\n \"pino-pretty\": \"^13.1.3\",\n \"react\": \"^18.3.1\"\n },\n \"devDependencies\": {\n \"@types/mailparser\": \"^3.4.6\",\n \"@types/nodemailer\": \"^6.4.23\",\n \"@types/react\": \"^18.3.28\",\n \"ink-testing-library\": \"^4.0.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"repository\": \"https://github.com/routecraftjs/routecraft\",\n \"bin\": {\n \"craft\": \"./dist/index.js\"\n },\n \"engines\": {\n \"bun\": \">=1.1.0\"\n },\n \"keywords\": [\n \"routecraft\",\n \"cli\",\n \"routes\",\n \"automation\",\n \"typescript\"\n ],\n \"author\": \"routecraftjs\",\n \"license\": \"Apache-2.0\",\n \"homepage\": \"https://routecraft.dev\"\n}\n"]}
@@ -1,2 +0,0 @@
1
- import {resolve,extname}from'path';import {logger,ContextBuilder,RUNNER_ARGV,shutdownHandler,isRouteBuilder,isRouteDefinition}from'@routecraft/routecraft';var d=[".mjs",".js",".cjs",".ts"];async function h(t,e=[]){let r=resolve(process.cwd(),t),l=extname(r);if(!d.includes(l))return {success:false,code:1,message:`Error: Only the following file types are supported: ${d.join(", ")}`};try{let s=await import(r),i=s.craftConfig;logger.info(`Loading file: ${r}`);let n=new ContextBuilder;i&&(logger.info("Found craftConfig export, applying configuration"),n.with(i));let a=w(n,s.default);if(!a.success)return a;let{context:u}=await n.build();return u.setStore(RUNNER_ARGV,e),shutdownHandler(u),await u.start(),{success:!0}}catch(s){return s instanceof Error?(logger.error(`Failed to run ${r}: ${s.message}`),{success:false,code:1,message:s.message}):(logger.error(`Failed to run ${r}: Unknown error occurred`),{success:false,code:1,message:"Unknown error"})}}function w(t,e){return e?isRouteBuilder(e)?(t.routes(e),logger.info("Loaded single RouteBuilder from default export"),{success:true}):isRouteDefinition(e)?(t.routes(e),logger.info("Loaded single route from default export"),{success:true}):Array.isArray(e)?e.every(r=>isRouteBuilder(r)||isRouteDefinition(r))?(e.forEach(r=>t.routes(r)),logger.info(`Loaded ${e.length} routes from default export array`),{success:true}):(logger.error("All items in default export array must be RouteDefinition or RouteBuilder"),{success:false,code:1,message:"Invalid items in default export array"}):(logger.error("Invalid default export. Expected: RouteDefinition, RouteBuilder, or array of those."),{success:false,code:1,message:"Invalid default export. Expected: RouteDefinition, RouteBuilder, or array of those."}):(logger.error("No default export found. Expected routes as default export."),{success:false,code:1,message:"No default export found"})}export{h as runCommand};//# sourceMappingURL=run-3Y3G7OTJ.js.map
2
- //# sourceMappingURL=run-3Y3G7OTJ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/run.ts"],"names":["SUPPORTED_EXTENSIONS","runCommand","filePath","cliArgs","absFilePath","resolve","ext","extname","module","craftConfig","logger","contextBuilder","ContextBuilder","configured","configureRoutes","context","RUNNER_ARGV","shutdownHandler","error","defaultExport","isRouteBuilder","isRouteDefinition","item","routeOrBuilder"],"mappings":"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","file":"run-3Y3G7OTJ.js","sourcesContent":["import { resolve, extname } from \"node:path\";\nimport {\n ContextBuilder,\n type CraftConfig,\n isRouteBuilder,\n isRouteDefinition,\n logger,\n shutdownHandler,\n RUNNER_ARGV,\n type RouteBuilder,\n type RouteDefinition,\n} from \"@routecraft/routecraft\";\n\nconst SUPPORTED_EXTENSIONS = [\".mjs\", \".js\", \".cjs\", \".ts\"] as const;\n\ntype RunResult =\n | { success: true }\n | { success: false; code?: number; message: string };\n\n/**\n * Load a routecraft file, build a context, and start it.\n *\n * Adapter-agnostic: the runner knows nothing about which adapters (CLI, HTTP,\n * cron, etc.) are used. It sets `RUNNER_ARGV` in the context store so that\n * adapters can read remaining CLI tokens if needed.\n *\n * @param filePath - Path to the routecraft file to run\n * @param cliArgs - Remaining CLI arguments after the file path\n */\nexport async function runCommand(\n filePath: string,\n cliArgs: string[] = [],\n): Promise<RunResult> {\n const absFilePath = resolve(process.cwd(), filePath);\n const ext = extname(absFilePath);\n\n // Validate file extension\n if (\n !SUPPORTED_EXTENSIONS.includes(ext as (typeof SUPPORTED_EXTENSIONS)[number])\n ) {\n return {\n success: false,\n code: 1,\n message: `Error: Only the following file types are supported: ${SUPPORTED_EXTENSIONS.join(\", \")}`,\n };\n }\n\n try {\n // Load the module (CLI already set LOG_LEVEL / LOG_FILE from argv in index.ts).\n // Logger uses env first; context will apply craftConfig.log when built (env wins if set).\n const module = await import(absFilePath);\n const craftConfig = module.craftConfig as CraftConfig | undefined;\n\n logger.info(`Loading file: ${absFilePath}`);\n\n // Create context builder\n const contextBuilder = new ContextBuilder();\n\n // Apply craftConfig (routes, plugins, etc.); context applies config.log when built.\n if (craftConfig) {\n logger.info(\"Found craftConfig export, applying configuration\");\n contextBuilder.with(craftConfig);\n }\n\n // Handle routes from the default export\n const configured = configureRoutes(contextBuilder, module.default);\n if (!configured.success) {\n return configured;\n }\n\n // Build and start the context. Adapters handle their own lifecycle.\n // RUNNER_ARGV lets adapters (e.g. CLI) read remaining args without\n // the runner needing to know which adapters are in use.\n const { context } = await contextBuilder.build();\n context.setStore(RUNNER_ARGV, cliArgs);\n shutdownHandler(context);\n await context.start();\n\n return { success: true };\n } catch (error: unknown) {\n if (error instanceof Error) {\n logger.error(`Failed to run ${absFilePath}: ${error.message}`);\n return { success: false, code: 1, message: error.message };\n }\n logger.error(`Failed to run ${absFilePath}: Unknown error occurred`);\n return { success: false, code: 1, message: \"Unknown error\" };\n }\n}\n\nfunction configureRoutes(\n contextBuilder: InstanceType<typeof ContextBuilder>,\n defaultExport: unknown,\n): RunResult {\n if (!defaultExport) {\n logger.error(\"No default export found. Expected routes as default export.\");\n return { success: false, code: 1, message: \"No default export found\" };\n }\n\n // Handle single RouteBuilder or RouteDefinition (brand-based guards for cross-instance)\n if (isRouteBuilder(defaultExport)) {\n contextBuilder.routes(defaultExport as RouteBuilder<unknown>);\n logger.info(\"Loaded single RouteBuilder from default export\");\n return { success: true };\n }\n\n if (isRouteDefinition(defaultExport)) {\n contextBuilder.routes(defaultExport as RouteDefinition);\n logger.info(\"Loaded single route from default export\");\n return { success: true };\n }\n\n // Handle array of routes\n if (Array.isArray(defaultExport)) {\n // Check each item, prioritizing RouteBuilder check\n if (\n !defaultExport.every(\n (item) => isRouteBuilder(item) || isRouteDefinition(item),\n )\n ) {\n logger.error(\n \"All items in default export array must be RouteDefinition or RouteBuilder\",\n );\n return {\n success: false,\n code: 1,\n message: \"Invalid items in default export array\",\n };\n }\n\n defaultExport.forEach((routeOrBuilder) =>\n contextBuilder.routes(\n routeOrBuilder as RouteDefinition | RouteBuilder<unknown>,\n ),\n );\n logger.info(\n `Loaded ${defaultExport.length} routes from default export array`,\n );\n return { success: true };\n }\n\n // Invalid default export\n logger.error(\n \"Invalid default export. Expected: RouteDefinition, RouteBuilder, or array of those.\",\n );\n return {\n success: false,\n code: 1,\n message:\n \"Invalid default export. Expected: RouteDefinition, RouteBuilder, or array of those.\",\n };\n}\n"]}