granclaw 0.0.1-beta.0 → 0.0.1-beta.3

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.
Binary file
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ exports.main = main;
8
8
  /* eslint-disable no-console */
9
9
  const child_process_1 = require("child_process");
10
10
  const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
11
12
  const path_1 = __importDefault(require("path"));
12
13
  const home_js_1 = require("./home.js");
13
14
  // package.json is resolved at runtime; require() avoids rootDir complaints
@@ -15,6 +16,54 @@ const home_js_1 = require("./home.js");
15
16
  // eslint-disable-next-line @typescript-eslint/no-var-requires
16
17
  const pkg = require('../package.json');
17
18
  const DEFAULT_PORT = 8787;
19
+ // ANSI colour helpers — no external deps
20
+ const c = {
21
+ reset: '\x1b[0m',
22
+ bold: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ cyan: '\x1b[36m',
25
+ magenta: '\x1b[35m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ white: '\x1b[97m',
29
+ gray: '\x1b[90m',
30
+ };
31
+ function col(code, text) {
32
+ return `${code}${text}${c.reset}`;
33
+ }
34
+ function printBanner(version, port, homeDir) {
35
+ const art = [
36
+ ' ____ ____ _ ',
37
+ ' / ___|_ __ __ _ _ __ / ___| | __ ___ __',
38
+ " | | _| '__/ _` | '_ \\| | | |/ _` \\ \\ /\\ / /",
39
+ ' | |_| | | | (_| | | | | |___| | (_| |\\ V V / ',
40
+ ' \\____|_| \\__,_|_| |_|\\____|_|\\__,_| \\_/\\_/ ',
41
+ ];
42
+ console.log('');
43
+ for (const line of art) {
44
+ console.log(col(c.magenta + c.bold, line));
45
+ }
46
+ console.log('');
47
+ console.log(col(c.gray, ' Multi-agent AI framework') +
48
+ col(c.gray, ' · ') +
49
+ col(c.dim, `v${version}`));
50
+ console.log('');
51
+ console.log(col(c.cyan, ' Dashboard ') + col(c.white + c.bold, `http://localhost:${port}`));
52
+ console.log(col(c.cyan, ' Home ') + col(c.white, homeDir));
53
+ console.log(col(c.cyan, ' Workspaces ') + col(c.white, path_1.default.join(homeDir, 'workspaces')));
54
+ console.log(col(c.cyan, ' Config ') + col(c.white, path_1.default.join(homeDir, 'agents.config.json')));
55
+ console.log(col(c.cyan, ' Logs ') + col(c.white, path_1.default.join(homeDir, 'data')));
56
+ console.log('');
57
+ console.log(col(c.green, ' Opening browser…'));
58
+ console.log('');
59
+ }
60
+ function openBrowser(url) {
61
+ const platform = os_1.default.platform();
62
+ const cmd = platform === 'darwin' ? `open "${url}"` :
63
+ platform === 'win32' ? `start "" "${url}"` :
64
+ `xdg-open "${url}"`;
65
+ (0, child_process_1.exec)(cmd, () => { });
66
+ }
18
67
  /**
19
68
  * Parse the CLI argv slice (sans `node` and the script path).
20
69
  *
@@ -94,6 +143,91 @@ Install from https://claude.ai/download, then rerun.
94
143
  process.exit(1);
95
144
  }
96
145
  }
146
+ /**
147
+ * Return the path to a real system Chrome/Chromium install, or null if none
148
+ * is found. Deliberately excludes ~/.agent-browser/browsers/ (Chrome for
149
+ * Testing) — GranClaw requires a real browser installed on the system.
150
+ *
151
+ * Respects AGENT_BROWSER_EXECUTABLE_PATH for users who want a custom binary.
152
+ */
153
+ function detectSystemChrome() {
154
+ const override = process.env.AGENT_BROWSER_EXECUTABLE_PATH;
155
+ if (override)
156
+ return fs_1.default.existsSync(override) ? override : null;
157
+ const platform = os_1.default.platform();
158
+ if (platform === 'darwin') {
159
+ const candidates = [
160
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
161
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
162
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
163
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
164
+ path_1.default.join(os_1.default.homedir(), 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome'),
165
+ ];
166
+ return candidates.find((p) => fs_1.default.existsSync(p)) ?? null;
167
+ }
168
+ if (platform === 'linux') {
169
+ const candidates = [
170
+ '/usr/bin/google-chrome',
171
+ '/usr/bin/google-chrome-stable',
172
+ '/usr/bin/chromium',
173
+ '/usr/bin/chromium-browser',
174
+ '/snap/bin/chromium',
175
+ '/usr/bin/brave-browser',
176
+ '/usr/bin/microsoft-edge',
177
+ ];
178
+ return candidates.find((p) => fs_1.default.existsSync(p)) ?? null;
179
+ }
180
+ if (platform === 'win32') {
181
+ const local = process.env.LOCALAPPDATA ?? '';
182
+ const pf = process.env.ProgramFiles ?? 'C:\\Program Files';
183
+ const pf86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
184
+ const candidates = [
185
+ path_1.default.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe'),
186
+ path_1.default.join(pf, 'Google', 'Chrome', 'Application', 'chrome.exe'),
187
+ path_1.default.join(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
188
+ path_1.default.join(local, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
189
+ path_1.default.join(pf, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
190
+ path_1.default.join(local, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
191
+ path_1.default.join(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
192
+ ];
193
+ return candidates.find((p) => fs_1.default.existsSync(p)) ?? null;
194
+ }
195
+ return null;
196
+ }
197
+ function requireAgentBrowser() {
198
+ try {
199
+ (0, child_process_1.execSync)('agent-browser --version', { stdio: 'ignore' });
200
+ }
201
+ catch {
202
+ console.error(`
203
+ error: agent-browser not found.
204
+
205
+ GranClaw requires \`agent-browser\` for browser automation.
206
+
207
+ npm install -g agent-browser
208
+ `);
209
+ process.exit(1);
210
+ }
211
+ const chrome = detectSystemChrome();
212
+ if (!chrome) {
213
+ const platform = os_1.default.platform();
214
+ const installLink = 'https://google.com/chrome';
215
+ const linuxExtra = platform === 'linux'
216
+ ? '\n sudo apt install google-chrome-stable # Debian/Ubuntu'
217
+ : '';
218
+ console.error(`
219
+ error: Chrome not found.
220
+
221
+ GranClaw requires Google Chrome (or Chromium / Brave / Edge) installed on
222
+ your system. Install it from:
223
+
224
+ ${installLink}${linuxExtra}
225
+
226
+ To use a custom binary, set AGENT_BROWSER_EXECUTABLE_PATH before running.
227
+ `);
228
+ process.exit(1);
229
+ }
230
+ }
97
231
  function cliPackageDir() {
98
232
  // dist/index.js runs at <cli-pkg>/dist/, so the package root is one up.
99
233
  return path_1.default.resolve(__dirname, '..');
@@ -103,6 +237,7 @@ function startServer(parsed) {
103
237
  const templatesDir = path_1.default.join(cliPackageDir(), 'templates');
104
238
  const staticDir = path_1.default.join(cliPackageDir(), 'dist', 'frontend');
105
239
  requireClaudeCli();
240
+ requireAgentBrowser();
106
241
  (0, home_js_1.seedHomeIfNeeded)(homeDir, templatesDir);
107
242
  const port = parsed.port ?? (Number(process.env.PORT) || DEFAULT_PORT);
108
243
  const env = {
@@ -112,11 +247,10 @@ function startServer(parsed) {
112
247
  GRANCLAW_STATIC_DIR: staticDir,
113
248
  PORT: String(port),
114
249
  };
115
- console.log(`GranClaw ${pkg.version}`);
116
- console.log(`Data: ${homeDir}`);
117
- console.log(`Port: ${port}`);
118
- console.log(`Ready: http://localhost:${port}`);
119
- console.log('');
250
+ printBanner(pkg.version, port, homeDir);
251
+ // Open the browser shortly after the server process starts.
252
+ // 1.5 s is enough for the backend to bind its port on most machines.
253
+ setTimeout(() => openBrowser(`http://localhost:${port}`), 1500);
120
254
  // Spawn the compiled backend entrypoint. Bundled at dist/backend/index.js
121
255
  // by the CLI build script (see scripts/build.js).
122
256
  const backendEntry = path_1.default.join(cliPackageDir(), 'dist', 'backend', 'index.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granclaw",
3
- "version": "0.0.1-beta.0",
3
+ "version": "0.0.1-beta.3",
4
4
  "description": "A personal AI assistant you run on your own machine.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,334 +0,0 @@
1
- "use strict";
2
- /**
3
- * agent/runner.ts
4
- *
5
- * Spawns the Claude Code CLI as a child process for a given agent.
6
- * Streams output back via a callback. Persists session IDs so conversations
7
- * continue across messages.
8
- *
9
- * Onboarding: if CLAUDE.md is missing from the workspace, the runner copies
10
- * templates/CLAUDE.onboarding.md there. Claude reads it, decides to onboard
11
- * (or not), and replaces the file itself when done. The host never checks
12
- * onboarding state — Claude controls it entirely.
13
- *
14
- * Claude CLI invocation:
15
- * claude -p "<message>" --output-format stream-json --verbose [--resume <sessionId>]
16
- */
17
- var __importDefault = (this && this.__importDefault) || function (mod) {
18
- return (mod && mod.__esModule) ? mod : { "default": mod };
19
- };
20
- Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.spawnEnv = exports.claudeBin = void 0;
22
- exports.resolveTemplatesDir = resolveTemplatesDir;
23
- exports.stopAgent = stopAgent;
24
- exports.bootstrapWorkspace = bootstrapWorkspace;
25
- exports.runAgent = runAgent;
26
- const child_process_1 = require("child_process");
27
- const path_1 = __importDefault(require("path"));
28
- const fs_1 = __importDefault(require("fs"));
29
- const config_js_1 = require("../config.js");
30
- const agent_db_js_1 = require("../agent-db.js");
31
- const logs_db_js_1 = require("../logs-db.js");
32
- const messages_db_js_1 = require("../messages-db.js");
33
- const secrets_vault_js_1 = require("../secrets-vault.js");
34
- const schedules_db_js_1 = require("../schedules-db.js");
35
- const cron_parser_1 = require("cron-parser");
36
- // ── Claude binary resolution ──────────────────────────────────────────────────
37
- // Ensure ~/.local/bin is in PATH for child processes (where the claude CLI lives).
38
- exports.claudeBin = process.env.CLAUDE_BIN ?? 'claude';
39
- exports.spawnEnv = {
40
- ...process.env,
41
- PATH: [
42
- path_1.default.join(process.env.HOME ?? '', '.local', 'bin'),
43
- path_1.default.join(process.env.HOME ?? '', '.nvm', 'versions', 'node', process.version, 'bin'),
44
- process.env.PATH ?? '',
45
- ].filter(Boolean).join(':'),
46
- };
47
- /**
48
- * Resolve the templates directory.
49
- *
50
- * Priority:
51
- * 1. GRANCLAW_TEMPLATES_DIR env var — set by the CLI entrypoint to the
52
- * templates dir bundled inside the published package.
53
- * 2. <GRANCLAW_HOME>/packages/cli/templates — dev-mode fallback when the
54
- * root dev script does not set the env var.
55
- *
56
- * Note: the env var is read on every call (not captured at module load)
57
- * so the CLI entrypoint can set it just before requiring the backend.
58
- * The fallback path closes over REPO_ROOT, which is a load-time snapshot
59
- * of GRANCLAW_HOME — once the process has started, the fallback is stable.
60
- */
61
- function resolveTemplatesDir() {
62
- const envDir = process.env.GRANCLAW_TEMPLATES_DIR?.trim();
63
- if (envDir) {
64
- return path_1.default.resolve(envDir);
65
- }
66
- return path_1.default.resolve(config_js_1.REPO_ROOT, 'packages/cli/templates');
67
- }
68
- // Track active Claude processes so they can be killed on stop
69
- const activeProcesses = new Map();
70
- function stopAgent(agentId) {
71
- const proc = activeProcesses.get(agentId);
72
- if (proc) {
73
- try {
74
- proc.kill('SIGTERM');
75
- }
76
- catch { /* already dead */ }
77
- activeProcesses.delete(agentId);
78
- return true;
79
- }
80
- return false;
81
- }
82
- // ── Workspace bootstrap ───────────────────────────────────────────────────────
83
- function bootstrapWorkspace(workspaceDir) {
84
- fs_1.default.mkdirSync(workspaceDir, { recursive: true });
85
- const claudeMd = path_1.default.join(workspaceDir, 'CLAUDE.md');
86
- if (!fs_1.default.existsSync(claudeMd)) {
87
- const template = path_1.default.join(resolveTemplatesDir(), 'CLAUDE.onboarding.md');
88
- fs_1.default.copyFileSync(template, claudeMd);
89
- console.log(`[runner] copied onboarding CLAUDE.md to ${workspaceDir}`);
90
- }
91
- // Ensure agent has its own .mcp.json to prevent inheriting project-root MCP servers.
92
- // Claude CLI walks up the directory tree to discover .mcp.json — without this,
93
- // agents inherit whatever MCP servers the host developer has configured.
94
- const mcpJson = path_1.default.join(workspaceDir, '.mcp.json');
95
- if (!fs_1.default.existsSync(mcpJson)) {
96
- fs_1.default.writeFileSync(mcpJson, JSON.stringify({ mcpServers: {} }, null, 2));
97
- console.log(`[runner] created empty .mcp.json in ${workspaceDir}`);
98
- }
99
- // Bootstrap vault directory structure (second brain)
100
- const vaultDir = path_1.default.join(workspaceDir, 'vault');
101
- if (!fs_1.default.existsSync(vaultDir)) {
102
- for (const sub of ['journal', 'sessions', 'actions', 'topics', 'knowledge']) {
103
- fs_1.default.mkdirSync(path_1.default.join(vaultDir, sub), { recursive: true });
104
- }
105
- console.log(`[runner] created vault structure in ${vaultDir}`);
106
- }
107
- // Bootstrap skills from templates
108
- const skillsTemplateDir = path_1.default.join(resolveTemplatesDir(), 'skills');
109
- if (fs_1.default.existsSync(skillsTemplateDir)) {
110
- const targetSkillsDir = path_1.default.join(workspaceDir, '.claude', 'skills');
111
- for (const skillName of fs_1.default.readdirSync(skillsTemplateDir)) {
112
- const srcDir = path_1.default.join(skillsTemplateDir, skillName);
113
- const destDir = path_1.default.join(targetSkillsDir, skillName);
114
- if (!fs_1.default.statSync(srcDir).isDirectory())
115
- continue;
116
- if (fs_1.default.existsSync(destDir))
117
- continue; // don't overwrite existing
118
- fs_1.default.mkdirSync(destDir, { recursive: true });
119
- for (const file of fs_1.default.readdirSync(srcDir)) {
120
- fs_1.default.copyFileSync(path_1.default.join(srcDir, file), path_1.default.join(destDir, file));
121
- }
122
- // Make shell scripts executable
123
- for (const file of fs_1.default.readdirSync(destDir)) {
124
- if (file.endsWith('.sh')) {
125
- fs_1.default.chmodSync(path_1.default.join(destDir, file), 0o755);
126
- }
127
- }
128
- console.log(`[runner] bootstrapped skill "${skillName}" to ${destDir}`);
129
- }
130
- }
131
- // Bootstrap default vault housekeeping schedule
132
- const agentId = path_1.default.basename(workspaceDir);
133
- try {
134
- const existing = (0, schedules_db_js_1.listSchedules)(agentId);
135
- const hasHousekeeping = existing.some(s => s.name === 'Vault housekeeping');
136
- if (!hasHousekeeping) {
137
- const cron = '30 23 * * *';
138
- const nextRun = (0, cron_parser_1.parseExpression)(cron, { tz: 'Asia/Singapore' }).next().getTime();
139
- (0, schedules_db_js_1.createSchedule)(agentId, {
140
- name: 'Vault housekeeping',
141
- message: 'Run vault housekeeping: scan all vault folders, rebuild every index.md with one-line summaries for each file, update vault/index.md with folder counts and recent activity. Check for orphaned wikilinks and entities that need topic notes. Never delete files.',
142
- cron,
143
- timezone: 'Asia/Singapore',
144
- nextRun,
145
- });
146
- console.log(`[runner] created default vault housekeeping schedule for ${agentId}`);
147
- }
148
- }
149
- catch { /* schedules DB may not be ready yet for this agent */ }
150
- }
151
- function extractAgentName(workspaceDir) {
152
- // After onboarding, Claude writes a real CLAUDE.md with the agent name as H1
153
- const claudeMd = path_1.default.join(workspaceDir, 'CLAUDE.md');
154
- if (!fs_1.default.existsSync(claudeMd))
155
- return null;
156
- const content = fs_1.default.readFileSync(claudeMd, 'utf8');
157
- const match = content.match(/^#\s+(.+)/m);
158
- return match ? match[1].trim() : null;
159
- }
160
- // ── Runner ────────────────────────────────────────────────────────────────────
161
- async function runAgent(agent, message, onChunk, options) {
162
- const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
163
- const channelId = options?.channelId ?? 'ui';
164
- bootstrapWorkspace(workspaceDir);
165
- // Track soul state before this turn so we know if onboarding just completed
166
- const soulExistedBefore = fs_1.default.existsSync(path_1.default.join(workspaceDir, 'SOUL.md'));
167
- // If SOUL.md doesn't exist, this is a fresh agent — prepend onboarding nudge
168
- // so Claude doesn't give a generic reply and actually follows CLAUDE.md
169
- let finalMessage = message;
170
- if (!soulExistedBefore) {
171
- finalMessage = `[SYSTEM: You are a brand new agent with no identity yet. SOUL.md does not exist. You MUST follow the onboarding instructions in your CLAUDE.md before doing anything else. Do NOT give a generic greeting. Start the onboarding process immediately.]\n\nUser message: ${message}`;
172
- }
173
- const sessionId = (0, agent_db_js_1.getSession)(workspaceDir, agent.id, channelId);
174
- // Always inject recent history from ALL channels so the agent has full context
175
- // (UI chat, workflow results — everything the agent said or received)
176
- if (soulExistedBefore && !finalMessage.includes('--- Recent History ---')) {
177
- const recentMessages = (0, messages_db_js_1.getAllRecentMessages)(agent.id, 50)
178
- .filter(m => m.role !== 'tool_call')
179
- .slice(-20);
180
- if (recentMessages.length > 0) {
181
- const history = recentMessages
182
- .map(m => {
183
- const ch = m.channelId !== 'ui' ? ` [${m.channelId}]` : '';
184
- return `[${m.role}${ch}]: ${m.content.slice(0, 500)}`;
185
- })
186
- .join('\n\n');
187
- finalMessage = `[SYSTEM: Here is recent activity across all channels for context.]\n\n--- Recent History ---\n${history}\n--- End History ---\n\nUser: ${message}`;
188
- }
189
- }
190
- const startedAt = Date.now();
191
- (0, logs_db_js_1.logAction)(agent.id, 'message', { text: message });
192
- const attempt = (resume) => new Promise((resolve, reject) => {
193
- const args = ['-p', finalMessage, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions'];
194
- if (resume)
195
- args.push('--resume', resume);
196
- // Inject core system instructions that every agent must follow (vault, security, skills)
197
- const systemMd = path_1.default.join(resolveTemplatesDir(), 'DO_NOT_DELETE.md');
198
- if (fs_1.default.existsSync(systemMd)) {
199
- args.push('--append-system-prompt-file', systemMd);
200
- }
201
- // Use --strict-mcp-config to prevent Claude CLI from inheriting MCP servers
202
- // from parent directories (e.g., project-root .mcp.json with Playwright).
203
- // Only MCP servers explicitly in --mcp-config are loaded.
204
- const agentMcpConfig = path_1.default.join(workspaceDir, 'tools.mcp.json');
205
- const workspaceMcpConfig = path_1.default.join(workspaceDir, '.mcp.json');
206
- if (fs_1.default.existsSync(agentMcpConfig)) {
207
- args.push('--mcp-config', agentMcpConfig, '--strict-mcp-config');
208
- console.log(`[agent:${agent.id}] loading MCP tools from ${agentMcpConfig} (strict)`);
209
- }
210
- else if (fs_1.default.existsSync(workspaceMcpConfig)) {
211
- args.push('--mcp-config', workspaceMcpConfig, '--strict-mcp-config');
212
- }
213
- // Inject secrets so they're available regardless of calling process
214
- // (agent process has them in env, but orchestrator/workflow runner does not)
215
- const agentSecrets = (0, secrets_vault_js_1.getSecrets)(agent.id);
216
- const agentEnv = { ...exports.spawnEnv, ...agentSecrets };
217
- // CRITICAL: never pass Anthropic auth env vars to the Claude CLI.
218
- // Claude Code must use the user's subscription (OAuth), not API mode.
219
- // If these are set, the CLI switches to API mode and fails auth.
220
- delete agentEnv.ANTHROPIC_API_KEY;
221
- delete agentEnv.ANTHROPIC_AUTH_TOKEN;
222
- delete agentEnv.ANTHROPIC_BASE_URL;
223
- delete agentEnv.CLAUDE_API_KEY;
224
- // Browser: session directory + persistent profile (so agent-browser always uses saved logins)
225
- agentEnv.AGENT_BROWSER_SESSIONS_DIR = path_1.default.join(workspaceDir, '.browser-sessions');
226
- const profileDir = path_1.default.join(workspaceDir, '.browser-profile');
227
- if (fs_1.default.existsSync(profileDir)) {
228
- agentEnv.AGENT_BROWSER_PROFILE = profileDir;
229
- }
230
- const proc = (0, child_process_1.spawn)(exports.claudeBin, args, { cwd: workspaceDir, env: agentEnv, stdio: ['pipe', 'pipe', 'pipe'] });
231
- proc.stdin?.end();
232
- activeProcesses.set(agent.id, proc);
233
- proc.on('exit', () => { activeProcesses.delete(agent.id); });
234
- let buffer = '';
235
- let newSessionId = resume ?? '';
236
- proc.stdout.on('data', (raw) => {
237
- buffer += raw.toString();
238
- const lines = buffer.split('\n');
239
- buffer = lines.pop() ?? '';
240
- for (const line of lines) {
241
- const trimmed = line.trim();
242
- if (!trimmed)
243
- continue;
244
- let parsed;
245
- try {
246
- parsed = JSON.parse(trimmed);
247
- }
248
- catch {
249
- onChunk({ type: 'text', text: trimmed });
250
- continue;
251
- }
252
- handleClaudeEvent(parsed, onChunk, (id) => { newSessionId = id; }, agent.id);
253
- }
254
- });
255
- proc.stderr.on('data', (raw) => {
256
- const msg = raw.toString().trim();
257
- if (msg)
258
- console.error(`[agent:${agent.id}] stderr:`, msg);
259
- });
260
- proc.on('close', async (code) => {
261
- if (code === 0 || code === null) {
262
- if (newSessionId) {
263
- (0, agent_db_js_1.saveSession)(workspaceDir, agent.id, newSessionId, channelId);
264
- }
265
- // If SOUL.md was created this turn, Claude finished onboarding — announce name
266
- if (!soulExistedBefore && fs_1.default.existsSync(path_1.default.join(workspaceDir, 'SOUL.md'))) {
267
- const name = extractAgentName(workspaceDir);
268
- if (name) {
269
- console.log(`[agent:${agent.id}] onboarding complete — name: "${name}"`);
270
- onChunk({ type: 'agent_ready', name });
271
- }
272
- }
273
- onChunk({ type: 'done', sessionId: newSessionId });
274
- (0, logs_db_js_1.logAction)(agent.id, 'system', null, { exitCode: code }, Date.now() - startedAt);
275
- resolve();
276
- }
277
- else {
278
- (0, logs_db_js_1.logAction)(agent.id, 'system', null, { exitCode: code }, Date.now() - startedAt);
279
- reject(new Error(`claude exited with code ${code}`));
280
- }
281
- });
282
- });
283
- try {
284
- await attempt(sessionId);
285
- }
286
- catch (err) {
287
- if (sessionId) {
288
- console.warn(`[agent:${agent.id}] session ${sessionId} rejected, retrying fresh`);
289
- (0, agent_db_js_1.saveSession)(workspaceDir, agent.id, '', channelId);
290
- try {
291
- await attempt(null);
292
- }
293
- catch (retryErr) {
294
- onChunk({ type: 'error', message: retryErr instanceof Error ? retryErr.message : String(retryErr) });
295
- (0, logs_db_js_1.logAction)(agent.id, 'error', null, { message: retryErr instanceof Error ? retryErr.message : String(retryErr) });
296
- }
297
- }
298
- else {
299
- onChunk({ type: 'error', message: err instanceof Error ? err.message : String(err) });
300
- (0, logs_db_js_1.logAction)(agent.id, 'error', null, { message: err instanceof Error ? err.message : String(err) });
301
- }
302
- }
303
- }
304
- function handleClaudeEvent(event, onChunk, onSessionId, agentId) {
305
- const type = event.type;
306
- if (type === 'assistant') {
307
- const msg = event.message;
308
- if (msg?.content) {
309
- for (const block of msg.content) {
310
- if (block.type === 'text' && block.text) {
311
- onChunk({ type: 'text', text: block.text });
312
- }
313
- else if (block.type === 'tool_use') {
314
- const b = block;
315
- onChunk({ type: 'tool_call', tool: b.name ?? 'unknown', input: b.input });
316
- if (agentId)
317
- (0, logs_db_js_1.logAction)(agentId, 'tool_call', { tool: b.name, input: b.input });
318
- }
319
- }
320
- }
321
- }
322
- else if (type === 'tool_result') {
323
- const content = event.content;
324
- const text = content?.find((c) => c.type === 'text')?.text ?? '';
325
- onChunk({ type: 'tool_result', tool: '', output: text });
326
- if (agentId)
327
- (0, logs_db_js_1.logAction)(agentId, 'tool_result', null, { text: text.slice(0, 500) });
328
- }
329
- else if (type === 'result') {
330
- const sid = event.session_id;
331
- if (sid)
332
- onSessionId(sid);
333
- }
334
- }
@@ -1,304 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <title>Chat History — Architecture Options</title>
6
- <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
7
- <style>
8
- :root {
9
- --bg: #0d0d0d;
10
- --surface: #161616;
11
- --card: #1e1e1e;
12
- --border: #2a2a2a;
13
- --text: #e8e8e8;
14
- --muted: #888;
15
- --accent-a: #7c3aed;
16
- --accent-b: #0ea5e9;
17
- --accent-c: #10b981;
18
- --warn: #f59e0b;
19
- }
20
-
21
- * { box-sizing: border-box; margin: 0; padding: 0; }
22
-
23
- body {
24
- background: var(--bg);
25
- color: var(--text);
26
- font-family: 'Inter', system-ui, sans-serif;
27
- padding: 48px 32px;
28
- }
29
-
30
- h1 {
31
- font-size: 1.5rem;
32
- font-weight: 700;
33
- margin-bottom: 8px;
34
- letter-spacing: -0.02em;
35
- }
36
-
37
- .subtitle {
38
- color: var(--muted);
39
- font-size: 0.875rem;
40
- margin-bottom: 48px;
41
- }
42
-
43
- .options {
44
- display: grid;
45
- grid-template-columns: repeat(3, 1fr);
46
- gap: 24px;
47
- margin-bottom: 48px;
48
- }
49
-
50
- .option {
51
- background: var(--card);
52
- border: 1px solid var(--border);
53
- border-radius: 12px;
54
- overflow: hidden;
55
- }
56
-
57
- .option-header {
58
- padding: 16px 20px 12px;
59
- border-bottom: 1px solid var(--border);
60
- }
61
-
62
- .option-label {
63
- font-size: 0.7rem;
64
- font-weight: 600;
65
- letter-spacing: 0.1em;
66
- text-transform: uppercase;
67
- margin-bottom: 4px;
68
- }
69
-
70
- .option-a .option-label { color: var(--accent-a); }
71
- .option-b .option-label { color: var(--accent-b); }
72
- .option-c .option-label { color: var(--accent-c); }
73
-
74
- .option-title {
75
- font-size: 1rem;
76
- font-weight: 600;
77
- }
78
-
79
- .diagram {
80
- padding: 20px;
81
- }
82
-
83
- /* Hand-drawn style diagrams with SVG */
84
- .diagram svg {
85
- width: 100%;
86
- height: auto;
87
- }
88
-
89
- .tradeoffs {
90
- padding: 16px 20px;
91
- border-top: 1px solid var(--border);
92
- display: flex;
93
- flex-direction: column;
94
- gap: 8px;
95
- }
96
-
97
- .tradeoff {
98
- display: flex;
99
- align-items: flex-start;
100
- gap: 8px;
101
- font-size: 0.78rem;
102
- line-height: 1.4;
103
- }
104
-
105
- .tradeoff .icon {
106
- flex-shrink: 0;
107
- margin-top: 1px;
108
- }
109
-
110
- .pro { color: var(--accent-c); }
111
- .con { color: #f87171; }
112
-
113
- .recommendation {
114
- background: var(--card);
115
- border: 1px solid var(--accent-c);
116
- border-radius: 12px;
117
- padding: 24px;
118
- }
119
-
120
- .recommendation h2 {
121
- font-size: 0.7rem;
122
- font-weight: 600;
123
- letter-spacing: 0.1em;
124
- text-transform: uppercase;
125
- color: var(--accent-c);
126
- margin-bottom: 8px;
127
- }
128
-
129
- .recommendation h3 {
130
- font-size: 1.1rem;
131
- font-weight: 600;
132
- margin-bottom: 16px;
133
- }
134
-
135
- .rec-diagram {
136
- background: var(--surface);
137
- border-radius: 8px;
138
- padding: 20px;
139
- }
140
-
141
- /* Mermaid theme overrides */
142
- .mermaid {
143
- background: transparent !important;
144
- }
145
- </style>
146
- </head>
147
- <body>
148
-
149
- <h1>Chat History — Architecture Options</h1>
150
- <p class="subtitle">How should agent-brother store messages so agents remember across channels and the UI can display history?</p>
151
-
152
- <div class="options">
153
-
154
- <!-- OPTION A -->
155
- <div class="option option-a">
156
- <div class="option-header">
157
- <div class="option-label">Option A</div>
158
- <div class="option-title">Session per conversation</div>
159
- </div>
160
- <div class="diagram">
161
- <div class="mermaid">
162
- %%{init: {'theme': 'dark', 'themeVariables': {'background': '#1e1e1e', 'primaryColor': '#7c3aed', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#7c3aed', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#1e1e1e', 'edgeLabelBackground': '#1e1e1e', 'fontSize': '12px'}}}%%
163
- flowchart TD
164
- WA["📱 WhatsApp"] -->|msg + channel:wa| B
165
- LI["💼 LinkedIn"] -->|msg + channel:li| B
166
- UI["🖥 Dashboard UI"] -->|msg + channel:ui| B
167
-
168
- B["Backend\nOrchestrator"]
169
-
170
- B -->|resume session_wa| AG
171
- B -->|resume session_li| AG
172
- B -->|resume session_ui| AG
173
-
174
- AG["🤖 Agent\nRunner"]
175
- AG -->|spawns| CL["claude CLI\n--resume &lt;session_id&gt;"]
176
-
177
- AG -->|stores| DB[("SQLite\nsession_wa\nsession_li\nsession_ui")]
178
-
179
- B -->|log msg| MG[("MongoDB\nmessages")]
180
-
181
- MG -->|history| UI
182
- </div>
183
- </div>
184
- <div class="tradeoffs">
185
- <div class="tradeoff pro"><span class="icon">✓</span><span>Each channel has its own Claude context window — no cross-talk</span></div>
186
- <div class="tradeoff pro"><span class="icon">✓</span><span>Fast — no extra tokens injected per request</span></div>
187
- <div class="tradeoff con"><span class="icon">✗</span><span>Claude sessions expire — agent loses memory cold on restart</span></div>
188
- <div class="tradeoff con"><span class="icon">✗</span><span>Can't move a conversation between channels</span></div>
189
- </div>
190
- </div>
191
-
192
- <!-- OPTION B -->
193
- <div class="option option-b">
194
- <div class="option-header">
195
- <div class="option-label">Option B</div>
196
- <div class="option-title">Inject history, no sessions</div>
197
- </div>
198
- <div class="diagram">
199
- <div class="mermaid">
200
- %%{init: {'theme': 'dark', 'themeVariables': {'background': '#1e1e1e', 'primaryColor': '#0ea5e9', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#0ea5e9', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#1e1e1e', 'edgeLabelBackground': '#1e1e1e', 'fontSize': '12px'}}}%%
201
- flowchart TD
202
- WA["📱 WhatsApp"] -->|msg| B
203
- LI["💼 LinkedIn"] -->|msg| B
204
- UI["🖥 Dashboard UI"] -->|msg| B
205
-
206
- B["Backend\nOrchestrator"]
207
-
208
- B -->|fetch last N msgs| MG[("MongoDB\nmessages")]
209
- MG -->|history| B
210
-
211
- B -->|prompt = history + msg| AG["🤖 Agent\nRunner"]
212
- AG -->|spawns fresh| CL["claude CLI\n-p 'prev: ...\nuser: ...'"]
213
-
214
- AG -->|save reply| MG
215
-
216
- MG -->|display| UI
217
- </div>
218
- </div>
219
- <div class="tradeoffs">
220
- <div class="tradeoff pro"><span class="icon">✓</span><span>Full control over what agent sees — no session expiry surprises</span></div>
221
- <div class="tradeoff pro"><span class="icon">✓</span><span>Conversation can move freely across channels</span></div>
222
- <div class="tradeoff con"><span class="icon">✗</span><span>Extra tokens on every request — slower and more expensive</span></div>
223
- <div class="tradeoff con"><span class="icon">✗</span><span>Context window fills up on long conversations</span></div>
224
- </div>
225
- </div>
226
-
227
- <!-- OPTION C -->
228
- <div class="option option-c">
229
- <div class="option-header">
230
- <div class="option-label">Option C — Recommended</div>
231
- <div class="option-title">Hybrid: resume + fallback inject</div>
232
- </div>
233
- <div class="diagram">
234
- <div class="mermaid">
235
- %%{init: {'theme': 'dark', 'themeVariables': {'background': '#1e1e1e', 'primaryColor': '#10b981', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#10b981', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#1e1e1e', 'edgeLabelBackground': '#1e1e1e', 'fontSize': '12px'}}}%%
236
- flowchart TD
237
- WA["📱 WhatsApp"] --> B
238
- LI["💼 LinkedIn"] --> B
239
- UI["🖥 Dashboard UI"] --> B
240
-
241
- B["Backend\nOrchestrator"]
242
- B -->|save msg| MG[("MongoDB\nmessages")]
243
- B -->|run| AG["🤖 Agent\nRunner"]
244
-
245
- AG -->|happy path| CL1["claude CLI\n--resume session_id"]
246
- CL1 -->|exit 1 / expired| FB["⚠ Session expired\nfallback"]
247
-
248
- FB -->|fetch last 20 msgs| MG
249
- MG --> FB
250
- FB -->|inject as context| CL2["claude CLI\nfresh + history"]
251
-
252
- CL1 -->|reply| B
253
- CL2 -->|reply + new session_id| B
254
-
255
- B -->|save reply| MG
256
- MG -->|display| UI
257
- </div>
258
- </div>
259
- <div class="tradeoffs">
260
- <div class="tradeoff pro"><span class="icon">✓</span><span>Fast on happy path (--resume, no extra tokens)</span></div>
261
- <div class="tradeoff pro"><span class="icon">✓</span><span>Survives session expiry — injects history automatically</span></div>
262
- <div class="tradeoff pro"><span class="icon">✓</span><span>Full message log in MongoDB for UI + cross-channel queries</span></div>
263
- <div class="tradeoff con"><span class="icon">✗</span><span>Slightly more complex runner logic (already partially there)</span></div>
264
- </div>
265
- </div>
266
-
267
- </div>
268
-
269
- <!-- DATA MODEL -->
270
- <div class="recommendation">
271
- <h2>Recommended data model — Option C</h2>
272
- <h3>MongoDB <code>messages</code> collection + SQLite <code>agent_sessions</code> per (agent, channel, conversation)</h3>
273
- <div class="rec-diagram">
274
- <div class="mermaid">
275
- %%{init: {'theme': 'dark', 'themeVariables': {'background': '#161616', 'primaryColor': '#10b981', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#10b981', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#161616', 'edgeLabelBackground': '#161616', 'fontSize': '13px'}}}%%
276
- erDiagram
277
- MESSAGE {
278
- string id PK
279
- string agentId
280
- string channelId "ui | whatsapp | linkedin"
281
- string conversationId "groups a thread"
282
- string role "user | assistant"
283
- string content
284
- number createdAt
285
- }
286
-
287
- AGENT_SESSION {
288
- string agentId PK
289
- string channelId PK
290
- string conversationId PK
291
- string sessionId "Claude resume token"
292
- number updatedAt
293
- }
294
-
295
- MESSAGE }o--|| AGENT_SESSION : "linked by agentId+channelId+conversationId"
296
- </div>
297
- </div>
298
- </div>
299
-
300
- <script>
301
- mermaid.initialize({ startOnLoad: true, theme: 'dark' });
302
- </script>
303
- </body>
304
- </html>