@alejandroroman/agent-kit 0.1.3 → 0.2.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 (102) hide show
  1. package/dist/_memory/dist/config.d.ts +14 -0
  2. package/dist/_memory/dist/config.js +16 -0
  3. package/dist/_memory/dist/db/client.d.ts +2 -0
  4. package/dist/_memory/dist/db/client.js +15 -0
  5. package/dist/_memory/dist/db/schema.d.ts +14 -0
  6. package/dist/_memory/dist/db/schema.js +51 -0
  7. package/dist/_memory/dist/embeddings/ollama.d.ts +12 -0
  8. package/dist/_memory/dist/embeddings/ollama.js +22 -0
  9. package/dist/_memory/dist/embeddings/provider.d.ts +4 -0
  10. package/dist/_memory/dist/embeddings/provider.js +1 -0
  11. package/dist/_memory/dist/index.d.ts +10 -0
  12. package/dist/_memory/dist/index.js +6 -0
  13. package/dist/_memory/dist/search.d.ts +30 -0
  14. package/dist/_memory/dist/search.js +121 -0
  15. package/dist/_memory/dist/server.d.ts +8 -0
  16. package/dist/_memory/dist/server.js +126 -0
  17. package/dist/_memory/dist/store.d.ts +51 -0
  18. package/dist/_memory/dist/store.js +115 -0
  19. package/dist/_memory/server.js +0 -0
  20. package/dist/agent/loop.js +210 -111
  21. package/dist/api/errors.d.ts +3 -0
  22. package/dist/api/errors.js +37 -0
  23. package/dist/api/events.d.ts +5 -0
  24. package/dist/api/events.js +28 -0
  25. package/dist/api/router.js +10 -0
  26. package/dist/api/traces.d.ts +3 -0
  27. package/dist/api/traces.js +35 -0
  28. package/dist/api/types.d.ts +2 -0
  29. package/dist/bootstrap.d.ts +6 -5
  30. package/dist/bootstrap.js +26 -7
  31. package/dist/cli/chat.js +18 -63
  32. package/dist/cli/claude-md-template.d.ts +5 -0
  33. package/dist/cli/claude-md-template.js +220 -0
  34. package/dist/cli/config-writer.js +3 -0
  35. package/dist/cli/create.js +1 -4
  36. package/dist/cli/env.d.ts +14 -0
  37. package/dist/cli/env.js +68 -0
  38. package/dist/cli/init.js +14 -7
  39. package/dist/cli/list.js +1 -2
  40. package/dist/cli/paths.d.ts +3 -0
  41. package/dist/cli/paths.js +4 -0
  42. package/dist/cli/repl.d.ts +23 -0
  43. package/dist/cli/repl.js +73 -0
  44. package/dist/cli/slack-setup.d.ts +6 -0
  45. package/dist/cli/slack-setup.js +234 -0
  46. package/dist/cli/start.js +96 -96
  47. package/dist/cli/ui.d.ts +2 -2
  48. package/dist/cli/ui.js +5 -5
  49. package/dist/cli/validate.js +1 -4
  50. package/dist/cli/whats-new.d.ts +1 -0
  51. package/dist/cli/whats-new.js +69 -0
  52. package/dist/cli.js +14 -0
  53. package/dist/config/resolve.d.ts +1 -0
  54. package/dist/config/resolve.js +1 -0
  55. package/dist/config/schema.d.ts +2 -0
  56. package/dist/config/schema.js +1 -0
  57. package/dist/config/writer.d.ts +18 -0
  58. package/dist/config/writer.js +85 -0
  59. package/dist/cron/scheduler.d.ts +4 -1
  60. package/dist/cron/scheduler.js +99 -52
  61. package/dist/gateways/slack/client.d.ts +1 -0
  62. package/dist/gateways/slack/client.js +9 -0
  63. package/dist/gateways/slack/handler.js +2 -1
  64. package/dist/gateways/slack/index.js +75 -29
  65. package/dist/gateways/slack/listener.d.ts +8 -1
  66. package/dist/gateways/slack/listener.js +36 -10
  67. package/dist/heartbeat/runner.js +99 -82
  68. package/dist/index.js +4 -209
  69. package/dist/llm/anthropic.d.ts +1 -0
  70. package/dist/llm/anthropic.js +11 -2
  71. package/dist/llm/fallback.js +34 -2
  72. package/dist/llm/openai.d.ts +2 -0
  73. package/dist/llm/openai.js +33 -2
  74. package/dist/llm/types.d.ts +16 -2
  75. package/dist/llm/types.js +9 -0
  76. package/dist/logger.js +8 -0
  77. package/dist/media/sanitize.d.ts +5 -0
  78. package/dist/media/sanitize.js +53 -0
  79. package/dist/multi/spawn.js +29 -10
  80. package/dist/session/compaction.js +3 -1
  81. package/dist/session/prune-images.d.ts +9 -0
  82. package/dist/session/prune-images.js +42 -0
  83. package/dist/skills/activate.d.ts +6 -0
  84. package/dist/skills/activate.js +72 -27
  85. package/dist/skills/index.d.ts +1 -1
  86. package/dist/skills/index.js +1 -1
  87. package/dist/telemetry/db.d.ts +63 -0
  88. package/dist/telemetry/db.js +193 -0
  89. package/dist/telemetry/index.d.ts +17 -0
  90. package/dist/telemetry/index.js +82 -0
  91. package/dist/telemetry/sanitize.d.ts +6 -0
  92. package/dist/telemetry/sanitize.js +48 -0
  93. package/dist/telemetry/sqlite-processor.d.ts +11 -0
  94. package/dist/telemetry/sqlite-processor.js +108 -0
  95. package/dist/telemetry/types.d.ts +30 -0
  96. package/dist/telemetry/types.js +31 -0
  97. package/dist/tools/builtin/index.d.ts +2 -0
  98. package/dist/tools/builtin/index.js +2 -0
  99. package/dist/tools/builtin/self-config.d.ts +4 -0
  100. package/dist/tools/builtin/self-config.js +182 -0
  101. package/dist/tools/registry.js +8 -1
  102. package/package.json +26 -20
@@ -0,0 +1,82 @@
1
+ import * as path from "path";
2
+ import * as os from "os";
3
+ import { trace } from "@opentelemetry/api";
4
+ import { NodeSDK } from "@opentelemetry/sdk-node";
5
+ import { SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
6
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7
+ import * as Sentry from "@sentry/node";
8
+ import { createLogger } from "../logger.js";
9
+ import { TelemetryDb } from "./db.js";
10
+ import { SQLiteSpanProcessor } from "./sqlite-processor.js";
11
+ const log = createLogger("telemetry");
12
+ const DEFAULT_DB_PATH = path.join(os.homedir(), ".agent-kit", "telemetry.db");
13
+ let sdk;
14
+ let telemetryDb;
15
+ /** Get the TelemetryDb instance (available after initTelemetry with a dbPath) */
16
+ export function getTelemetryDb() {
17
+ return telemetryDb;
18
+ }
19
+ export function initTelemetry(config = {}) {
20
+ const { enabled = process.env.OTEL_ENABLED !== "false", serviceName = process.env.OTEL_SERVICE_NAME ?? "agent-kit", otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT, sentryDsn, dbPath = DEFAULT_DB_PATH, sampleRate, } = config;
21
+ if (!enabled) {
22
+ log.info("telemetry disabled");
23
+ return async () => { };
24
+ }
25
+ // Guard against double initialization
26
+ if (sdk) {
27
+ log.warn("telemetry already initialized, shutting down previous instance");
28
+ sdk.shutdown().catch(() => { });
29
+ sdk = undefined;
30
+ telemetryDb = undefined;
31
+ }
32
+ const spanProcessors = [];
33
+ // SQLite span processor (local persistence)
34
+ if (dbPath) {
35
+ try {
36
+ telemetryDb = new TelemetryDb(dbPath);
37
+ spanProcessors.push(new SQLiteSpanProcessor(telemetryDb));
38
+ log.info({ dbPath }, "SQLite span processor enabled");
39
+ }
40
+ catch (err) {
41
+ log.error({ err, dbPath }, "failed to initialize SQLite span processor");
42
+ }
43
+ }
44
+ // OTLP exporter (optional — for Grafana/Jaeger)
45
+ if (otlpEndpoint) {
46
+ spanProcessors.push(new SimpleSpanProcessor(new OTLPTraceExporter({ url: otlpEndpoint })));
47
+ log.info({ endpoint: otlpEndpoint }, "OTLP exporter enabled");
48
+ }
49
+ // Console exporter for dev (when no OTLP endpoint)
50
+ if (!otlpEndpoint && process.env.OTEL_DEBUG === "true") {
51
+ spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
52
+ }
53
+ // Sentry error tracking (optional)
54
+ if (sentryDsn) {
55
+ Sentry.init({
56
+ dsn: sentryDsn,
57
+ tracesSampleRate: sampleRate ?? 1.0,
58
+ release: process.env.npm_package_version,
59
+ environment: process.env.NODE_ENV ?? "development",
60
+ });
61
+ log.info("Sentry initialized");
62
+ }
63
+ sdk = new NodeSDK({
64
+ serviceName,
65
+ spanProcessors,
66
+ });
67
+ sdk.start();
68
+ log.info({ serviceName }, "telemetry initialized");
69
+ return async () => {
70
+ if (sdk) {
71
+ await sdk.shutdown();
72
+ log.info("telemetry shut down");
73
+ }
74
+ await Sentry.close(2000);
75
+ };
76
+ }
77
+ export function getTracer(name) {
78
+ return trace.getTracer(name);
79
+ }
80
+ export { ATTR } from "./types.js";
81
+ export { TelemetryDb } from "./db.js";
82
+ export { SQLiteSpanProcessor } from "./sqlite-processor.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sanitize span attributes before persisting:
3
+ * 1. Redact values matching known secret patterns
4
+ * 2. Truncate long string values (unless key is in the safe list)
5
+ */
6
+ export declare function sanitizeAttributes(attrs: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,48 @@
1
+ import { ATTR } from "./types.js";
2
+ /**
3
+ * Patterns that match known secret/credential formats.
4
+ * If any pattern matches within a string value, the entire value is redacted.
5
+ */
6
+ const SECRET_PATTERNS = [
7
+ /sk-[a-zA-Z0-9\-]{20,}/, // OpenAI/Anthropic API keys
8
+ /xoxb-[a-zA-Z0-9\-]+/, // Slack bot tokens
9
+ /xoxp-[a-zA-Z0-9\-]+/, // Slack user tokens
10
+ /xapp-[a-zA-Z0-9\-]+/, // Slack app tokens
11
+ /Bearer\s+[a-zA-Z0-9\-_.]+/, // Bearer tokens
12
+ /https?:\/\/[^@\s]+@[^\s]*sentry\.io[^\s]*/, // Sentry DSNs
13
+ /ghp_[a-zA-Z0-9]{36,}/, // GitHub personal access tokens
14
+ /gho_[a-zA-Z0-9]{36,}/, // GitHub OAuth tokens
15
+ ];
16
+ const MAX_STRING_LENGTH = 500;
17
+ /**
18
+ * ATTR keys that are controlled by our code and should never be truncated.
19
+ * They are still checked for secret patterns (in case a user accidentally
20
+ * passes a credential as e.g. an agent name).
21
+ */
22
+ const SAFE_KEYS = new Set(Object.values(ATTR));
23
+ /**
24
+ * Sanitize span attributes before persisting:
25
+ * 1. Redact values matching known secret patterns
26
+ * 2. Truncate long string values (unless key is in the safe list)
27
+ */
28
+ export function sanitizeAttributes(attrs) {
29
+ const result = {};
30
+ for (const [key, value] of Object.entries(attrs)) {
31
+ result[key] = sanitizeValue(key, value);
32
+ }
33
+ return result;
34
+ }
35
+ function sanitizeValue(key, value) {
36
+ if (typeof value !== "string")
37
+ return value;
38
+ // Check for secret patterns — always, even for safe keys
39
+ for (const pattern of SECRET_PATTERNS) {
40
+ if (pattern.test(value))
41
+ return "[REDACTED]";
42
+ }
43
+ // Truncate long strings, but skip safe (ATTR) keys
44
+ if (!SAFE_KEYS.has(key) && value.length > MAX_STRING_LENGTH) {
45
+ return value.slice(0, MAX_STRING_LENGTH) + "...[truncated]";
46
+ }
47
+ return value;
48
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context } from "@opentelemetry/api";
2
+ import type { SpanProcessor, Span, ReadableSpan } from "@opentelemetry/sdk-trace-base";
3
+ import type { TelemetryDb } from "./db.js";
4
+ export declare class SQLiteSpanProcessor implements SpanProcessor {
5
+ private db;
6
+ constructor(db: TelemetryDb);
7
+ onStart(_span: Span, _parentContext: Context): void;
8
+ onEnd(span: ReadableSpan): void;
9
+ shutdown(): Promise<void>;
10
+ forceFlush(): Promise<void>;
11
+ }
@@ -0,0 +1,108 @@
1
+ import { SpanStatusCode } from "@opentelemetry/api";
2
+ import { sanitizeAttributes } from "./sanitize.js";
3
+ import { ATTR } from "./types.js";
4
+ const SPAN_KIND_MAP = {
5
+ 0: "internal",
6
+ 1: "server",
7
+ 2: "client",
8
+ 3: "producer",
9
+ 4: "consumer",
10
+ };
11
+ const STATUS_MAP = {
12
+ [SpanStatusCode.UNSET]: "unset",
13
+ [SpanStatusCode.OK]: "ok",
14
+ [SpanStatusCode.ERROR]: "error",
15
+ };
16
+ /** Convert OTel HrTime [seconds, nanoseconds] to milliseconds */
17
+ function hrToMs(hr) {
18
+ return hr[0] * 1000 + hr[1] / 1e6;
19
+ }
20
+ /**
21
+ * Generate an error fingerprint from exception info.
22
+ * Prefers: error_type + first file + line from stack trace.
23
+ * Fallback: error_type + message.
24
+ */
25
+ function generateFingerprint(errorType, message, stack) {
26
+ const type = errorType ?? "Error";
27
+ if (stack) {
28
+ // Try to extract file:line from first stack frame
29
+ // Match patterns like: at Object.<anonymous> (src/agent/loop.ts:42:10)
30
+ // or: at src/agent/loop.ts:42:10
31
+ const frameMatch = stack.match(/(?:at\s+(?:.*?\s+)?(?:\()?)((?:[a-zA-Z]:)?[^:(\s]+):(\d+)/);
32
+ if (frameMatch) {
33
+ const file = frameMatch[1];
34
+ const line = frameMatch[2];
35
+ return `${type}:${file}:${line}`;
36
+ }
37
+ }
38
+ return `${type}:${message}`;
39
+ }
40
+ export class SQLiteSpanProcessor {
41
+ db;
42
+ constructor(db) {
43
+ this.db = db;
44
+ }
45
+ onStart(_span, _parentContext) {
46
+ // no-op
47
+ }
48
+ onEnd(span) {
49
+ const ctx = span.spanContext();
50
+ const traceId = ctx.traceId;
51
+ const spanId = ctx.spanId;
52
+ const parentSpanId = span.parentSpanContext?.spanId ?? null;
53
+ const agent = span.attributes[ATTR.AGENT];
54
+ const source = span.attributes[ATTR.SOURCE];
55
+ const startTime = hrToMs(span.startTime);
56
+ const endTime = hrToMs(span.endTime);
57
+ const durationMs = hrToMs(span.duration);
58
+ const kind = SPAN_KIND_MAP[span.kind] ?? "internal";
59
+ const status = STATUS_MAP[span.status.code] ?? "unset";
60
+ // Serialize all span events for storage
61
+ const serializedEvents = span.events.map((e) => ({
62
+ name: e.name,
63
+ time: hrToMs(e.time),
64
+ attributes: e.attributes ?? {},
65
+ }));
66
+ // Insert span
67
+ this.db.insertSpan({
68
+ trace_id: traceId,
69
+ span_id: spanId,
70
+ parent_span_id: parentSpanId,
71
+ name: span.name,
72
+ kind,
73
+ status,
74
+ agent: agent ?? null,
75
+ source: source ?? null,
76
+ start_time: startTime,
77
+ end_time: endTime,
78
+ duration_ms: durationMs,
79
+ attributes: JSON.stringify(sanitizeAttributes(span.attributes)),
80
+ events: JSON.stringify(serializedEvents),
81
+ });
82
+ // Extract exception events and write to errors table
83
+ for (const event of span.events) {
84
+ if (event.name === "exception") {
85
+ const errorType = event.attributes?.["exception.type"];
86
+ const message = event.attributes?.["exception.message"] ?? "Unknown error";
87
+ const stack = event.attributes?.["exception.stacktrace"];
88
+ const fingerprint = generateFingerprint(errorType, message, stack);
89
+ this.db.insertError({
90
+ trace_id: traceId,
91
+ span_id: spanId,
92
+ fingerprint,
93
+ error_type: errorType ?? null,
94
+ message,
95
+ stack: stack ?? null,
96
+ agent: agent ?? null,
97
+ source: source ?? null,
98
+ });
99
+ }
100
+ }
101
+ }
102
+ async shutdown() {
103
+ this.db.close();
104
+ }
105
+ async forceFlush() {
106
+ // Writes are synchronous with better-sqlite3, nothing to flush
107
+ }
108
+ }
@@ -0,0 +1,30 @@
1
+ export declare const ATTR: {
2
+ readonly AGENT: "agent.name";
3
+ readonly SOURCE: "agent.source";
4
+ readonly RUN_ID: "agent.run_id";
5
+ readonly MODEL: "llm.model";
6
+ readonly PROVIDER: "llm.provider";
7
+ readonly INPUT_TOKENS: "llm.tokens.input";
8
+ readonly OUTPUT_TOKENS: "llm.tokens.output";
9
+ readonly CACHE_READ_TOKENS: "llm.tokens.cache_read";
10
+ readonly CACHE_CREATION_TOKENS: "llm.tokens.cache_creation";
11
+ readonly LATENCY_MS: "llm.latency_ms";
12
+ readonly STOP_REASON: "llm.stop_reason";
13
+ readonly TOOL_NAME: "tool.name";
14
+ readonly TOOL_STATUS: "tool.status";
15
+ readonly TOOL_DURATION_MS: "tool.duration_ms";
16
+ readonly SKILL_NAME: "skill.name";
17
+ readonly SKILL_TOOLS_LOADED: "skill.tools_loaded";
18
+ readonly ITERATION: "agent.iteration";
19
+ readonly JOB_ID: "cron.job_id";
20
+ readonly SCHEDULE: "cron.schedule";
21
+ readonly CHANNEL: "slack.channel";
22
+ readonly THREAD_TS: "slack.thread_ts";
23
+ readonly TOKENS_BEFORE: "compaction.tokens_before";
24
+ readonly TOKENS_AFTER: "compaction.tokens_after";
25
+ readonly FALLBACK_ORIGINAL: "llm.fallback.original_model";
26
+ readonly FALLBACK_MODEL: "llm.fallback.model";
27
+ readonly FALLBACK_ERROR: "llm.fallback.error";
28
+ readonly SUPPRESSED: "agent.suppressed";
29
+ readonly SPAWN_CHILD: "agent.spawn.child";
30
+ };
@@ -0,0 +1,31 @@
1
+ // Span attribute keys — centralized to avoid typos
2
+ export const ATTR = {
3
+ AGENT: "agent.name",
4
+ SOURCE: "agent.source",
5
+ RUN_ID: "agent.run_id",
6
+ MODEL: "llm.model",
7
+ PROVIDER: "llm.provider",
8
+ INPUT_TOKENS: "llm.tokens.input",
9
+ OUTPUT_TOKENS: "llm.tokens.output",
10
+ CACHE_READ_TOKENS: "llm.tokens.cache_read",
11
+ CACHE_CREATION_TOKENS: "llm.tokens.cache_creation",
12
+ LATENCY_MS: "llm.latency_ms",
13
+ STOP_REASON: "llm.stop_reason",
14
+ TOOL_NAME: "tool.name",
15
+ TOOL_STATUS: "tool.status",
16
+ TOOL_DURATION_MS: "tool.duration_ms",
17
+ SKILL_NAME: "skill.name",
18
+ SKILL_TOOLS_LOADED: "skill.tools_loaded",
19
+ ITERATION: "agent.iteration",
20
+ JOB_ID: "cron.job_id",
21
+ SCHEDULE: "cron.schedule",
22
+ CHANNEL: "slack.channel",
23
+ THREAD_TS: "slack.thread_ts",
24
+ TOKENS_BEFORE: "compaction.tokens_before",
25
+ TOKENS_AFTER: "compaction.tokens_after",
26
+ FALLBACK_ORIGINAL: "llm.fallback.original_model",
27
+ FALLBACK_MODEL: "llm.fallback.model",
28
+ FALLBACK_ERROR: "llm.fallback.error",
29
+ SUPPRESSED: "agent.suppressed",
30
+ SPAWN_CHILD: "agent.spawn.child",
31
+ };
@@ -2,7 +2,9 @@ import { ToolRegistry } from "../registry.js";
2
2
  import { readFileTool } from "./read-file.js";
3
3
  import { writeFileTool } from "./write-file.js";
4
4
  import { runCommandTool } from "./run-command.js";
5
+ import { createUpdateAgentConfigTool, createManageCronTool } from "./self-config.js";
5
6
  export { readFileTool, writeFileTool, runCommandTool };
7
+ export { createUpdateAgentConfigTool, createManageCronTool };
6
8
  interface BuiltinRegistryOptions {
7
9
  allowedCommands?: string[];
8
10
  allowedPaths?: string[];
@@ -4,7 +4,9 @@ import { createWriteFileTool, writeFileTool } from "./write-file.js";
4
4
  import { createRunCommandTool, runCommandTool } from "./run-command.js";
5
5
  import { createMemoryTools } from "./memory.js";
6
6
  import { createWebSearchTool } from "./web-search.js";
7
+ import { createUpdateAgentConfigTool, createManageCronTool } from "./self-config.js";
7
8
  export { readFileTool, writeFileTool, runCommandTool };
9
+ export { createUpdateAgentConfigTool, createManageCronTool };
8
10
  export function createBuiltinRegistry(options = {}) {
9
11
  const registry = new ToolRegistry();
10
12
  registry.register(createReadFileTool(options.allowedPaths));
@@ -0,0 +1,4 @@
1
+ import type { Tool } from "../types.js";
2
+ import type { ConfigWriter } from "../../config/writer.js";
3
+ export declare function createUpdateAgentConfigTool(agentName: string, writer: ConfigWriter): Tool;
4
+ export declare function createManageCronTool(agentName: string, writer: ConfigWriter): Tool;
@@ -0,0 +1,182 @@
1
+ import cron from "node-cron";
2
+ import { createLogger } from "../../logger.js";
3
+ const log = createLogger("tools:self-config");
4
+ const ALLOWED_CONFIG_FIELDS = [
5
+ "displayName", "emoji", "avatar", "model",
6
+ "maxIterations", "maxTokens", "heartbeat",
7
+ ];
8
+ function pickAllowed(input) {
9
+ const result = {};
10
+ for (const key of ALLOWED_CONFIG_FIELDS) {
11
+ if (key in input)
12
+ result[key] = input[key];
13
+ }
14
+ return result;
15
+ }
16
+ export function createUpdateAgentConfigTool(agentName, writer) {
17
+ return {
18
+ name: "update_agent_config",
19
+ description: `Update your own agent configuration. Allowed fields: ${ALLOWED_CONFIG_FIELDS.join(", ")}. Fields like tools, skills, sandbox, and slack cannot be changed.`,
20
+ parameters: {
21
+ type: "object",
22
+ properties: {
23
+ displayName: { type: "string", description: "Your display name" },
24
+ emoji: { type: "string", description: "Your emoji icon" },
25
+ avatar: { type: "string", description: "URL to your avatar image" },
26
+ model: { type: "string", description: "Model alias to use (must be a configured alias)" },
27
+ maxIterations: { type: "number", description: "Max agent loop iterations" },
28
+ maxTokens: { type: "number", description: "Max tokens per response" },
29
+ heartbeat: {
30
+ type: "object",
31
+ description: "Heartbeat settings (partial update)",
32
+ properties: {
33
+ enabled: { type: "boolean" },
34
+ intervalMinutes: { type: "number" },
35
+ model: { type: "string" },
36
+ },
37
+ },
38
+ },
39
+ },
40
+ execute: async (args) => {
41
+ try {
42
+ const updates = pickAllowed(args);
43
+ if (Object.keys(updates).length === 0) {
44
+ return "Error: No allowed fields provided. Allowed: " + ALLOWED_CONFIG_FIELDS.join(", ");
45
+ }
46
+ // Pre-validate model alias if provided
47
+ if (updates.model) {
48
+ const config = writer.read();
49
+ if (config.models) {
50
+ const aliases = config.models.map((m) => m.alias);
51
+ if (!aliases.includes(updates.model)) {
52
+ return `Error: Unknown model alias "${updates.model}". Available: ${aliases.join(", ")}`;
53
+ }
54
+ }
55
+ }
56
+ await writer.mutate((config) => {
57
+ const agent = config.agents[agentName];
58
+ if (!agent)
59
+ throw new Error(`Agent "${agentName}" not found in config`);
60
+ for (const [key, value] of Object.entries(updates)) {
61
+ if (key === "heartbeat" && typeof value === "object" && value !== null) {
62
+ agent.heartbeat = { ...agent.heartbeat, ...value };
63
+ }
64
+ else {
65
+ agent[key] = value;
66
+ }
67
+ }
68
+ });
69
+ const fields = Object.keys(updates).join(", ");
70
+ log.info({ agent: agentName, fields }, "agent config updated");
71
+ return `Updated ${agentName} config: ${fields}`;
72
+ }
73
+ catch (err) {
74
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
75
+ }
76
+ },
77
+ };
78
+ }
79
+ const MIN_CRON_INTERVAL_MINUTES = 5;
80
+ function isTooFrequent(schedule) {
81
+ const parts = schedule.trim().split(/\s+/);
82
+ if (parts.length < 5)
83
+ return false;
84
+ const minuteField = parts[0];
85
+ if (minuteField === "*")
86
+ return true;
87
+ const stepMatch = minuteField.match(/^\*\/(\d+)$/);
88
+ if (stepMatch && parseInt(stepMatch[1], 10) < MIN_CRON_INTERVAL_MINUTES)
89
+ return true;
90
+ return false;
91
+ }
92
+ export function createManageCronTool(agentName, writer) {
93
+ return {
94
+ name: "manage_cron",
95
+ description: "Manage your own cron jobs. Actions: list, create, update, delete. You can only manage jobs that belong to you.",
96
+ parameters: {
97
+ type: "object",
98
+ properties: {
99
+ action: { type: "string", enum: ["list", "create", "update", "delete"], description: "The operation to perform" },
100
+ id: { type: "string", description: "Cron job ID (required for create/update/delete)" },
101
+ schedule: { type: "string", description: "Cron schedule expression (create/update)" },
102
+ prompt: { type: "string", description: "The prompt to run (create/update)" },
103
+ enabled: { type: "boolean", description: "Whether the job is enabled (create/update)" },
104
+ },
105
+ required: ["action"],
106
+ },
107
+ execute: async (args) => {
108
+ try {
109
+ const action = args.action;
110
+ if (action === "list") {
111
+ const config = writer.read();
112
+ const jobs = (config.cron ?? []).filter((j) => j.agent === agentName);
113
+ return JSON.stringify(jobs, null, 2);
114
+ }
115
+ const id = args.id;
116
+ if (!id)
117
+ return "Error: id is required for create/update/delete";
118
+ if (action === "create") {
119
+ const schedule = args.schedule;
120
+ const prompt = args.prompt;
121
+ const enabled = args.enabled ?? true;
122
+ if (!schedule || !prompt)
123
+ return "Error: schedule and prompt are required for create";
124
+ if (!cron.validate(schedule))
125
+ return `Error: Invalid cron schedule "${schedule}"`;
126
+ if (isTooFrequent(schedule))
127
+ return `Error: Schedule runs too frequently. Minimum interval is ${MIN_CRON_INTERVAL_MINUTES} minutes.`;
128
+ await writer.mutate((config) => {
129
+ config.cron = config.cron ?? [];
130
+ if (config.cron.some((j) => j.id === id)) {
131
+ throw new Error(`Cron job "${id}" already exists`);
132
+ }
133
+ config.cron.push({ id, agent: agentName, schedule, prompt, enabled });
134
+ });
135
+ log.info({ agent: agentName, jobId: id }, "cron job created");
136
+ return `Created cron job "${id}"`;
137
+ }
138
+ if (action === "update") {
139
+ if (args.schedule) {
140
+ const schedule = args.schedule;
141
+ if (!cron.validate(schedule))
142
+ return `Error: Invalid cron schedule "${schedule}"`;
143
+ if (isTooFrequent(schedule))
144
+ return `Error: Schedule runs too frequently. Minimum interval is ${MIN_CRON_INTERVAL_MINUTES} minutes.`;
145
+ }
146
+ await writer.mutate((config) => {
147
+ const job = (config.cron ?? []).find((j) => j.id === id);
148
+ if (!job)
149
+ throw new Error(`Cron job "${id}" not found`);
150
+ if (job.agent !== agentName)
151
+ throw new Error(`Cron job "${id}" belongs to "${job.agent}", not "${agentName}"`);
152
+ if (args.schedule)
153
+ job.schedule = args.schedule;
154
+ if (args.prompt)
155
+ job.prompt = args.prompt;
156
+ if (args.enabled !== undefined)
157
+ job.enabled = args.enabled;
158
+ });
159
+ log.info({ agent: agentName, jobId: id }, "cron job updated");
160
+ return `Updated cron job "${id}"`;
161
+ }
162
+ if (action === "delete") {
163
+ await writer.mutate((config) => {
164
+ const idx = (config.cron ?? []).findIndex((j) => j.id === id);
165
+ if (idx === -1)
166
+ throw new Error(`Cron job "${id}" not found`);
167
+ if (config.cron[idx].agent !== agentName) {
168
+ throw new Error(`Cron job "${id}" belongs to "${config.cron[idx].agent}", not "${agentName}"`);
169
+ }
170
+ config.cron.splice(idx, 1);
171
+ });
172
+ log.info({ agent: agentName, jobId: id }, "cron job deleted");
173
+ return `Deleted cron job "${id}"`;
174
+ }
175
+ return `Error: Unknown action "${action}"`;
176
+ }
177
+ catch (err) {
178
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
179
+ }
180
+ },
181
+ };
182
+ }
@@ -1,3 +1,5 @@
1
+ import { createLogger } from "../logger.js";
2
+ const log = createLogger("tools");
1
3
  export class ToolRegistry {
2
4
  tools = new Map();
3
5
  register(tool) {
@@ -5,7 +7,12 @@ export class ToolRegistry {
5
7
  }
6
8
  resolve(names) {
7
9
  return names
8
- .map((name) => this.tools.get(name))
10
+ .map((name) => {
11
+ const tool = this.tools.get(name);
12
+ if (!tool)
13
+ log.warn(`Unknown tool "${name}" — skipping`);
14
+ return tool;
15
+ })
9
16
  .filter((tool) => !!tool);
10
17
  }
11
18
  list() {
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@alejandroroman/agent-kit",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
7
- ".": "./dist/index.js",
8
- "./bootstrap": "./dist/bootstrap.js"
7
+ ".": "./dist/index.js"
9
8
  },
10
9
  "bin": {
11
10
  "agent-kit": "./dist/cli.js"
@@ -13,23 +12,16 @@
13
12
  "files": [
14
13
  "dist/"
15
14
  ],
16
- "scripts": {
17
- "build": "pnpm --filter @agent-kit/memory run build && tsc && cp -r packages/memory/dist dist/_memory && sed -i '' 's|from \"@agent-kit/memory\"|from \"../../_memory/index.js\"|g' dist/tools/builtin/memory.js",
18
- "prepublishOnly": "pnpm run build",
19
- "cli": "tsx src/cli.ts",
20
- "dev": "doppler run -- tsx src/index.ts",
21
- "dev:local": "tsx src/index.ts",
22
- "memory": "doppler run -- pnpm --filter @agent-kit/memory start",
23
- "ledger": "doppler run -- pnpm --filter @agent-kit/ledger ledger",
24
- "dev:dashboard": "pnpm --filter @agent-kit/dashboard dev",
25
- "agent:validate": "tsx src/scripts/validate-agent-cli.ts",
26
- "test": "vitest run",
27
- "test:watch": "vitest"
28
- },
29
15
  "dependencies": {
30
16
  "@anthropic-ai/sdk": "^0.78.0",
31
- "@modelcontextprotocol/sdk": "^1.12.1",
32
17
  "@clack/prompts": "^1.0.1",
18
+ "@modelcontextprotocol/sdk": "^1.12.1",
19
+ "@opentelemetry/api": "^1.9.0",
20
+ "@opentelemetry/context-async-hooks": "^2.6.0",
21
+ "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
22
+ "@opentelemetry/sdk-node": "^0.213.0",
23
+ "@opentelemetry/sdk-trace-base": "^2.6.0",
24
+ "@sentry/node": "^10.42.0",
33
25
  "@slack/bolt": "^4.6.0",
34
26
  "better-sqlite3": "^11.9.1",
35
27
  "nanoid": "^5.1.5",
@@ -37,17 +29,31 @@
37
29
  "openai": "^6.24.0",
38
30
  "pino": "^10.3.1",
39
31
  "pino-pretty": "^13.1.3",
32
+ "sharp": "0.33.5",
40
33
  "sqlite-vec": "^0.1.6",
41
34
  "twitter-api-v2": "^1.29.0",
42
35
  "zod": "^4.3.6"
43
36
  },
44
37
  "devDependencies": {
45
- "@agent-kit/memory": "workspace:*",
46
38
  "@types/better-sqlite3": "^7.6.13",
47
39
  "@types/node": "^25.3.0",
48
40
  "@types/node-cron": "^3.0.11",
41
+ "@types/sharp": "^0.32.0",
49
42
  "tsx": "^4.21.0",
50
43
  "typescript": "^5.9.3",
51
- "vitest": "^4.0.18"
44
+ "vitest": "^4.0.18",
45
+ "@agent-kit/memory": "0.1.0"
46
+ },
47
+ "scripts": {
48
+ "build": "pnpm --filter @agent-kit/memory run build && tsc && cp -r packages/memory/dist dist/_memory && node -e \"const f='dist/tools/builtin/memory.js',s=require('fs');s.writeFileSync(f,s.readFileSync(f,'utf8').replaceAll('from \\\"@agent-kit/memory\\\"','from \\\"../../_memory/index.js\\\"'))\"",
49
+ "cli": "tsx src/cli.ts",
50
+ "dev": "doppler run -- tsx src/index.ts",
51
+ "dev:local": "tsx src/index.ts",
52
+ "memory": "doppler run -- pnpm --filter @agent-kit/memory start",
53
+ "ledger": "doppler run -- pnpm --filter @agent-kit/ledger ledger",
54
+ "dev:dashboard": "pnpm --filter @agent-kit/dashboard dev",
55
+ "agent:validate": "tsx src/scripts/validate-agent-cli.ts",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest"
52
58
  }
53
- }
59
+ }