@alejandroroman/agent-kit 0.1.4 → 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 (76) hide show
  1. package/dist/_memory/dist/server.js +0 -0
  2. package/dist/_memory/server.js +0 -0
  3. package/dist/agent/loop.js +210 -111
  4. package/dist/api/errors.d.ts +3 -0
  5. package/dist/api/errors.js +37 -0
  6. package/dist/api/events.d.ts +5 -0
  7. package/dist/api/events.js +28 -0
  8. package/dist/api/router.js +10 -0
  9. package/dist/api/traces.d.ts +3 -0
  10. package/dist/api/traces.js +35 -0
  11. package/dist/api/types.d.ts +2 -0
  12. package/dist/bootstrap.d.ts +3 -1
  13. package/dist/bootstrap.js +26 -7
  14. package/dist/cli/chat.js +3 -1
  15. package/dist/cli/claude-md-template.d.ts +5 -0
  16. package/dist/cli/claude-md-template.js +220 -0
  17. package/dist/cli/config-writer.js +3 -0
  18. package/dist/cli/env.d.ts +14 -0
  19. package/dist/cli/env.js +68 -0
  20. package/dist/cli/init.js +10 -0
  21. package/dist/cli/slack-setup.d.ts +6 -0
  22. package/dist/cli/slack-setup.js +234 -0
  23. package/dist/cli/start.js +65 -16
  24. package/dist/cli/ui.d.ts +2 -0
  25. package/dist/cli/ui.js +4 -1
  26. package/dist/cli/whats-new.d.ts +1 -0
  27. package/dist/cli/whats-new.js +69 -0
  28. package/dist/cli.js +14 -0
  29. package/dist/config/resolve.d.ts +1 -0
  30. package/dist/config/resolve.js +1 -0
  31. package/dist/config/schema.d.ts +2 -0
  32. package/dist/config/schema.js +1 -0
  33. package/dist/config/writer.d.ts +18 -0
  34. package/dist/config/writer.js +85 -0
  35. package/dist/cron/scheduler.d.ts +4 -1
  36. package/dist/cron/scheduler.js +99 -52
  37. package/dist/gateways/slack/client.d.ts +1 -0
  38. package/dist/gateways/slack/client.js +9 -0
  39. package/dist/gateways/slack/handler.js +2 -1
  40. package/dist/gateways/slack/index.js +75 -29
  41. package/dist/gateways/slack/listener.d.ts +8 -1
  42. package/dist/gateways/slack/listener.js +36 -10
  43. package/dist/heartbeat/runner.js +99 -82
  44. package/dist/llm/anthropic.d.ts +1 -0
  45. package/dist/llm/anthropic.js +11 -2
  46. package/dist/llm/fallback.js +34 -2
  47. package/dist/llm/openai.d.ts +2 -0
  48. package/dist/llm/openai.js +33 -2
  49. package/dist/llm/types.d.ts +16 -2
  50. package/dist/llm/types.js +9 -0
  51. package/dist/logger.js +8 -0
  52. package/dist/media/sanitize.d.ts +5 -0
  53. package/dist/media/sanitize.js +53 -0
  54. package/dist/multi/spawn.js +29 -10
  55. package/dist/session/compaction.js +3 -1
  56. package/dist/session/prune-images.d.ts +9 -0
  57. package/dist/session/prune-images.js +42 -0
  58. package/dist/skills/activate.d.ts +6 -0
  59. package/dist/skills/activate.js +72 -27
  60. package/dist/skills/index.d.ts +1 -1
  61. package/dist/skills/index.js +1 -1
  62. package/dist/telemetry/db.d.ts +63 -0
  63. package/dist/telemetry/db.js +193 -0
  64. package/dist/telemetry/index.d.ts +17 -0
  65. package/dist/telemetry/index.js +82 -0
  66. package/dist/telemetry/sanitize.d.ts +6 -0
  67. package/dist/telemetry/sanitize.js +48 -0
  68. package/dist/telemetry/sqlite-processor.d.ts +11 -0
  69. package/dist/telemetry/sqlite-processor.js +108 -0
  70. package/dist/telemetry/types.d.ts +30 -0
  71. package/dist/telemetry/types.js +31 -0
  72. package/dist/tools/builtin/index.d.ts +2 -0
  73. package/dist/tools/builtin/index.js +2 -0
  74. package/dist/tools/builtin/self-config.d.ts +4 -0
  75. package/dist/tools/builtin/self-config.js +182 -0
  76. package/package.json +25 -18
package/dist/cli/chat.js CHANGED
@@ -2,6 +2,7 @@ import { loadConfig } from "../config/index.js";
2
2
  import { buildAgentRuntime } from "../bootstrap.js";
3
3
  import { ensureOllama } from "./ollama.js";
4
4
  import { resolveApiKey } from "./ui.js";
5
+ import { showWhatsNew } from "./whats-new.js";
5
6
  import { CONFIG_PATH, DATA_DIR, SKILLS_DIR } from "./paths.js";
6
7
  import { startRepl } from "./repl.js";
7
8
  export async function chat(agentName) {
@@ -19,6 +20,7 @@ export async function chat(agentName) {
19
20
  }
20
21
  }
21
22
  await resolveApiKey({ save: false });
23
+ showWhatsNew();
22
24
  const config = loadConfig(CONFIG_PATH);
23
25
  if (!config.agents[agentName]) {
24
26
  const names = Object.keys(config.agents).join(", ");
@@ -31,7 +33,7 @@ export async function chat(agentName) {
31
33
  console.log(" (Ollama not running — memory tools unavailable)\n");
32
34
  }
33
35
  }
34
- const runtime = buildAgentRuntime(agentName, config, {
36
+ const runtime = await buildAgentRuntime(agentName, config, {
35
37
  dataDir: DATA_DIR,
36
38
  skillsDir: SKILLS_DIR,
37
39
  });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CLAUDE.md template generated by `agent-kit init`.
3
+ * Gives any Claude Code session full context for working on this project.
4
+ */
5
+ export declare const CLAUDE_MD_TEMPLATE = "# Agent Kit Project\n\nPersonal AI agent framework \u2014 multi-model agent loop with Slack gateway, cron scheduling, skills, and tool sandboxing.\n\nPowered by [`@alejandroroman/agent-kit`](https://www.npmjs.com/package/@alejandroroman/agent-kit).\n\n## Run Commands\n\n- `pnpm start` \u2014 start all services (cron, Slack, heartbeat, REPL)\n- `pnpm chat` \u2014 lightweight single-agent chat\n- `pnpm create` \u2014 AI-powered agent creation wizard\n\n## Project Layout\n\n```\nagent-kit.json # Main config \u2014 agents, cron jobs, models, defaults\n.env # Secrets (API keys, Slack tokens)\ndata/ # Runtime data (per-agent sessions, SOUL.md files, DBs)\n agents/<name>/\n SOUL.md # Agent personality / system prompt\n sessions/ # JSONL conversation logs\nskills/ # Custom skill definitions\n <skill-name>/\n skill.json # Manifest (name, description, tools list)\n SKILL.md # Instructions injected when skill is activated\n tools/ # Tool implementations (TypeScript)\n```\n\n## Configuration (agent-kit.json)\n\n### Models\n\n```json\n\"models\": [\n { \"model\": \"anthropic:claude-sonnet-4-6\", \"alias\": \"sonnet\" },\n { \"model\": \"anthropic:claude-haiku-4-5-20251001\", \"alias\": \"haiku\" }\n]\n```\n\nAgents reference models by alias. The framework handles automatic fallback between models on rate limits/server errors.\n\n### Agent Definition\n\n```json\n\"agents\": {\n \"my-agent\": {\n \"displayName\": \"My Agent\",\n \"emoji\": \"\uD83E\uDD16\",\n \"model\": \"sonnet\",\n \"tools\": [\"read_file\", \"write_file\", \"web_search\"],\n \"skills\": [\"my-skill\"],\n \"sandbox\": { \"allowedPaths\": [\"data/agents/my-agent/\"] },\n \"slack\": { \"channelId\": \"C0XXX\", \"channelName\": \"#my-channel\" },\n \"heartbeat\": { \"enabled\": true, \"intervalMinutes\": 60, \"model\": \"haiku\" },\n \"can_spawn\": [{ \"agent\": \"helper\", \"tool\": \"delegate\", \"description\": \"...\" }]\n }\n}\n```\n\n**Key fields:**\n- `tools` \u2014 builtin tools the agent can use (see list below)\n- `skills` \u2014 skill directories to make available via `activate_skill`\n- `sandbox` \u2014 restrict file access (`allowedPaths`) or commands (`allowedCommands`)\n- `slack` \u2014 bind agent to a Slack channel for inbound/outbound messages\n- `heartbeat` \u2014 periodic autonomous check-ins (posts to Slack if something noteworthy)\n- `spawn_only: true` \u2014 agent can only be invoked by other agents, not directly\n\n### Cron Jobs\n\n```json\n\"cron\": [\n {\n \"id\": \"daily-task\",\n \"agent\": \"my-agent\",\n \"schedule\": \"0 8 * * *\",\n \"prompt\": \"Activate the my-skill skill. Do the daily task.\",\n \"enabled\": true\n }\n]\n```\n\nSchedule uses standard cron syntax. Agents must activate their skill first in the prompt.\n\n### Defaults\n\n```json\n\"defaults\": {\n \"model\": \"sonnet\",\n \"maxTokens\": 4096,\n \"maxIterations\": 20,\n \"memory\": {\n \"dbPath\": \"./data/memory.db\",\n \"ollamaEndpoint\": \"http://localhost:11434\",\n \"ollamaModel\": \"all-minilm:l6-v2\"\n }\n}\n```\n\n## Built-in Tools\n\n| Tool | Description |\n|------|-------------|\n| `read_file` | Read a file (respects sandbox allowedPaths) |\n| `write_file` | Write a file (respects sandbox allowedPaths) |\n| `run_command` | Execute a shell command (respects sandbox allowedCommands) |\n| `web_search` | Search the web (Brave or Grok provider) |\n| `store_memory` | Store a memory with content and metadata |\n| `search_memory` | Semantic search across stored memories |\n| `get_memory` | Retrieve a specific memory by ID |\n| `update_memory` | Update an existing memory |\n| `forget_memory` | Delete a memory |\n| `list_memories` | List all memories with optional filtering |\n| `update_agent_config` | Agent updates its own config (allowlisted fields) |\n| `manage_cron` | Agent manages its own cron jobs (CRUD) |\n\nMemory tools require Ollama running with `all-minilm:l6-v2` model.\n\n## Creating Skills\n\nA skill is a directory in `skills/` with:\n\n1. **`skill.json`** \u2014 manifest\n```json\n{\n \"name\": \"my-skill\",\n \"description\": \"What this skill does\",\n \"tools\": [\"my_tool\"],\n \"prompt\": \"SKILL.md\"\n}\n```\n\n2. **`SKILL.md`** \u2014 instructions injected into agent context when activated\n\n3. **`tools/<name>.ts`** \u2014 tool implementations\n```typescript\nimport type { Tool } from \"@alejandroroman/agent-kit/dist/tools/types.js\";\n\nconst tool: Tool = {\n name: \"my_tool\",\n description: \"What this tool does\",\n parameters: {\n type: \"object\",\n properties: {\n input: { type: \"string\", description: \"The input\" }\n },\n required: [\"input\"]\n },\n execute: async (args) => {\n return \"result\";\n },\n};\n\nexport default tool;\n```\n\n## Environment Variables\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `ANTHROPIC_API_KEY` | Yes | Anthropic API key for Claude models |\n| `OPENAI_API_KEY` | No | OpenAI API key (for fallback models) |\n| `SLACK_BOT_TOKEN` | No | Slack bot token (xoxb-...) for gateway |\n| `SLACK_APP_TOKEN` | No | Slack app token (xapp-...) for Socket Mode |\n| `BRAVE_SEARCH_API_KEY` | No | Brave Search API key for web_search |\n| `XAI_API_KEY` | No | xAI/Grok API key for web_search |\n\n## Slack Setup\n\n1. Create a Slack app at api.slack.com/apps \u2192 From Manifest (YAML):\n```yaml\ndisplay_information:\n name: Agent Kit\nsettings:\n socket_mode_enabled: true\nfeatures:\n bot_user:\n display_name: Agent Kit\n always_online: true\n event_subscriptions:\n bot_events:\n - message.channels\n - message.groups\noauth_config:\n scopes:\n bot:\n - chat:write\n - channels:history\n - channels:read\n - groups:history\n - groups:read\n```\n2. Generate an App-Level Token (Settings \u2192 Basic Information) with `connections:write` scope \u2192 `SLACK_APP_TOKEN`\n3. Install to workspace \u2192 copy Bot Token \u2192 `SLACK_BOT_TOKEN`\n4. Create channels, invite the bot (`/invite @Agent Kit`), copy channel IDs\n5. Add `slack` binding to each agent in `agent-kit.json`\n\n## Ollama (for Memory)\n\nMemory tools use Ollama for local embeddings:\n```bash\n# Install: https://ollama.com\nollama pull all-minilm:l6-v2\n# Ollama must be running when agent-kit starts\n```\n\nIf Ollama isn't running, everything else works \u2014 memory tools are just unavailable.\n\n## pnpm v10 Note\n\nIf using pnpm v10+, native dependencies need explicit approval:\n```json\n// In package.json:\n\"pnpm\": { \"onlyBuiltDependencies\": [\"better-sqlite3\"] }\n```\nThen `pnpm install` to rebuild native bindings.\n";
@@ -0,0 +1,220 @@
1
+ /**
2
+ * CLAUDE.md template generated by `agent-kit init`.
3
+ * Gives any Claude Code session full context for working on this project.
4
+ */
5
+ export const CLAUDE_MD_TEMPLATE = `# Agent Kit Project
6
+
7
+ Personal AI agent framework — multi-model agent loop with Slack gateway, cron scheduling, skills, and tool sandboxing.
8
+
9
+ Powered by [\`@alejandroroman/agent-kit\`](https://www.npmjs.com/package/@alejandroroman/agent-kit).
10
+
11
+ ## Run Commands
12
+
13
+ - \`pnpm start\` — start all services (cron, Slack, heartbeat, REPL)
14
+ - \`pnpm chat\` — lightweight single-agent chat
15
+ - \`pnpm create\` — AI-powered agent creation wizard
16
+
17
+ ## Project Layout
18
+
19
+ \`\`\`
20
+ agent-kit.json # Main config — agents, cron jobs, models, defaults
21
+ .env # Secrets (API keys, Slack tokens)
22
+ data/ # Runtime data (per-agent sessions, SOUL.md files, DBs)
23
+ agents/<name>/
24
+ SOUL.md # Agent personality / system prompt
25
+ sessions/ # JSONL conversation logs
26
+ skills/ # Custom skill definitions
27
+ <skill-name>/
28
+ skill.json # Manifest (name, description, tools list)
29
+ SKILL.md # Instructions injected when skill is activated
30
+ tools/ # Tool implementations (TypeScript)
31
+ \`\`\`
32
+
33
+ ## Configuration (agent-kit.json)
34
+
35
+ ### Models
36
+
37
+ \`\`\`json
38
+ "models": [
39
+ { "model": "anthropic:claude-sonnet-4-6", "alias": "sonnet" },
40
+ { "model": "anthropic:claude-haiku-4-5-20251001", "alias": "haiku" }
41
+ ]
42
+ \`\`\`
43
+
44
+ Agents reference models by alias. The framework handles automatic fallback between models on rate limits/server errors.
45
+
46
+ ### Agent Definition
47
+
48
+ \`\`\`json
49
+ "agents": {
50
+ "my-agent": {
51
+ "displayName": "My Agent",
52
+ "emoji": "🤖",
53
+ "model": "sonnet",
54
+ "tools": ["read_file", "write_file", "web_search"],
55
+ "skills": ["my-skill"],
56
+ "sandbox": { "allowedPaths": ["data/agents/my-agent/"] },
57
+ "slack": { "channelId": "C0XXX", "channelName": "#my-channel" },
58
+ "heartbeat": { "enabled": true, "intervalMinutes": 60, "model": "haiku" },
59
+ "can_spawn": [{ "agent": "helper", "tool": "delegate", "description": "..." }]
60
+ }
61
+ }
62
+ \`\`\`
63
+
64
+ **Key fields:**
65
+ - \`tools\` — builtin tools the agent can use (see list below)
66
+ - \`skills\` — skill directories to make available via \`activate_skill\`
67
+ - \`sandbox\` — restrict file access (\`allowedPaths\`) or commands (\`allowedCommands\`)
68
+ - \`slack\` — bind agent to a Slack channel for inbound/outbound messages
69
+ - \`heartbeat\` — periodic autonomous check-ins (posts to Slack if something noteworthy)
70
+ - \`spawn_only: true\` — agent can only be invoked by other agents, not directly
71
+
72
+ ### Cron Jobs
73
+
74
+ \`\`\`json
75
+ "cron": [
76
+ {
77
+ "id": "daily-task",
78
+ "agent": "my-agent",
79
+ "schedule": "0 8 * * *",
80
+ "prompt": "Activate the my-skill skill. Do the daily task.",
81
+ "enabled": true
82
+ }
83
+ ]
84
+ \`\`\`
85
+
86
+ Schedule uses standard cron syntax. Agents must activate their skill first in the prompt.
87
+
88
+ ### Defaults
89
+
90
+ \`\`\`json
91
+ "defaults": {
92
+ "model": "sonnet",
93
+ "maxTokens": 4096,
94
+ "maxIterations": 20,
95
+ "memory": {
96
+ "dbPath": "./data/memory.db",
97
+ "ollamaEndpoint": "http://localhost:11434",
98
+ "ollamaModel": "all-minilm:l6-v2"
99
+ }
100
+ }
101
+ \`\`\`
102
+
103
+ ## Built-in Tools
104
+
105
+ | Tool | Description |
106
+ |------|-------------|
107
+ | \`read_file\` | Read a file (respects sandbox allowedPaths) |
108
+ | \`write_file\` | Write a file (respects sandbox allowedPaths) |
109
+ | \`run_command\` | Execute a shell command (respects sandbox allowedCommands) |
110
+ | \`web_search\` | Search the web (Brave or Grok provider) |
111
+ | \`store_memory\` | Store a memory with content and metadata |
112
+ | \`search_memory\` | Semantic search across stored memories |
113
+ | \`get_memory\` | Retrieve a specific memory by ID |
114
+ | \`update_memory\` | Update an existing memory |
115
+ | \`forget_memory\` | Delete a memory |
116
+ | \`list_memories\` | List all memories with optional filtering |
117
+ | \`update_agent_config\` | Agent updates its own config (allowlisted fields) |
118
+ | \`manage_cron\` | Agent manages its own cron jobs (CRUD) |
119
+
120
+ Memory tools require Ollama running with \`all-minilm:l6-v2\` model.
121
+
122
+ ## Creating Skills
123
+
124
+ A skill is a directory in \`skills/\` with:
125
+
126
+ 1. **\`skill.json\`** — manifest
127
+ \`\`\`json
128
+ {
129
+ "name": "my-skill",
130
+ "description": "What this skill does",
131
+ "tools": ["my_tool"],
132
+ "prompt": "SKILL.md"
133
+ }
134
+ \`\`\`
135
+
136
+ 2. **\`SKILL.md\`** — instructions injected into agent context when activated
137
+
138
+ 3. **\`tools/<name>.ts\`** — tool implementations
139
+ \`\`\`typescript
140
+ import type { Tool } from "@alejandroroman/agent-kit/dist/tools/types.js";
141
+
142
+ const tool: Tool = {
143
+ name: "my_tool",
144
+ description: "What this tool does",
145
+ parameters: {
146
+ type: "object",
147
+ properties: {
148
+ input: { type: "string", description: "The input" }
149
+ },
150
+ required: ["input"]
151
+ },
152
+ execute: async (args) => {
153
+ return "result";
154
+ },
155
+ };
156
+
157
+ export default tool;
158
+ \`\`\`
159
+
160
+ ## Environment Variables
161
+
162
+ | Variable | Required | Description |
163
+ |----------|----------|-------------|
164
+ | \`ANTHROPIC_API_KEY\` | Yes | Anthropic API key for Claude models |
165
+ | \`OPENAI_API_KEY\` | No | OpenAI API key (for fallback models) |
166
+ | \`SLACK_BOT_TOKEN\` | No | Slack bot token (xoxb-...) for gateway |
167
+ | \`SLACK_APP_TOKEN\` | No | Slack app token (xapp-...) for Socket Mode |
168
+ | \`BRAVE_SEARCH_API_KEY\` | No | Brave Search API key for web_search |
169
+ | \`XAI_API_KEY\` | No | xAI/Grok API key for web_search |
170
+
171
+ ## Slack Setup
172
+
173
+ 1. Create a Slack app at api.slack.com/apps → From Manifest (YAML):
174
+ \`\`\`yaml
175
+ display_information:
176
+ name: Agent Kit
177
+ settings:
178
+ socket_mode_enabled: true
179
+ features:
180
+ bot_user:
181
+ display_name: Agent Kit
182
+ always_online: true
183
+ event_subscriptions:
184
+ bot_events:
185
+ - message.channels
186
+ - message.groups
187
+ oauth_config:
188
+ scopes:
189
+ bot:
190
+ - chat:write
191
+ - channels:history
192
+ - channels:read
193
+ - groups:history
194
+ - groups:read
195
+ \`\`\`
196
+ 2. Generate an App-Level Token (Settings → Basic Information) with \`connections:write\` scope → \`SLACK_APP_TOKEN\`
197
+ 3. Install to workspace → copy Bot Token → \`SLACK_BOT_TOKEN\`
198
+ 4. Create channels, invite the bot (\`/invite @Agent Kit\`), copy channel IDs
199
+ 5. Add \`slack\` binding to each agent in \`agent-kit.json\`
200
+
201
+ ## Ollama (for Memory)
202
+
203
+ Memory tools use Ollama for local embeddings:
204
+ \`\`\`bash
205
+ # Install: https://ollama.com
206
+ ollama pull all-minilm:l6-v2
207
+ # Ollama must be running when agent-kit starts
208
+ \`\`\`
209
+
210
+ If Ollama isn't running, everything else works — memory tools are just unavailable.
211
+
212
+ ## pnpm v10 Note
213
+
214
+ If using pnpm v10+, native dependencies need explicit approval:
215
+ \`\`\`json
216
+ // In package.json:
217
+ "pnpm": { "onlyBuiltDependencies": ["better-sqlite3"] }
218
+ \`\`\`
219
+ Then \`pnpm install\` to rebuild native bindings.
220
+ `;
@@ -103,6 +103,9 @@ export function scaffoldProjectPackageJson(projectDir, projectName) {
103
103
  dependencies: {
104
104
  "@alejandroroman/agent-kit": "^0.1.0",
105
105
  },
106
+ pnpm: {
107
+ onlyBuiltDependencies: ["better-sqlite3"],
108
+ },
106
109
  };
107
110
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
108
111
  // Create .gitignore if missing
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Parse a .env file into a key-value map.
3
+ * Strips quotes, ignores comments and blank lines.
4
+ */
5
+ export declare function loadEnvFile(envPath: string): Record<string, string>;
6
+ /**
7
+ * Upsert a key in a .env file. Replaces if exists, appends if not.
8
+ * Values are always quoted to prevent shell issues.
9
+ */
10
+ export declare function upsertEnvKey(envPath: string, key: string, value: string): void;
11
+ /**
12
+ * Load specific keys from .env into process.env (if not already set).
13
+ */
14
+ export declare function loadEnvIntoProcess(envPath: string, keys: string[]): void;
@@ -0,0 +1,68 @@
1
+ import * as fs from "fs";
2
+ /**
3
+ * Parse a .env file into a key-value map.
4
+ * Strips quotes, ignores comments and blank lines.
5
+ */
6
+ export function loadEnvFile(envPath) {
7
+ if (!fs.existsSync(envPath))
8
+ return {};
9
+ const result = {};
10
+ const content = fs.readFileSync(envPath, "utf-8");
11
+ for (const line of content.split("\n")) {
12
+ const trimmed = line.trim();
13
+ if (!trimmed || trimmed.startsWith("#"))
14
+ continue;
15
+ const eqIndex = trimmed.indexOf("=");
16
+ if (eqIndex === -1)
17
+ continue;
18
+ const key = trimmed.slice(0, eqIndex).trim();
19
+ let value = trimmed.slice(eqIndex + 1).trim();
20
+ if ((value.startsWith('"') && value.endsWith('"')) ||
21
+ (value.startsWith("'") && value.endsWith("'"))) {
22
+ value = value.slice(1, -1);
23
+ }
24
+ result[key] = value;
25
+ }
26
+ return result;
27
+ }
28
+ /**
29
+ * Upsert a key in a .env file. Replaces if exists, appends if not.
30
+ * Values are always quoted to prevent shell issues.
31
+ */
32
+ export function upsertEnvKey(envPath, key, value) {
33
+ const quotedLine = `${key}="${value}"`;
34
+ if (!fs.existsSync(envPath)) {
35
+ fs.writeFileSync(envPath, quotedLine + "\n", { mode: 0o600 });
36
+ return;
37
+ }
38
+ const content = fs.readFileSync(envPath, "utf-8");
39
+ const lines = content.split("\n");
40
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
+ const pattern = new RegExp(`^${escaped}\\s*=`);
42
+ let replaced = false;
43
+ const updated = lines.map((line) => {
44
+ if (pattern.test(line.trim())) {
45
+ replaced = true;
46
+ return quotedLine;
47
+ }
48
+ return line;
49
+ });
50
+ if (!replaced) {
51
+ const needsNewline = content.length > 0 && !content.endsWith("\n");
52
+ fs.writeFileSync(envPath, content + (needsNewline ? "\n" : "") + quotedLine + "\n", { mode: 0o600 });
53
+ }
54
+ else {
55
+ fs.writeFileSync(envPath, updated.join("\n"), { mode: 0o600 });
56
+ }
57
+ }
58
+ /**
59
+ * Load specific keys from .env into process.env (if not already set).
60
+ */
61
+ export function loadEnvIntoProcess(envPath, keys) {
62
+ const env = loadEnvFile(envPath);
63
+ for (const key of keys) {
64
+ if (!process.env[key] && env[key]) {
65
+ process.env[key] = env[key];
66
+ }
67
+ }
68
+ }
package/dist/cli/init.js CHANGED
@@ -5,7 +5,10 @@ import { banner, isCancel, done, resolveApiKey } from "./ui.js";
5
5
  import { checkOllama } from "./ollama.js";
6
6
  import { createFreshConfig, scaffoldProjectPackageJson } from "./config-writer.js";
7
7
  import { runSetupAgent } from "./setup-agent/index.js";
8
+ import { setupSlack } from "./slack-setup.js";
8
9
  import { CONFIG_PATH, DATA_DIR, SKILLS_DIR } from "./paths.js";
10
+ import { CLAUDE_MD_TEMPLATE } from "./claude-md-template.js";
11
+ import * as path from "path";
9
12
  function detectPackageManager() {
10
13
  try {
11
14
  execSync("pnpm --version", { stdio: "ignore" });
@@ -40,6 +43,11 @@ export async function init() {
40
43
  createFreshConfig({ configPath: CONFIG_PATH });
41
44
  fs.mkdirSync(DATA_DIR, { recursive: true });
42
45
  fs.mkdirSync(SKILLS_DIR, { recursive: true });
46
+ // Generate CLAUDE.md for Claude Code context
47
+ const claudeMdPath = path.join(process.cwd(), "CLAUDE.md");
48
+ if (!fs.existsSync(claudeMdPath)) {
49
+ fs.writeFileSync(claudeMdPath, CLAUDE_MD_TEMPLATE);
50
+ }
43
51
  }
44
52
  catch (err) {
45
53
  s.stop(`Failed to create project: ${err instanceof Error ? err.message : err}`);
@@ -79,4 +87,6 @@ export async function init() {
79
87
  });
80
88
  console.log();
81
89
  done(summary);
90
+ // Offer Slack setup after agent creation
91
+ await setupSlack({ configPath: CONFIG_PATH, fromInit: true });
82
92
  }
@@ -0,0 +1,6 @@
1
+ interface SetupSlackOpts {
2
+ configPath: string;
3
+ fromInit?: boolean;
4
+ }
5
+ export declare function setupSlack(opts: SetupSlackOpts): Promise<void>;
6
+ export {};
@@ -0,0 +1,234 @@
1
+ import * as path from "path";
2
+ import { execFileSync } from "child_process";
3
+ import * as p from "@clack/prompts";
4
+ import { isCancel } from "./ui.js";
5
+ import { upsertEnvKey, loadEnvFile } from "./env.js";
6
+ import { readRawConfig, writeConfig } from "./config-writer.js";
7
+ import { loadConfig } from "../config/index.js";
8
+ const SLACK_MANIFEST = `display_information:
9
+ name: Agent Kit
10
+ settings:
11
+ socket_mode_enabled: true
12
+ features:
13
+ bot_user:
14
+ display_name: Agent Kit
15
+ always_online: true
16
+ event_subscriptions:
17
+ bot_events:
18
+ - message.channels
19
+ - message.groups
20
+ oauth_config:
21
+ scopes:
22
+ bot:
23
+ - chat:write
24
+ - channels:history
25
+ - channels:read
26
+ - groups:history
27
+ - groups:read`;
28
+ function openBrowser(url) {
29
+ try {
30
+ const cmd = process.platform === "darwin" ? "open"
31
+ : process.platform === "win32" ? "start"
32
+ : "xdg-open";
33
+ execFileSync(cmd, [url], { stdio: "ignore" });
34
+ }
35
+ catch {
36
+ p.log.warn(`Could not open browser. Visit: ${url}`);
37
+ }
38
+ }
39
+ export async function setupSlack(opts) {
40
+ const envPath = path.join(process.cwd(), ".env");
41
+ // 1. Opt-in (only during init)
42
+ if (opts.fromInit) {
43
+ const wantSlack = await p.confirm({
44
+ message: "Would you like to connect Slack?",
45
+ });
46
+ if (isCancel(wantSlack) || !wantSlack)
47
+ return;
48
+ }
49
+ // Check for existing tokens
50
+ const existingEnv = loadEnvFile(envPath);
51
+ const hasAppToken = !!existingEnv.SLACK_APP_TOKEN;
52
+ const hasBotToken = !!existingEnv.SLACK_BOT_TOKEN;
53
+ let skipTokens = false;
54
+ if (hasAppToken && hasBotToken) {
55
+ const keep = await p.confirm({
56
+ message: "Slack tokens already exist in .env. Keep them?",
57
+ });
58
+ if (isCancel(keep))
59
+ return;
60
+ if (keep) {
61
+ skipTokens = true;
62
+ process.env.SLACK_APP_TOKEN ??= existingEnv.SLACK_APP_TOKEN;
63
+ process.env.SLACK_BOT_TOKEN ??= existingEnv.SLACK_BOT_TOKEN;
64
+ p.log.info("Using existing tokens from .env");
65
+ }
66
+ }
67
+ // 2. Instructions — Create App
68
+ if (!skipTokens) {
69
+ p.log.step("Step 1: Create Slack App");
70
+ console.log();
71
+ console.log(" Go to api.slack.com/apps → Create New App → From a manifest");
72
+ console.log();
73
+ const openPage = await p.confirm({
74
+ message: "Open api.slack.com in your browser?",
75
+ });
76
+ if (!isCancel(openPage) && openPage) {
77
+ openBrowser("https://api.slack.com/apps");
78
+ }
79
+ console.log();
80
+ console.log(" Select your workspace, then paste this manifest:");
81
+ console.log();
82
+ for (const line of SLACK_MANIFEST.split("\n")) {
83
+ console.log(` ${line}`);
84
+ }
85
+ console.log();
86
+ console.log(" Click Create.");
87
+ console.log();
88
+ // 3. Instructions — App Token
89
+ p.log.step("Step 2: Generate App-Level Token");
90
+ console.log();
91
+ console.log(" Settings → Basic Information → App-Level Tokens");
92
+ console.log(" Click 'Generate Token and Scopes'");
93
+ console.log(" Name: socket-mode | Scope: connections:write");
94
+ console.log(" Copy the xapp-... token");
95
+ console.log();
96
+ // 4. Collect App Token
97
+ const appToken = await p.password({
98
+ message: "Paste App Token (xapp-...):",
99
+ validate: (val) => {
100
+ if (!val?.trim())
101
+ return "App token is required";
102
+ if (!val.startsWith("xapp-"))
103
+ return "Token should start with xapp-";
104
+ },
105
+ });
106
+ if (isCancel(appToken))
107
+ return;
108
+ upsertEnvKey(envPath, "SLACK_APP_TOKEN", appToken.trim());
109
+ process.env.SLACK_APP_TOKEN = appToken.trim();
110
+ p.log.success("App token saved to .env");
111
+ // 5. Instructions — Bot Token
112
+ p.log.step("Step 3: Install to Workspace");
113
+ console.log();
114
+ console.log(" Settings → Install App → Install to Workspace");
115
+ console.log(" Authorize, then copy the Bot User OAuth Token (xoxb-...)");
116
+ console.log();
117
+ // 6. Collect Bot Token
118
+ const botToken = await p.password({
119
+ message: "Paste Bot Token (xoxb-...):",
120
+ validate: (val) => {
121
+ if (!val?.trim())
122
+ return "Bot token is required";
123
+ if (!val.startsWith("xoxb-"))
124
+ return "Token should start with xoxb-";
125
+ },
126
+ });
127
+ if (isCancel(botToken))
128
+ return;
129
+ upsertEnvKey(envPath, "SLACK_BOT_TOKEN", botToken.trim());
130
+ process.env.SLACK_BOT_TOKEN = botToken.trim();
131
+ p.log.success("Bot token saved to .env");
132
+ } // end if (!skipTokens)
133
+ // 7. Channel Binding
134
+ let config;
135
+ try {
136
+ config = loadConfig(opts.configPath);
137
+ }
138
+ catch (err) {
139
+ p.log.error(`Could not load config: ${err instanceof Error ? err.message : err}`);
140
+ p.log.info("Slack tokens were saved to .env. Run `agent-kit slack-setup` again after fixing your config.");
141
+ return;
142
+ }
143
+ const agentNames = Object.keys(config.agents);
144
+ if (agentNames.length === 0) {
145
+ p.log.info("No agents configured yet. Run `agent-kit create` first, then `agent-kit slack-setup` to bind channels.");
146
+ }
147
+ else {
148
+ p.log.step("Step 4: Connect agents to Slack channels");
149
+ console.log();
150
+ console.log(" Create channels in Slack, then invite the bot: /invite @Agent Kit");
151
+ console.log(" Get channel IDs: right-click channel → View channel details → copy ID (starts with C)");
152
+ console.log();
153
+ const raw = readRawConfig(opts.configPath);
154
+ if (!raw) {
155
+ p.log.error("Could not read agent-kit.json");
156
+ return;
157
+ }
158
+ const agents = raw.agents;
159
+ if (!agents) {
160
+ p.log.error("No agents section in config");
161
+ return;
162
+ }
163
+ const usedChannels = new Map(); // channelId → agentName
164
+ // Collect already-bound channels
165
+ for (const [name, def] of Object.entries(agents)) {
166
+ const slack = def.slack;
167
+ if (slack?.channelId) {
168
+ usedChannels.set(slack.channelId, name);
169
+ }
170
+ }
171
+ for (const name of agentNames) {
172
+ const agentDef = config.agents[name];
173
+ const emoji = agentDef.emoji ?? " ";
174
+ const display = agentDef.displayName ?? name;
175
+ // Skip if already bound
176
+ if (agentDef.slack) {
177
+ p.log.info(`${emoji} ${display} — already bound to ${agentDef.slack.channelName}`);
178
+ continue;
179
+ }
180
+ const bind = await p.confirm({
181
+ message: `Connect ${emoji} ${display} to a Slack channel?`,
182
+ });
183
+ if (isCancel(bind))
184
+ return;
185
+ if (!bind)
186
+ continue;
187
+ const channelId = await p.text({
188
+ message: "Channel ID (e.g. C0123456789):",
189
+ validate: (val) => {
190
+ if (!val?.trim())
191
+ return "Channel ID is required";
192
+ if (!/^[CG][A-Z0-9]{8,}$/.test(val.trim())) {
193
+ return "Channel ID should start with C or G followed by uppercase letters/numbers";
194
+ }
195
+ const existing = usedChannels.get(val.trim());
196
+ if (existing) {
197
+ return `Channel already bound to agent "${existing}"`;
198
+ }
199
+ },
200
+ });
201
+ if (isCancel(channelId))
202
+ return;
203
+ const channelName = await p.text({
204
+ message: "Channel name (e.g. #finances):",
205
+ validate: (val) => {
206
+ if (!val?.trim())
207
+ return "Channel name is required";
208
+ },
209
+ });
210
+ if (isCancel(channelName))
211
+ return;
212
+ const id = channelId.trim();
213
+ const cName = channelName.trim();
214
+ // Update raw config
215
+ agents[name] = {
216
+ ...agents[name],
217
+ slack: { channelId: id, channelName: cName },
218
+ };
219
+ usedChannels.set(id, name);
220
+ p.log.success(`${emoji} ${display} → ${cName}`);
221
+ }
222
+ try {
223
+ writeConfig(opts.configPath, raw);
224
+ }
225
+ catch (err) {
226
+ p.log.error(`Could not save config: ${err instanceof Error ? err.message : err}`);
227
+ p.log.info("Slack tokens were saved, but channel bindings were not persisted.");
228
+ return;
229
+ }
230
+ }
231
+ // 8. Done
232
+ console.log();
233
+ p.log.success("Slack configured! Start with `pnpm start` to connect.");
234
+ }