@alejandroroman/agent-kit 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.
Files changed (169) hide show
  1. package/dist/_memory/config.d.ts +14 -0
  2. package/dist/_memory/config.js +16 -0
  3. package/dist/_memory/db/client.d.ts +2 -0
  4. package/dist/_memory/db/client.js +15 -0
  5. package/dist/_memory/db/schema.d.ts +14 -0
  6. package/dist/_memory/db/schema.js +51 -0
  7. package/dist/_memory/embeddings/ollama.d.ts +12 -0
  8. package/dist/_memory/embeddings/ollama.js +22 -0
  9. package/dist/_memory/embeddings/provider.d.ts +4 -0
  10. package/dist/_memory/embeddings/provider.js +1 -0
  11. package/dist/_memory/index.d.ts +10 -0
  12. package/dist/_memory/index.js +6 -0
  13. package/dist/_memory/search.d.ts +30 -0
  14. package/dist/_memory/search.js +121 -0
  15. package/dist/_memory/server.d.ts +8 -0
  16. package/dist/_memory/server.js +126 -0
  17. package/dist/_memory/store.d.ts +51 -0
  18. package/dist/_memory/store.js +115 -0
  19. package/dist/agent/loop.d.ts +3 -0
  20. package/dist/agent/loop.js +195 -0
  21. package/dist/agent/setup.d.ts +6 -0
  22. package/dist/agent/setup.js +11 -0
  23. package/dist/agent/soul.d.ts +1 -0
  24. package/dist/agent/soul.js +8 -0
  25. package/dist/agent/types.d.ts +23 -0
  26. package/dist/agent/types.js +1 -0
  27. package/dist/api/agents.d.ts +2 -0
  28. package/dist/api/agents.js +43 -0
  29. package/dist/api/config.d.ts +2 -0
  30. package/dist/api/config.js +20 -0
  31. package/dist/api/cron.d.ts +2 -0
  32. package/dist/api/cron.js +15 -0
  33. package/dist/api/health.d.ts +2 -0
  34. package/dist/api/health.js +8 -0
  35. package/dist/api/logs.d.ts +5 -0
  36. package/dist/api/logs.js +28 -0
  37. package/dist/api/router.d.ts +6 -0
  38. package/dist/api/router.js +80 -0
  39. package/dist/api/sessions.d.ts +2 -0
  40. package/dist/api/sessions.js +67 -0
  41. package/dist/api/types.d.ts +12 -0
  42. package/dist/api/types.js +13 -0
  43. package/dist/api/usage.d.ts +3 -0
  44. package/dist/api/usage.js +50 -0
  45. package/dist/bootstrap.d.ts +51 -0
  46. package/dist/bootstrap.js +110 -0
  47. package/dist/cli/chat.d.ts +1 -0
  48. package/dist/cli/chat.js +102 -0
  49. package/dist/cli/config-writer.d.ts +40 -0
  50. package/dist/cli/config-writer.js +108 -0
  51. package/dist/cli/create.d.ts +1 -0
  52. package/dist/cli/create.js +37 -0
  53. package/dist/cli/init.d.ts +1 -0
  54. package/dist/cli/init.js +85 -0
  55. package/dist/cli/list.d.ts +1 -0
  56. package/dist/cli/list.js +36 -0
  57. package/dist/cli/ollama.d.ts +6 -0
  58. package/dist/cli/ollama.js +44 -0
  59. package/dist/cli/setup-agent/index.d.ts +9 -0
  60. package/dist/cli/setup-agent/index.js +100 -0
  61. package/dist/cli/setup-agent/soul.d.ts +2 -0
  62. package/dist/cli/setup-agent/soul.js +79 -0
  63. package/dist/cli/setup-agent/tools.d.ts +9 -0
  64. package/dist/cli/setup-agent/tools.js +362 -0
  65. package/dist/cli/start.d.ts +1 -0
  66. package/dist/cli/start.js +235 -0
  67. package/dist/cli/ui.d.ts +17 -0
  68. package/dist/cli/ui.js +79 -0
  69. package/dist/cli/validate.d.ts +1 -0
  70. package/dist/cli/validate.js +47 -0
  71. package/dist/cli.d.ts +4 -0
  72. package/dist/cli.js +59 -0
  73. package/dist/config/index.d.ts +4 -0
  74. package/dist/config/index.js +3 -0
  75. package/dist/config/loader.d.ts +2 -0
  76. package/dist/config/loader.js +10 -0
  77. package/dist/config/resolve.d.ts +22 -0
  78. package/dist/config/resolve.js +45 -0
  79. package/dist/config/schema.d.ts +217 -0
  80. package/dist/config/schema.js +159 -0
  81. package/dist/cron/scheduler.d.ts +22 -0
  82. package/dist/cron/scheduler.js +115 -0
  83. package/dist/gateways/slack/client.d.ts +13 -0
  84. package/dist/gateways/slack/client.js +44 -0
  85. package/dist/gateways/slack/format.d.ts +30 -0
  86. package/dist/gateways/slack/format.js +170 -0
  87. package/dist/gateways/slack/handler.d.ts +9 -0
  88. package/dist/gateways/slack/handler.js +95 -0
  89. package/dist/gateways/slack/index.d.ts +16 -0
  90. package/dist/gateways/slack/index.js +102 -0
  91. package/dist/gateways/slack/listener.d.ts +10 -0
  92. package/dist/gateways/slack/listener.js +35 -0
  93. package/dist/gateways/slack/sessions.d.ts +11 -0
  94. package/dist/gateways/slack/sessions.js +32 -0
  95. package/dist/gateways/slack/types.d.ts +13 -0
  96. package/dist/gateways/slack/types.js +7 -0
  97. package/dist/heartbeat/index.d.ts +2 -0
  98. package/dist/heartbeat/index.js +1 -0
  99. package/dist/heartbeat/runner.d.ts +31 -0
  100. package/dist/heartbeat/runner.js +215 -0
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +207 -0
  103. package/dist/llm/anthropic.d.ts +12 -0
  104. package/dist/llm/anthropic.js +89 -0
  105. package/dist/llm/fallback.d.ts +6 -0
  106. package/dist/llm/fallback.js +30 -0
  107. package/dist/llm/index.d.ts +9 -0
  108. package/dist/llm/index.js +40 -0
  109. package/dist/llm/openai.d.ts +12 -0
  110. package/dist/llm/openai.js +85 -0
  111. package/dist/llm/provider.d.ts +12 -0
  112. package/dist/llm/provider.js +9 -0
  113. package/dist/llm/types.d.ts +73 -0
  114. package/dist/llm/types.js +6 -0
  115. package/dist/logger.d.ts +2 -0
  116. package/dist/logger.js +15 -0
  117. package/dist/multi/registry.d.ts +15 -0
  118. package/dist/multi/registry.js +28 -0
  119. package/dist/multi/spawn.d.ts +14 -0
  120. package/dist/multi/spawn.js +14 -0
  121. package/dist/scripts/validate-agent-cli.d.ts +1 -0
  122. package/dist/scripts/validate-agent-cli.js +47 -0
  123. package/dist/scripts/validate-agent.d.ts +17 -0
  124. package/dist/scripts/validate-agent.js +242 -0
  125. package/dist/session/compaction.d.ts +4 -0
  126. package/dist/session/compaction.js +30 -0
  127. package/dist/session/manager.d.ts +9 -0
  128. package/dist/session/manager.js +41 -0
  129. package/dist/skills/activate.d.ts +11 -0
  130. package/dist/skills/activate.js +62 -0
  131. package/dist/skills/index.d.ts +3 -0
  132. package/dist/skills/index.js +3 -0
  133. package/dist/skills/loader.d.ts +3 -0
  134. package/dist/skills/loader.js +20 -0
  135. package/dist/skills/schema.d.ts +8 -0
  136. package/dist/skills/schema.js +7 -0
  137. package/dist/text.d.ts +8 -0
  138. package/dist/text.js +24 -0
  139. package/dist/tools/builtin/index.d.ts +21 -0
  140. package/dist/tools/builtin/index.js +21 -0
  141. package/dist/tools/builtin/memory.d.ts +8 -0
  142. package/dist/tools/builtin/memory.js +164 -0
  143. package/dist/tools/builtin/read-file.d.ts +3 -0
  144. package/dist/tools/builtin/read-file.js +30 -0
  145. package/dist/tools/builtin/run-command.d.ts +3 -0
  146. package/dist/tools/builtin/run-command.js +37 -0
  147. package/dist/tools/builtin/spawn.d.ts +15 -0
  148. package/dist/tools/builtin/spawn.js +59 -0
  149. package/dist/tools/builtin/web-search.d.ts +8 -0
  150. package/dist/tools/builtin/web-search.js +99 -0
  151. package/dist/tools/builtin/write-file.d.ts +3 -0
  152. package/dist/tools/builtin/write-file.js +34 -0
  153. package/dist/tools/registry.d.ts +10 -0
  154. package/dist/tools/registry.js +26 -0
  155. package/dist/tools/sandbox.d.ts +9 -0
  156. package/dist/tools/sandbox.js +74 -0
  157. package/dist/tools/types.d.ts +8 -0
  158. package/dist/tools/types.js +7 -0
  159. package/dist/usage/index.d.ts +4 -0
  160. package/dist/usage/index.js +3 -0
  161. package/dist/usage/pricing.d.ts +10 -0
  162. package/dist/usage/pricing.js +35 -0
  163. package/dist/usage/schema.d.ts +1 -0
  164. package/dist/usage/schema.js +45 -0
  165. package/dist/usage/store.d.ts +10 -0
  166. package/dist/usage/store.js +227 -0
  167. package/dist/usage/types.d.ts +61 -0
  168. package/dist/usage/types.js +1 -0
  169. package/package.json +53 -0
@@ -0,0 +1,99 @@
1
+ const XAI_API_ENDPOINT = "https://api.x.ai/v1/responses";
2
+ const DEFAULT_GROK_MODEL = "grok-4-1-fast";
3
+ function extractGrokContent(data) {
4
+ for (const output of data.output ?? []) {
5
+ if (output.type !== "message")
6
+ continue;
7
+ for (const block of output.content ?? []) {
8
+ if (block.type === "output_text" && block.text) {
9
+ const urls = (block.annotations ?? [])
10
+ .filter((a) => a.type === "url_citation" && a.url)
11
+ .map((a) => a.url);
12
+ return { text: block.text, citations: [...new Set(urls)] };
13
+ }
14
+ }
15
+ }
16
+ return { text: data.output_text ?? "No results found.", citations: [] };
17
+ }
18
+ export function createWebSearchTool(options) {
19
+ const provider = options?.provider ?? "brave";
20
+ const description = provider === "grok"
21
+ ? "Search the web using xAI Grok. Returns AI-synthesized answers with citations from real-time web search. The count parameter is ignored for this provider."
22
+ : "Search the web using Brave Search. Returns titles, URLs, and snippets for the top results.";
23
+ return {
24
+ name: "web_search",
25
+ description,
26
+ parameters: {
27
+ type: "object",
28
+ properties: {
29
+ query: { type: "string", description: "Search query" },
30
+ count: {
31
+ type: "number",
32
+ description: "Number of results to return (default 5)",
33
+ },
34
+ },
35
+ required: ["query"],
36
+ },
37
+ execute: async (args) => {
38
+ try {
39
+ const query = args.query;
40
+ const rawCount = args.count ?? 5;
41
+ const count = Math.max(1, Math.min(20, Math.floor(rawCount)));
42
+ if (provider === "grok") {
43
+ const apiKey = process.env.XAI_API_KEY;
44
+ if (!apiKey)
45
+ return "Error: XAI_API_KEY is not set";
46
+ const model = options?.grok?.model ?? DEFAULT_GROK_MODEL;
47
+ const res = await fetch(XAI_API_ENDPOINT, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ Authorization: `Bearer ${apiKey}`,
52
+ },
53
+ body: JSON.stringify({
54
+ model,
55
+ input: [{ role: "user", content: query }],
56
+ tools: [{ type: "web_search" }],
57
+ }),
58
+ });
59
+ if (!res.ok)
60
+ return `Error: xAI API returned ${res.status} ${res.statusText}`;
61
+ const data = (await res.json());
62
+ const { text, citations } = extractGrokContent(data);
63
+ const parts = [text];
64
+ if (citations.length > 0) {
65
+ parts.push("\nSources:\n" + citations.map((u) => `- ${u}`).join("\n"));
66
+ }
67
+ return parts.join("\n");
68
+ }
69
+ const apiKey = process.env.BRAVE_SEARCH_API_KEY;
70
+ if (!apiKey) {
71
+ return "Error: BRAVE_SEARCH_API_KEY is not set";
72
+ }
73
+ const url = new URL("https://api.search.brave.com/res/v1/web/search");
74
+ url.searchParams.set("q", query);
75
+ url.searchParams.set("count", String(count));
76
+ const res = await fetch(url.toString(), {
77
+ headers: {
78
+ "X-Subscription-Token": apiKey,
79
+ Accept: "application/json",
80
+ },
81
+ });
82
+ if (!res.ok) {
83
+ return `Error: Brave Search API returned ${res.status} ${res.statusText}`;
84
+ }
85
+ const data = (await res.json());
86
+ const results = data.web?.results ?? [];
87
+ if (results.length === 0) {
88
+ return "No results found.";
89
+ }
90
+ return results
91
+ .map((r) => `${r.title}\n${r.url}\n${r.description}`)
92
+ .join("\n\n");
93
+ }
94
+ catch (err) {
95
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
96
+ }
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,3 @@
1
+ import type { Tool } from "../types.js";
2
+ export declare function createWriteFileTool(allowedPaths?: string[]): Tool;
3
+ export declare const writeFileTool: Tool;
@@ -0,0 +1,34 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { validatePath } from "../sandbox.js";
4
+ export function createWriteFileTool(allowedPaths) {
5
+ return {
6
+ name: "write_file",
7
+ description: "Write content to a file. Creates parent directories if needed.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ path: { type: "string", description: "Absolute path to the file" },
12
+ content: { type: "string", description: "Content to write" },
13
+ },
14
+ required: ["path", "content"],
15
+ },
16
+ execute: async (args) => {
17
+ try {
18
+ const filePath = args.path;
19
+ const validation = validatePath(filePath, allowedPaths);
20
+ if (!validation.allowed) {
21
+ return validation.error;
22
+ }
23
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
24
+ fs.writeFileSync(filePath, args.content);
25
+ return `Wrote to ${filePath}`;
26
+ }
27
+ catch (err) {
28
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
29
+ }
30
+ },
31
+ };
32
+ }
33
+ // Backwards-compatible default export (unrestricted)
34
+ export const writeFileTool = createWriteFileTool();
@@ -0,0 +1,10 @@
1
+ import type { Tool } from "./types.js";
2
+ export declare class ToolRegistry {
3
+ private tools;
4
+ register(tool: Tool): void;
5
+ resolve(names: string[]): Tool[];
6
+ list(): string[];
7
+ validateAgents(agents: Record<string, {
8
+ tools: string[];
9
+ }>): void;
10
+ }
@@ -0,0 +1,26 @@
1
+ export class ToolRegistry {
2
+ tools = new Map();
3
+ register(tool) {
4
+ this.tools.set(tool.name, tool);
5
+ }
6
+ resolve(names) {
7
+ return names.map((name) => {
8
+ const tool = this.tools.get(name);
9
+ if (!tool)
10
+ throw new Error(`Unknown tool "${name}"`);
11
+ return tool;
12
+ });
13
+ }
14
+ list() {
15
+ return [...this.tools.keys()];
16
+ }
17
+ validateAgents(agents) {
18
+ for (const [agentName, agent] of Object.entries(agents)) {
19
+ for (const toolName of agent.tools) {
20
+ if (!this.tools.has(toolName)) {
21
+ throw new Error(`Unknown tool "${toolName}" for agent "${agentName}"`);
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,9 @@
1
+ export interface ValidationResult {
2
+ allowed: boolean;
3
+ denied: string[];
4
+ error?: string;
5
+ }
6
+ export declare function parseCommandSegments(command: string): string[];
7
+ export declare function containsShellMetachars(command: string): boolean;
8
+ export declare function validateCommand(command: string, allowedCommands: string[] | undefined): ValidationResult;
9
+ export declare function validatePath(filePath: string, allowedPaths: string[] | undefined): ValidationResult;
@@ -0,0 +1,74 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ export function parseCommandSegments(command) {
4
+ if (!command.trim())
5
+ return [];
6
+ return command
7
+ .split(/\s*(?:\|\||&&|;|\||\r?\n)\s*/)
8
+ .map((seg) => seg.trim().split(/\s+/)[0])
9
+ .filter(Boolean);
10
+ }
11
+ export function containsShellMetachars(command) {
12
+ return /`|\$\(|<\(|>\(|(?:^|[^>&])&(?!&)|[\r\n]/.test(command);
13
+ }
14
+ export function validateCommand(command, allowedCommands) {
15
+ if (!allowedCommands)
16
+ return { allowed: true, denied: [] };
17
+ if (containsShellMetachars(command)) {
18
+ return {
19
+ allowed: false,
20
+ denied: [],
21
+ error: "Error: Shell metacharacters are not allowed in sandboxed commands.",
22
+ };
23
+ }
24
+ const segments = parseCommandSegments(command);
25
+ const denied = segments.filter((cmd) => !allowedCommands.includes(cmd));
26
+ if (denied.length === 0)
27
+ return { allowed: true, denied: [] };
28
+ return {
29
+ allowed: false,
30
+ denied,
31
+ error: `Error: Command "${denied.join('", "')}" is not allowed. Allowed commands: ${allowedCommands.join(", ")}`,
32
+ };
33
+ }
34
+ /**
35
+ * Resolve a path to its canonical form, following symlinks.
36
+ * For paths that don't exist yet (write case), resolve the parent directory
37
+ * and append the basename to prevent symlink escapes.
38
+ */
39
+ function resolveRealPath(filePath) {
40
+ try {
41
+ return fs.realpathSync(filePath);
42
+ }
43
+ catch {
44
+ // File doesn't exist yet — resolve parent + basename
45
+ const dir = path.dirname(filePath);
46
+ try {
47
+ return path.join(fs.realpathSync(dir), path.basename(filePath));
48
+ }
49
+ catch {
50
+ // Parent doesn't exist either — fall back to path.resolve
51
+ return path.resolve(filePath);
52
+ }
53
+ }
54
+ }
55
+ function isPathWithinAllowedPath(realFilePath, realAllowedPath) {
56
+ if (realFilePath === realAllowedPath)
57
+ return true;
58
+ const relative = path.relative(realAllowedPath, realFilePath);
59
+ return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
60
+ }
61
+ export function validatePath(filePath, allowedPaths) {
62
+ if (!allowedPaths)
63
+ return { allowed: true, denied: [] };
64
+ const realFilePath = resolveRealPath(filePath);
65
+ const realAllowedPaths = allowedPaths.map((p) => resolveRealPath(p));
66
+ const allowed = realAllowedPaths.some((ap) => isPathWithinAllowedPath(realFilePath, ap));
67
+ if (allowed)
68
+ return { allowed: true, denied: [] };
69
+ return {
70
+ allowed: false,
71
+ denied: [realFilePath],
72
+ error: `Error: Path "${realFilePath}" is not allowed. Allowed paths: ${realAllowedPaths.join(", ")}`,
73
+ };
74
+ }
@@ -0,0 +1,8 @@
1
+ import type { ToolDefinition } from "../llm/types.js";
2
+ export interface Tool {
3
+ name: string;
4
+ description: string;
5
+ parameters: ToolDefinition["parameters"];
6
+ execute: (args: Record<string, unknown>) => Promise<string>;
7
+ }
8
+ export declare function toolToDefinition(tool: Tool): ToolDefinition;
@@ -0,0 +1,7 @@
1
+ export function toolToDefinition(tool) {
2
+ return {
3
+ name: tool.name,
4
+ description: tool.description,
5
+ parameters: tool.parameters,
6
+ };
7
+ }
@@ -0,0 +1,4 @@
1
+ export { createUsageStore, type UsageStore } from "./store.js";
2
+ export { estimateCost, PRICING } from "./pricing.js";
3
+ export { GROUP_BY_VALUES } from "./types.js";
4
+ export type { LLMCallRecord, AgentRunRecord, CallFilters, RunFilters, GroupBy, SummaryFilters, UsageSummaryRow, } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { createUsageStore } from "./store.js";
2
+ export { estimateCost, PRICING } from "./pricing.js";
3
+ export { GROUP_BY_VALUES } from "./types.js";
@@ -0,0 +1,10 @@
1
+ import type { TokenUsage } from "../llm/types.js";
2
+ interface ModelPricing {
3
+ inputPerMTok: number;
4
+ outputPerMTok: number;
5
+ cacheCreationPerMTok: number;
6
+ cacheReadPerMTok: number;
7
+ }
8
+ declare const PRICING: Record<string, ModelPricing>;
9
+ export declare function estimateCost(model: string, usage: TokenUsage): number;
10
+ export { PRICING };
@@ -0,0 +1,35 @@
1
+ import { createLogger } from "../logger.js";
2
+ const log = createLogger("pricing");
3
+ const PRICING = {
4
+ "anthropic:claude-opus-4-6": {
5
+ inputPerMTok: 5,
6
+ outputPerMTok: 25,
7
+ cacheCreationPerMTok: 6.25,
8
+ cacheReadPerMTok: 0.5,
9
+ },
10
+ "anthropic:claude-sonnet-4-6": {
11
+ inputPerMTok: 3,
12
+ outputPerMTok: 15,
13
+ cacheCreationPerMTok: 3.75,
14
+ cacheReadPerMTok: 0.3,
15
+ },
16
+ "anthropic:claude-haiku-4-5-20251001": {
17
+ inputPerMTok: 1,
18
+ outputPerMTok: 5,
19
+ cacheCreationPerMTok: 1.25,
20
+ cacheReadPerMTok: 0.1,
21
+ },
22
+ };
23
+ export function estimateCost(model, usage) {
24
+ const pricing = PRICING[model];
25
+ if (!pricing) {
26
+ log.warn({ model }, "no pricing data for model");
27
+ return 0;
28
+ }
29
+ return ((usage.inputTokens * pricing.inputPerMTok +
30
+ usage.outputTokens * pricing.outputPerMTok +
31
+ usage.cacheCreationTokens * pricing.cacheCreationPerMTok +
32
+ usage.cacheReadTokens * pricing.cacheReadPerMTok) /
33
+ 1_000_000);
34
+ }
35
+ export { PRICING };
@@ -0,0 +1 @@
1
+ export declare const USAGE_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS llm_calls (\n id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n cache_creation_tokens INTEGER NOT NULL DEFAULT 0,\n cache_read_tokens INTEGER NOT NULL DEFAULT 0,\n reasoning_tokens INTEGER NOT NULL DEFAULT 0,\n stop_reason TEXT NOT NULL,\n latency_ms INTEGER NOT NULL DEFAULT 0,\n estimated_cost_usd REAL NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_llm_calls_run_id ON llm_calls(run_id);\nCREATE INDEX IF NOT EXISTS idx_llm_calls_agent_name ON llm_calls(agent_name);\nCREATE INDEX IF NOT EXISTS idx_llm_calls_model ON llm_calls(model);\nCREATE INDEX IF NOT EXISTS idx_llm_calls_created_at ON llm_calls(created_at);\n\nCREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n agent_name TEXT NOT NULL,\n source TEXT NOT NULL,\n model TEXT NOT NULL,\n total_input_tokens INTEGER NOT NULL DEFAULT 0,\n total_output_tokens INTEGER NOT NULL DEFAULT 0,\n total_cache_creation_tokens INTEGER NOT NULL DEFAULT 0,\n total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,\n total_reasoning_tokens INTEGER NOT NULL DEFAULT 0,\n iterations INTEGER NOT NULL DEFAULT 0,\n tool_calls_count INTEGER NOT NULL DEFAULT 0,\n estimated_cost_usd REAL NOT NULL DEFAULT 0,\n duration_ms INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_agent_runs_agent_name ON agent_runs(agent_name);\nCREATE INDEX IF NOT EXISTS idx_agent_runs_source ON agent_runs(source);\nCREATE INDEX IF NOT EXISTS idx_agent_runs_model ON agent_runs(model);\nCREATE INDEX IF NOT EXISTS idx_agent_runs_created_at ON agent_runs(created_at);\n";
@@ -0,0 +1,45 @@
1
+ export const USAGE_SCHEMA_SQL = `
2
+ CREATE TABLE IF NOT EXISTS llm_calls (
3
+ id TEXT PRIMARY KEY,
4
+ run_id TEXT NOT NULL,
5
+ agent_name TEXT NOT NULL,
6
+ provider TEXT NOT NULL,
7
+ model TEXT NOT NULL,
8
+ input_tokens INTEGER NOT NULL DEFAULT 0,
9
+ output_tokens INTEGER NOT NULL DEFAULT 0,
10
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
11
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
12
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0,
13
+ stop_reason TEXT NOT NULL,
14
+ latency_ms INTEGER NOT NULL DEFAULT 0,
15
+ estimated_cost_usd REAL NOT NULL DEFAULT 0,
16
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_llm_calls_run_id ON llm_calls(run_id);
20
+ CREATE INDEX IF NOT EXISTS idx_llm_calls_agent_name ON llm_calls(agent_name);
21
+ CREATE INDEX IF NOT EXISTS idx_llm_calls_model ON llm_calls(model);
22
+ CREATE INDEX IF NOT EXISTS idx_llm_calls_created_at ON llm_calls(created_at);
23
+
24
+ CREATE TABLE IF NOT EXISTS agent_runs (
25
+ id TEXT PRIMARY KEY,
26
+ agent_name TEXT NOT NULL,
27
+ source TEXT NOT NULL,
28
+ model TEXT NOT NULL,
29
+ total_input_tokens INTEGER NOT NULL DEFAULT 0,
30
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
31
+ total_cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
32
+ total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
33
+ total_reasoning_tokens INTEGER NOT NULL DEFAULT 0,
34
+ iterations INTEGER NOT NULL DEFAULT 0,
35
+ tool_calls_count INTEGER NOT NULL DEFAULT 0,
36
+ estimated_cost_usd REAL NOT NULL DEFAULT 0,
37
+ duration_ms INTEGER NOT NULL DEFAULT 0,
38
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
39
+ );
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_agent_name ON agent_runs(agent_name);
42
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_source ON agent_runs(source);
43
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_model ON agent_runs(model);
44
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_created_at ON agent_runs(created_at);
45
+ `;
@@ -0,0 +1,10 @@
1
+ import type { LLMCallRecord, AgentRunRecord, CallFilters, RunFilters, SummaryFilters, UsageSummaryRow } from "./types.js";
2
+ export interface UsageStore {
3
+ recordCall(call: LLMCallRecord): void;
4
+ recordRun(run: AgentRunRecord): void;
5
+ getCalls(filters: CallFilters): LLMCallRecord[];
6
+ getRuns(filters: RunFilters): AgentRunRecord[];
7
+ getSummary(filters: SummaryFilters): UsageSummaryRow[];
8
+ close(): void;
9
+ }
10
+ export declare function createUsageStore(dbPath: string): UsageStore;
@@ -0,0 +1,227 @@
1
+ import Database from "better-sqlite3";
2
+ import { nanoid } from "nanoid";
3
+ import { USAGE_SCHEMA_SQL } from "./schema.js";
4
+ import { estimateCost } from "./pricing.js";
5
+ export function createUsageStore(dbPath) {
6
+ const db = new Database(dbPath);
7
+ db.pragma("journal_mode = WAL");
8
+ db.exec(USAGE_SCHEMA_SQL);
9
+ const insertCall = db.prepare(`
10
+ INSERT INTO llm_calls (id, run_id, agent_name, provider, model, input_tokens, output_tokens,
11
+ cache_creation_tokens, cache_read_tokens, reasoning_tokens,
12
+ stop_reason, latency_ms, estimated_cost_usd)
13
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
14
+ `);
15
+ const insertRun = db.prepare(`
16
+ INSERT INTO agent_runs (id, agent_name, source, model, total_input_tokens, total_output_tokens,
17
+ total_cache_creation_tokens, total_cache_read_tokens, total_reasoning_tokens,
18
+ iterations, tool_calls_count, estimated_cost_usd, duration_ms)
19
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
20
+ `);
21
+ return {
22
+ recordCall(call) {
23
+ const id = call.id ?? nanoid();
24
+ const cost = call.estimatedCostUsd ?? estimateCost(call.model, call.usage);
25
+ insertCall.run(id, call.runId, call.agentName, call.provider, call.model, call.usage.inputTokens, call.usage.outputTokens, call.usage.cacheCreationTokens, call.usage.cacheReadTokens, call.usage.reasoningTokens, call.stopReason, call.latencyMs, cost);
26
+ },
27
+ recordRun(run) {
28
+ const cost = run.estimatedCostUsd ?? estimateCost(run.model, run.usage);
29
+ insertRun.run(run.id, run.agentName, run.source, run.model, run.usage.inputTokens, run.usage.outputTokens, run.usage.cacheCreationTokens, run.usage.cacheReadTokens, run.usage.reasoningTokens, run.iterations, run.toolCallsCount, cost, run.durationMs);
30
+ },
31
+ getCalls(filters) {
32
+ const conditions = [];
33
+ const params = [];
34
+ if (filters.model) {
35
+ conditions.push("model = ?");
36
+ params.push(filters.model);
37
+ }
38
+ if (filters.agentName) {
39
+ conditions.push("agent_name = ?");
40
+ params.push(filters.agentName);
41
+ }
42
+ if (filters.runId) {
43
+ conditions.push("run_id = ?");
44
+ params.push(filters.runId);
45
+ }
46
+ if (filters.from) {
47
+ conditions.push("created_at >= ?");
48
+ params.push(filters.from);
49
+ }
50
+ if (filters.to) {
51
+ conditions.push("created_at <= ?");
52
+ params.push(filters.to);
53
+ }
54
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
55
+ const limit = filters.limit ?? 50;
56
+ const offset = filters.offset ?? 0;
57
+ const rows = db.prepare(`SELECT * FROM llm_calls ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
58
+ return rows.map(rowToCallRecord);
59
+ },
60
+ getRuns(filters) {
61
+ const conditions = [];
62
+ const params = [];
63
+ if (filters.agentName) {
64
+ conditions.push("agent_name = ?");
65
+ params.push(filters.agentName);
66
+ }
67
+ if (filters.source) {
68
+ conditions.push("source = ?");
69
+ params.push(filters.source);
70
+ }
71
+ if (filters.model) {
72
+ conditions.push("model = ?");
73
+ params.push(filters.model);
74
+ }
75
+ if (filters.from) {
76
+ conditions.push("created_at >= ?");
77
+ params.push(filters.from);
78
+ }
79
+ if (filters.to) {
80
+ conditions.push("created_at <= ?");
81
+ params.push(filters.to);
82
+ }
83
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
84
+ const limit = filters.limit ?? 50;
85
+ const offset = filters.offset ?? 0;
86
+ const rows = db.prepare(`SELECT * FROM agent_runs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
87
+ return rows.map(rowToRunRecord);
88
+ },
89
+ getSummary(filters) {
90
+ const conditions = [];
91
+ const params = [];
92
+ if (filters.from) {
93
+ conditions.push("c.created_at >= ?");
94
+ params.push(filters.from);
95
+ }
96
+ if (filters.to) {
97
+ conditions.push("c.created_at <= ?");
98
+ params.push(filters.to);
99
+ }
100
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
101
+ const groupCol = (() => {
102
+ switch (filters.groupBy) {
103
+ case "model": return "c.model";
104
+ case "agent": return "c.agent_name";
105
+ case "source": return "r.source";
106
+ case "day": return "date(c.created_at)";
107
+ default: return "'all'";
108
+ }
109
+ })();
110
+ if (filters.groupBy === "source") {
111
+ const rows = db.prepare(`
112
+ SELECT r.source as grp,
113
+ SUM(c.input_tokens) as total_input,
114
+ SUM(c.output_tokens) as total_output,
115
+ SUM(c.cache_creation_tokens) as total_cache_creation,
116
+ SUM(c.cache_read_tokens) as total_cache_read,
117
+ SUM(c.reasoning_tokens) as total_reasoning,
118
+ SUM(c.estimated_cost_usd) as total_cost,
119
+ COUNT(DISTINCT c.id) as call_count,
120
+ COUNT(DISTINCT r.id) as run_count
121
+ FROM llm_calls c
122
+ JOIN agent_runs r ON c.run_id = r.id
123
+ ${where}
124
+ GROUP BY grp ORDER BY grp
125
+ `).all(...params);
126
+ return rows.map(summaryRowMapper);
127
+ }
128
+ const callRows = db.prepare(`
129
+ SELECT ${groupCol} as grp,
130
+ SUM(input_tokens) as total_input,
131
+ SUM(output_tokens) as total_output,
132
+ SUM(cache_creation_tokens) as total_cache_creation,
133
+ SUM(cache_read_tokens) as total_cache_read,
134
+ SUM(reasoning_tokens) as total_reasoning,
135
+ SUM(estimated_cost_usd) as total_cost,
136
+ COUNT(*) as call_count
137
+ FROM llm_calls c ${where}
138
+ GROUP BY grp ORDER BY grp
139
+ `).all(...params);
140
+ const runGroupCol = (() => {
141
+ switch (filters.groupBy) {
142
+ case "model": return "model";
143
+ case "agent": return "agent_name";
144
+ case "day": return "date(created_at)";
145
+ default: return "'all'";
146
+ }
147
+ })();
148
+ const runConditions = [];
149
+ const runParams = [];
150
+ if (filters.from) {
151
+ runConditions.push("created_at >= ?");
152
+ runParams.push(filters.from);
153
+ }
154
+ if (filters.to) {
155
+ runConditions.push("created_at <= ?");
156
+ runParams.push(filters.to);
157
+ }
158
+ const runWhere = runConditions.length > 0 ? `WHERE ${runConditions.join(" AND ")}` : "";
159
+ const runRows = db.prepare(`
160
+ SELECT ${runGroupCol} as grp, COUNT(*) as run_count
161
+ FROM agent_runs ${runWhere}
162
+ GROUP BY grp
163
+ `).all(...runParams);
164
+ const runCountMap = new Map(runRows.map((r) => [String(r.grp), r.run_count]));
165
+ return callRows.map((row) => ({
166
+ ...summaryRowMapper(row),
167
+ runCount: runCountMap.get(String(row.grp)) ?? 0,
168
+ }));
169
+ },
170
+ close() {
171
+ db.close();
172
+ },
173
+ };
174
+ }
175
+ function summaryRowMapper(row) {
176
+ return {
177
+ group: String(row.grp),
178
+ totalInputTokens: row.total_input ?? 0,
179
+ totalOutputTokens: row.total_output ?? 0,
180
+ totalCacheCreationTokens: row.total_cache_creation ?? 0,
181
+ totalCacheReadTokens: row.total_cache_read ?? 0,
182
+ totalReasoningTokens: row.total_reasoning ?? 0,
183
+ totalCostUsd: row.total_cost ?? 0,
184
+ callCount: row.call_count ?? 0,
185
+ runCount: row.run_count ?? 0,
186
+ };
187
+ }
188
+ function rowToCallRecord(row) {
189
+ return {
190
+ id: row.id,
191
+ runId: row.run_id,
192
+ agentName: row.agent_name,
193
+ provider: row.provider,
194
+ model: row.model,
195
+ usage: {
196
+ inputTokens: row.input_tokens,
197
+ outputTokens: row.output_tokens,
198
+ cacheCreationTokens: row.cache_creation_tokens,
199
+ cacheReadTokens: row.cache_read_tokens,
200
+ reasoningTokens: row.reasoning_tokens,
201
+ },
202
+ stopReason: row.stop_reason,
203
+ latencyMs: row.latency_ms,
204
+ estimatedCostUsd: row.estimated_cost_usd,
205
+ createdAt: row.created_at,
206
+ };
207
+ }
208
+ function rowToRunRecord(row) {
209
+ return {
210
+ id: row.id,
211
+ agentName: row.agent_name,
212
+ source: row.source,
213
+ model: row.model,
214
+ usage: {
215
+ inputTokens: row.total_input_tokens,
216
+ outputTokens: row.total_output_tokens,
217
+ cacheCreationTokens: row.total_cache_creation_tokens,
218
+ cacheReadTokens: row.total_cache_read_tokens,
219
+ reasoningTokens: row.total_reasoning_tokens,
220
+ },
221
+ iterations: row.iterations,
222
+ toolCallsCount: row.tool_calls_count,
223
+ estimatedCostUsd: row.estimated_cost_usd,
224
+ durationMs: row.duration_ms,
225
+ createdAt: row.created_at,
226
+ };
227
+ }