@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,195 @@
1
+ import { complete } from "../llm/index.js";
2
+ import { completeWithFallback } from "../llm/fallback.js";
3
+ import { extractText } from "../llm/types.js";
4
+ import { nanoid } from "nanoid";
5
+ import { toolToDefinition } from "../tools/types.js";
6
+ import { compactMessages, estimateTotalTokens } from "../session/compaction.js";
7
+ import { createLogger } from "../logger.js";
8
+ import { truncate } from "../text.js";
9
+ const log = createLogger("agent");
10
+ export async function runAgentLoop(initialMessages, config) {
11
+ const messages = [...initialMessages];
12
+ const registry = config.toolRegistry;
13
+ const staticTools = config.tools ?? [];
14
+ const maxIterations = config.maxIterations ?? 20;
15
+ const label = config.label ?? "main";
16
+ const loopStart = Date.now();
17
+ const currentTools = registry
18
+ ? registry.list().map((name) => registry.resolve([name])[0])
19
+ : staticTools;
20
+ const toolNames = currentTools.map((t) => t.name);
21
+ log.info({ label, model: config.model, tools: toolNames.length, toolNames, maxIterations, messages: initialMessages.length }, "loop start");
22
+ const runId = nanoid();
23
+ const agentName = config.agentName ?? config.label ?? "main";
24
+ const totalUsage = {
25
+ inputTokens: 0,
26
+ outputTokens: 0,
27
+ cacheCreationTokens: 0,
28
+ cacheReadTokens: 0,
29
+ reasoningTokens: 0,
30
+ };
31
+ let toolCallsCount = 0;
32
+ let didCompact = false;
33
+ for (let i = 0; i < maxIterations; i++) {
34
+ const iteration = i + 1;
35
+ // Rebuild tool definitions each iteration (skills may register new tools mid-loop)
36
+ const iterTools = registry
37
+ ? registry.list().map((name) => registry.resolve([name])[0])
38
+ : staticTools;
39
+ const toolMap = new Map(iterTools.map((t) => [t.name, t]));
40
+ const toolDefs = iterTools.map(toolToDefinition);
41
+ // Compact conversation if it exceeds the token threshold (one-shot: only once per loop)
42
+ if (!didCompact && config.compactionThreshold) {
43
+ const estimated = estimateTotalTokens(messages);
44
+ if (estimated > config.compactionThreshold) {
45
+ log.info({ label, iteration, estimated, threshold: config.compactionThreshold }, "compacting messages");
46
+ try {
47
+ const compacted = await compactMessages(messages, config.model, config.compactionThreshold);
48
+ messages.length = 0;
49
+ messages.push(...compacted);
50
+ didCompact = true;
51
+ }
52
+ catch (err) {
53
+ const isBug = err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof RangeError;
54
+ if (isBug) {
55
+ log.fatal({ err, label, iteration }, "compaction bug — programming error");
56
+ throw err;
57
+ }
58
+ log.warn({ err, label, iteration }, "compaction failed, proceeding with full context");
59
+ }
60
+ }
61
+ }
62
+ const response = await completeWithFallback(config.model, config.fallbacks ?? [], messages, {
63
+ systemPrompt: config.systemPrompt,
64
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
65
+ }, complete);
66
+ totalUsage.inputTokens += response.usage.inputTokens;
67
+ totalUsage.outputTokens += response.usage.outputTokens;
68
+ totalUsage.cacheCreationTokens += response.usage.cacheCreationTokens;
69
+ totalUsage.cacheReadTokens += response.usage.cacheReadTokens;
70
+ totalUsage.reasoningTokens += response.usage.reasoningTokens;
71
+ if (config.usageStore) {
72
+ try {
73
+ config.usageStore.recordCall({
74
+ runId,
75
+ agentName,
76
+ provider: response.model.split(":")[0],
77
+ model: response.model,
78
+ usage: response.usage,
79
+ stopReason: response.stopReason,
80
+ latencyMs: response.latencyMs,
81
+ });
82
+ }
83
+ catch (err) {
84
+ log.error({ err, runId }, "failed to record LLM call");
85
+ }
86
+ }
87
+ log.debug({ label, iteration, model: config.model, stopReason: response.stopReason, inputTokens: response.usage.inputTokens, outputTokens: response.usage.outputTokens }, "iteration complete");
88
+ messages.push({ role: "assistant", content: response.content });
89
+ if (response.stopReason !== "tool_use") {
90
+ let text = extractText(response.content);
91
+ // If the final turn has no text (e.g. LLM said everything alongside a tool call
92
+ // in a prior turn), scan backward for the last assistant text.
93
+ if (!text) {
94
+ for (let j = messages.length - 2; j >= 0; j--) {
95
+ const msg = messages[j];
96
+ if (msg.role === "assistant") {
97
+ const found = extractText(msg.content);
98
+ if (found) {
99
+ text = found;
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ const elapsed = Date.now() - loopStart;
106
+ if (config.usageStore) {
107
+ try {
108
+ config.usageStore.recordRun({
109
+ id: runId,
110
+ agentName,
111
+ source: config.source ?? "unknown",
112
+ model: config.model,
113
+ usage: totalUsage,
114
+ iterations: iteration,
115
+ toolCallsCount,
116
+ durationMs: elapsed,
117
+ });
118
+ }
119
+ catch (err) {
120
+ log.error({ err, runId }, "failed to record agent run");
121
+ }
122
+ }
123
+ log.info({ label, stopReason: response.stopReason, iterations: iteration, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "loop end");
124
+ return {
125
+ text,
126
+ messages,
127
+ usage: totalUsage,
128
+ };
129
+ }
130
+ const toolCalls = response.content.filter((b) => b.type === "tool_call");
131
+ toolCallsCount += toolCalls.length;
132
+ for (const call of toolCalls) {
133
+ log.debug({ label, iteration, tool: call.name, args: truncate(JSON.stringify(call.arguments), 200) }, "tool call");
134
+ const tool = toolMap.get(call.name);
135
+ let result;
136
+ if (!tool) {
137
+ log.warn({ label, iteration, tool: call.name }, "LLM called unknown tool");
138
+ result = `Error: unknown tool "${call.name}"`;
139
+ }
140
+ else {
141
+ const toolStart = Date.now();
142
+ try {
143
+ result = await tool.execute(call.arguments);
144
+ const toolElapsed = Date.now() - toolStart;
145
+ log.debug({ label, iteration, tool: call.name, resultLength: result.length, snippet: truncate(result, 150), elapsed: toolElapsed }, "tool result");
146
+ }
147
+ catch (err) {
148
+ const isBug = err instanceof TypeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof RangeError;
149
+ if (isBug) {
150
+ log.fatal({ err, tool: call.name, label, iteration }, "tool bug — programming error");
151
+ }
152
+ else {
153
+ log.error({ err, tool: call.name, label, iteration }, "tool execution failed");
154
+ }
155
+ result = `Error: ${err instanceof Error ? err.message : String(err)}`;
156
+ }
157
+ }
158
+ const maxResultSize = config.maxToolResultSize ?? 8192;
159
+ if (result.length > maxResultSize) {
160
+ const originalLength = result.length;
161
+ result = result.slice(0, maxResultSize) + `\n\n[Output truncated from ${originalLength} to ${maxResultSize} characters]`;
162
+ log.debug({ label, iteration, tool: call.name, originalLength, maxResultSize }, "tool result truncated");
163
+ }
164
+ messages.push({
165
+ role: "tool_result",
166
+ tool_call_id: call.id,
167
+ content: result,
168
+ });
169
+ }
170
+ }
171
+ const elapsed = Date.now() - loopStart;
172
+ if (config.usageStore) {
173
+ try {
174
+ config.usageStore.recordRun({
175
+ id: runId,
176
+ agentName,
177
+ source: config.source ?? "unknown",
178
+ model: config.model,
179
+ usage: totalUsage,
180
+ iterations: maxIterations,
181
+ toolCallsCount,
182
+ durationMs: elapsed,
183
+ });
184
+ }
185
+ catch (err) {
186
+ log.error({ err, runId }, "failed to record agent run");
187
+ }
188
+ }
189
+ log.warn({ label, maxIterations, inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens, elapsed }, "max iterations reached");
190
+ return {
191
+ text: "[Agent reached maximum iterations]",
192
+ messages,
193
+ usage: totalUsage,
194
+ };
195
+ }
@@ -0,0 +1,6 @@
1
+ import { SessionManager } from "../session/manager.js";
2
+ export interface AgentSession {
3
+ soul: string | undefined;
4
+ session: SessionManager;
5
+ }
6
+ export declare function setupAgentSession(dataDir: string, agentName: string, sessionId: string): AgentSession;
@@ -0,0 +1,11 @@
1
+ import * as path from "path";
2
+ import { loadSoul } from "./soul.js";
3
+ import { SessionManager } from "../session/manager.js";
4
+ export function setupAgentSession(dataDir, agentName, sessionId) {
5
+ const agentDir = path.join(dataDir, "agents", agentName);
6
+ const sessionDir = path.join(agentDir, "sessions");
7
+ return {
8
+ soul: loadSoul(agentDir),
9
+ session: new SessionManager(sessionDir, sessionId),
10
+ };
11
+ }
@@ -0,0 +1 @@
1
+ export declare function loadSoul(agentDir: string): string | undefined;
@@ -0,0 +1,8 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ export function loadSoul(agentDir) {
4
+ const soulPath = path.join(agentDir, "SOUL.md");
5
+ if (!fs.existsSync(soulPath))
6
+ return undefined;
7
+ return fs.readFileSync(soulPath, "utf-8");
8
+ }
@@ -0,0 +1,23 @@
1
+ import type { Tool } from "../tools/types.js";
2
+ import type { ToolRegistry } from "../tools/registry.js";
3
+ import type { TokenUsage } from "../llm/types.js";
4
+ import type { UsageStore } from "../usage/store.js";
5
+ export interface AgentConfig {
6
+ model: string;
7
+ fallbacks?: string[];
8
+ systemPrompt?: string;
9
+ tools?: Tool[];
10
+ toolRegistry?: ToolRegistry;
11
+ maxIterations?: number;
12
+ label?: string;
13
+ agentName?: string;
14
+ usageStore?: UsageStore;
15
+ source?: string;
16
+ maxToolResultSize?: number;
17
+ compactionThreshold?: number;
18
+ }
19
+ export interface AgentResult {
20
+ text: string;
21
+ messages: import("../llm/types.js").Message[];
22
+ usage: TokenUsage;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { type ApiContext, type RouteHandler } from "./types.js";
2
+ export declare function agentRoutes(ctx: ApiContext): Record<string, RouteHandler>;
@@ -0,0 +1,43 @@
1
+ import { sendJson, sendError } from "./types.js";
2
+ export function agentRoutes(ctx) {
3
+ return {
4
+ "GET /api/agents": (_url, _req, res) => {
5
+ const agents = Object.entries(ctx.config.agents).map(([name, def]) => ({
6
+ name,
7
+ displayName: def.displayName ?? null,
8
+ emoji: def.emoji ?? null,
9
+ avatar: def.avatar ?? null,
10
+ model: def.model ?? ctx.config.defaults.model,
11
+ tools: def.tools,
12
+ skills: def.skills,
13
+ spawnOnly: def.spawn_only,
14
+ canSpawn: def.can_spawn,
15
+ maxIterations: def.maxIterations ?? ctx.config.defaults.maxIterations,
16
+ slack: def.slack ?? null,
17
+ }));
18
+ sendJson(res, agents);
19
+ },
20
+ "GET /api/agents/:name": (url, _req, res) => {
21
+ const name = url.pathname.split("/")[3];
22
+ const def = ctx.config.agents[name];
23
+ if (!def) {
24
+ sendError(res, 404, "agent not found");
25
+ return;
26
+ }
27
+ sendJson(res, {
28
+ name,
29
+ displayName: def.displayName ?? null,
30
+ emoji: def.emoji ?? null,
31
+ avatar: def.avatar ?? null,
32
+ model: def.model ?? ctx.config.defaults.model,
33
+ tools: def.tools,
34
+ skills: def.skills,
35
+ spawnOnly: def.spawn_only,
36
+ canSpawn: def.can_spawn,
37
+ maxIterations: def.maxIterations ?? ctx.config.defaults.maxIterations,
38
+ slack: def.slack ?? null,
39
+ sandbox: def.sandbox ?? ctx.config.defaults.sandbox ?? null,
40
+ });
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,2 @@
1
+ import { type ApiContext, type RouteHandler } from "./types.js";
2
+ export declare function configRoutes(ctx: ApiContext): Record<string, RouteHandler>;
@@ -0,0 +1,20 @@
1
+ import * as fs from "fs";
2
+ import { sendJson } from "./types.js";
3
+ export function configRoutes(ctx) {
4
+ return {
5
+ "GET /api/config": (_url, _req, res) => {
6
+ const raw = fs.readFileSync(ctx.configPath, "utf-8");
7
+ const config = JSON.parse(raw);
8
+ sendJson(res, config);
9
+ },
10
+ "GET /api/config/models": (_url, _req, res) => {
11
+ const models = (ctx.config.models ?? []).map((m) => ({
12
+ alias: m.alias,
13
+ provider: m.model.split(":")[0],
14
+ model: m.model,
15
+ isDefault: m.alias === ctx.config.defaults.model,
16
+ }));
17
+ sendJson(res, models);
18
+ },
19
+ };
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type ApiContext, type RouteHandler } from "./types.js";
2
+ export declare function cronRoutes(ctx: ApiContext): Record<string, RouteHandler>;
@@ -0,0 +1,15 @@
1
+ import { sendJson } from "./types.js";
2
+ export function cronRoutes(ctx) {
3
+ return {
4
+ "GET /api/cron": (_url, _req, res) => {
5
+ const jobs = ctx.config.cron.map((job) => ({
6
+ id: job.id,
7
+ agent: job.agent,
8
+ schedule: job.schedule,
9
+ prompt: job.prompt,
10
+ enabled: job.enabled,
11
+ }));
12
+ sendJson(res, jobs);
13
+ },
14
+ };
15
+ }
@@ -0,0 +1,2 @@
1
+ import { type ApiContext, type RouteHandler } from "./types.js";
2
+ export declare function healthRoutes(_ctx: ApiContext): Record<string, RouteHandler>;
@@ -0,0 +1,8 @@
1
+ import { sendJson } from "./types.js";
2
+ export function healthRoutes(_ctx) {
3
+ return {
4
+ "GET /api/health": (_url, _req, res) => {
5
+ sendJson(res, { status: "ok", timestamp: new Date().toISOString() });
6
+ },
7
+ };
8
+ }
@@ -0,0 +1,5 @@
1
+ import type { RouteHandler } from "./types.js";
2
+ export declare function addLogListener(res: import("http").ServerResponse): void;
3
+ export declare function removeLogListener(res: import("http").ServerResponse): void;
4
+ export declare function broadcastLog(data: string): void;
5
+ export declare function logRoutes(): Record<string, RouteHandler>;
@@ -0,0 +1,28 @@
1
+ const clients = new Set();
2
+ export function addLogListener(res) {
3
+ clients.add(res);
4
+ }
5
+ export function removeLogListener(res) {
6
+ clients.delete(res);
7
+ }
8
+ export function broadcastLog(data) {
9
+ for (const client of clients) {
10
+ client.write(`data: ${data}\n\n`);
11
+ }
12
+ }
13
+ export function logRoutes() {
14
+ return {
15
+ "GET /api/logs/stream": (_url, req, res) => {
16
+ res.writeHead(200, {
17
+ "Content-Type": "text/event-stream",
18
+ "Cache-Control": "no-cache",
19
+ Connection: "keep-alive",
20
+ });
21
+ res.write(`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}\n\n`);
22
+ addLogListener(res);
23
+ req.on("close", () => {
24
+ removeLogListener(res);
25
+ });
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,6 @@
1
+ import { type ApiContext } from "./types.js";
2
+ export interface ApiServer {
3
+ start(): Promise<number>;
4
+ stop(): Promise<void>;
5
+ }
6
+ export declare function createApiServer(ctx: ApiContext, port: number): ApiServer;
@@ -0,0 +1,80 @@
1
+ import * as http from "http";
2
+ import { sendError } from "./types.js";
3
+ import { healthRoutes } from "./health.js";
4
+ import { sessionRoutes } from "./sessions.js";
5
+ import { agentRoutes } from "./agents.js";
6
+ import { configRoutes } from "./config.js";
7
+ import { cronRoutes } from "./cron.js";
8
+ import { logRoutes } from "./logs.js";
9
+ import { usageRoutes } from "./usage.js";
10
+ import { createLogger } from "../logger.js";
11
+ const log = createLogger("api");
12
+ function parseRouteKey(key) {
13
+ const [method, path] = key.split(" ");
14
+ // Convert :param segments to regex groups
15
+ const regexStr = "^" + path.replace(/:([^/]+)/g, "([^/]+)") + "$";
16
+ return { method, pattern: new RegExp(regexStr) };
17
+ }
18
+ export function createApiServer(ctx, port) {
19
+ // Collect routes from all modules
20
+ const routeHandlers = {
21
+ ...healthRoutes(ctx),
22
+ ...sessionRoutes(ctx),
23
+ ...agentRoutes(ctx),
24
+ ...configRoutes(ctx),
25
+ ...cronRoutes(ctx),
26
+ ...logRoutes(),
27
+ };
28
+ // If usage store exists, add usage routes
29
+ if (ctx.usageStore) {
30
+ Object.assign(routeHandlers, usageRoutes(ctx.usageStore));
31
+ }
32
+ const routes = Object.entries(routeHandlers).map(([key, handler]) => {
33
+ const { method, pattern } = parseRouteKey(key);
34
+ return { method, pattern, handler };
35
+ });
36
+ const server = http.createServer((req, res) => {
37
+ res.setHeader("Access-Control-Allow-Origin", "*");
38
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
39
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
40
+ if (req.method === "OPTIONS") {
41
+ res.writeHead(204);
42
+ res.end();
43
+ return;
44
+ }
45
+ res.setHeader("Content-Type", "application/json");
46
+ const url = new URL(req.url ?? "/", `http://localhost`);
47
+ const method = req.method ?? "GET";
48
+ const matched = routes.find((r) => r.method === method && r.pattern.test(url.pathname));
49
+ if (matched) {
50
+ try {
51
+ matched.handler(url, req, res);
52
+ }
53
+ catch (err) {
54
+ log.error({ err, path: url.pathname }, "API error");
55
+ sendError(res, 500, "internal server error");
56
+ }
57
+ }
58
+ else {
59
+ sendError(res, 404, "not found");
60
+ }
61
+ });
62
+ return {
63
+ start() {
64
+ return new Promise((resolve, reject) => {
65
+ server.once("error", reject);
66
+ server.listen(port, () => {
67
+ const addr = server.address();
68
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
69
+ log.info({ port: actualPort }, "API server started");
70
+ resolve(actualPort);
71
+ });
72
+ });
73
+ },
74
+ stop() {
75
+ return new Promise((resolve, reject) => {
76
+ server.close((err) => (err ? reject(err) : resolve()));
77
+ });
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,2 @@
1
+ import { type ApiContext, type RouteHandler } from "./types.js";
2
+ export declare function sessionRoutes(ctx: ApiContext): Record<string, RouteHandler>;
@@ -0,0 +1,67 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { sendJson, sendError } from "./types.js";
4
+ export function sessionRoutes(ctx) {
5
+ return {
6
+ "GET /api/sessions": (url, _req, res) => {
7
+ const agentsDir = path.join(ctx.dataDir, "agents");
8
+ if (!fs.existsSync(agentsDir)) {
9
+ sendJson(res, { total: 0, sessions: [] });
10
+ return;
11
+ }
12
+ const agentFilter = url.searchParams.get("agent") ?? undefined;
13
+ const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
14
+ const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
15
+ const sessions = [];
16
+ const agentDirs = fs.readdirSync(agentsDir);
17
+ for (const agent of agentDirs) {
18
+ if (agentFilter && agent !== agentFilter)
19
+ continue;
20
+ const agentDir = path.join(agentsDir, agent);
21
+ if (!fs.statSync(agentDir).isDirectory())
22
+ continue;
23
+ const files = fs.readdirSync(agentDir).filter((f) => f.endsWith(".jsonl"));
24
+ for (const file of files) {
25
+ const filePath = path.join(agentDir, file);
26
+ const stat = fs.statSync(filePath);
27
+ const content = fs.readFileSync(filePath, "utf-8");
28
+ const lines = content.split("\n").filter((l) => l.trim());
29
+ sessions.push({
30
+ agent,
31
+ sessionId: file.replace(".jsonl", ""),
32
+ messageCount: lines.length,
33
+ sizeBytes: stat.size,
34
+ lastModified: stat.mtime.toISOString(),
35
+ });
36
+ }
37
+ }
38
+ sessions.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
39
+ const page = sessions.slice(offset, offset + limit);
40
+ sendJson(res, { total: sessions.length, sessions: page });
41
+ },
42
+ "GET /api/sessions/:agent/:id": (url, _req, res) => {
43
+ const parts = url.pathname.split("/");
44
+ const agent = parts[3];
45
+ const id = parts[4];
46
+ const filePath = path.join(ctx.dataDir, "agents", agent, `${id}.jsonl`);
47
+ if (!fs.existsSync(filePath)) {
48
+ sendError(res, 404, "session not found");
49
+ return;
50
+ }
51
+ const content = fs.readFileSync(filePath, "utf-8");
52
+ const messages = content
53
+ .split("\n")
54
+ .filter((l) => l.trim())
55
+ .map((line) => {
56
+ try {
57
+ return JSON.parse(line);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ })
63
+ .filter(Boolean);
64
+ sendJson(res, { agent, sessionId: id, messages });
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,12 @@
1
+ import type { Config } from "../config/schema.js";
2
+ import type { UsageStore } from "../usage/store.js";
3
+ export interface ApiContext {
4
+ config: Config;
5
+ configPath: string;
6
+ usageStore?: UsageStore;
7
+ dataDir: string;
8
+ memoryDbPath?: string;
9
+ }
10
+ export type RouteHandler = (url: URL, req: import("http").IncomingMessage, res: import("http").ServerResponse) => void;
11
+ export declare function sendJson(res: import("http").ServerResponse, data: unknown, status?: number): void;
12
+ export declare function sendError(res: import("http").ServerResponse, status: number, message: string): void;
@@ -0,0 +1,13 @@
1
+ export function sendJson(res, data, status = 200) {
2
+ if (res.headersSent)
3
+ return;
4
+ const body = JSON.stringify(data);
5
+ res.writeHead(status);
6
+ res.end(body);
7
+ }
8
+ export function sendError(res, status, message) {
9
+ if (res.headersSent)
10
+ return;
11
+ res.writeHead(status);
12
+ res.end(JSON.stringify({ error: message }));
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { UsageStore } from "../usage/store.js";
2
+ import { type RouteHandler } from "./types.js";
3
+ export declare function usageRoutes(store: UsageStore): Record<string, RouteHandler>;
@@ -0,0 +1,50 @@
1
+ import { sendJson, sendError } from "./types.js";
2
+ import { GROUP_BY_VALUES } from "../usage/types.js";
3
+ const VALID_GROUP_BY = new Set(GROUP_BY_VALUES);
4
+ function parseIntParam(val) {
5
+ if (val === null)
6
+ return undefined;
7
+ const n = parseInt(val, 10);
8
+ return isNaN(n) || n < 0 ? undefined : n;
9
+ }
10
+ export function usageRoutes(store) {
11
+ return {
12
+ "GET /api/usage/calls": (url, _req, res) => {
13
+ const data = store.getCalls({
14
+ model: url.searchParams.get("model") ?? undefined,
15
+ agentName: url.searchParams.get("agent") ?? undefined,
16
+ runId: url.searchParams.get("run_id") ?? undefined,
17
+ from: url.searchParams.get("from") ?? undefined,
18
+ to: url.searchParams.get("to") ?? undefined,
19
+ limit: parseIntParam(url.searchParams.get("limit")),
20
+ offset: parseIntParam(url.searchParams.get("offset")),
21
+ });
22
+ sendJson(res, data);
23
+ },
24
+ "GET /api/usage/runs": (url, _req, res) => {
25
+ const data = store.getRuns({
26
+ agentName: url.searchParams.get("agent") ?? undefined,
27
+ source: url.searchParams.get("source") ?? undefined,
28
+ model: url.searchParams.get("model") ?? undefined,
29
+ from: url.searchParams.get("from") ?? undefined,
30
+ to: url.searchParams.get("to") ?? undefined,
31
+ limit: parseIntParam(url.searchParams.get("limit")),
32
+ offset: parseIntParam(url.searchParams.get("offset")),
33
+ });
34
+ sendJson(res, data);
35
+ },
36
+ "GET /api/usage/summary": (url, _req, res) => {
37
+ const groupByParam = url.searchParams.get("group_by");
38
+ if (groupByParam && !VALID_GROUP_BY.has(groupByParam)) {
39
+ sendError(res, 400, "Invalid group_by value. Must be one of: model, agent, source, day");
40
+ return;
41
+ }
42
+ const data = store.getSummary({
43
+ from: url.searchParams.get("from") ?? undefined,
44
+ to: url.searchParams.get("to") ?? undefined,
45
+ groupBy: groupByParam ?? undefined,
46
+ });
47
+ sendJson(res, data);
48
+ },
49
+ };
50
+ }