@c4t4/heyamigo 0.8.0 → 0.8.1

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.
package/dist/ai/claude.js CHANGED
@@ -3,7 +3,7 @@ import { resolve } from 'path';
3
3
  import { config } from '../config.js';
4
4
  import { logger } from '../logger.js';
5
5
  import { logPrompt } from '../promptlog.js';
6
- import { runClaude, TIMEOUT_MS } from './spawn.js';
6
+ import { parseStreamJson, runClaude, TIMEOUT_MS } from './spawn.js';
7
7
  let cachedSystemPrompt = null;
8
8
  function systemPrompt() {
9
9
  if (cachedSystemPrompt !== null)
@@ -25,10 +25,14 @@ export function reloadSystemPrompt() {
25
25
  cachedSystemPrompt = null;
26
26
  }
27
27
  function buildArgs(params) {
28
+ // stream-json gives per-event visibility into the agent loop (system init,
29
+ // assistant messages, tool_use, tool_result, final result). We parse the
30
+ // final 'result' event for the return shape, and log event types for
31
+ // diagnostic purposes.
28
32
  const args = [
29
33
  '-p',
30
34
  '--output-format',
31
- config.claude.outputFormat,
35
+ 'stream-json',
32
36
  '--model',
33
37
  config.claude.model,
34
38
  '--permission-mode',
@@ -57,35 +61,32 @@ function buildArgs(params) {
57
61
  export async function askClaude(params) {
58
62
  const args = buildArgs(params);
59
63
  logger.debug({ resume: !!params.sessionId, inputChars: params.input.length }, 'spawning claude');
60
- const { stdout, durationMs } = await runClaude({
64
+ const { stdout, stderr, durationMs } = await runClaude({
61
65
  args,
62
66
  input: params.input,
63
67
  timeoutMs: TIMEOUT_MS.main,
64
68
  caller: 'worker',
65
69
  });
66
70
  const startedAt = Date.now() - durationMs;
67
- let parsed;
68
- try {
69
- parsed = JSON.parse(stdout);
70
- }
71
- catch (err) {
72
- throw new Error(`failed to parse claude output: ${err.message}\nstdout: ${stdout.slice(0, 500)}`);
71
+ const parsed = parseStreamJson(stdout);
72
+ if (!parsed) {
73
+ throw new Error(`claude stream-json produced no result event; stdout: ${stdout.slice(0, 500)}`);
73
74
  }
74
- if (parsed.is_error || parsed.subtype !== 'success') {
75
- throw new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result ?? ''}`);
75
+ if (parsed.isError || parsed.subtype !== 'success') {
76
+ throw new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result}`);
76
77
  }
77
- if (!parsed.result || !parsed.session_id) {
78
+ if (!parsed.result || !parsed.sessionId) {
78
79
  throw new Error(`claude output missing result or session_id: ${stdout.slice(0, 200)}`);
79
80
  }
80
81
  const result = {
81
82
  reply: parsed.result,
82
- sessionId: parsed.session_id,
83
+ sessionId: parsed.sessionId,
83
84
  usage: {
84
85
  inputTokens: parsed.usage?.input_tokens ?? 0,
85
86
  cacheReadTokens: parsed.usage?.cache_read_input_tokens ?? 0,
86
87
  cacheCreationTokens: parsed.usage?.cache_creation_input_tokens ?? 0,
87
88
  outputTokens: parsed.usage?.output_tokens ?? 0,
88
- numTurns: parsed.num_turns ?? 0,
89
+ numTurns: parsed.numTurns ?? 0,
89
90
  },
90
91
  };
91
92
  void logPrompt({
@@ -97,6 +98,8 @@ export async function askClaude(params) {
97
98
  sessionId: result.sessionId,
98
99
  usage: result.usage,
99
100
  durationMs,
101
+ stderr,
102
+ eventTypes: parsed.eventTypes,
100
103
  });
101
104
  return result;
102
105
  }
package/dist/ai/spawn.js CHANGED
@@ -21,6 +21,60 @@ export class ClaudeSpawnError extends Error {
21
21
  this.name = 'ClaudeSpawnError';
22
22
  }
23
23
  }
24
+ // Parse Claude CLI's --output-format stream-json output. Each line is a JSON
25
+ // event; the final event with type === 'result' carries the completion
26
+ // summary (same shape as the old single-json output format). Returns null if
27
+ // no result event is found — caller should treat that as an error.
28
+ export function parseStreamJson(stdout) {
29
+ const events = [];
30
+ const eventTypes = [];
31
+ const lines = stdout.split(/\r?\n/);
32
+ for (const line of lines) {
33
+ const trimmed = line.trim();
34
+ if (!trimmed)
35
+ continue;
36
+ try {
37
+ const parsed = JSON.parse(trimmed);
38
+ events.push(parsed);
39
+ if (typeof parsed.type === 'string')
40
+ eventTypes.push(parsed.type);
41
+ }
42
+ catch {
43
+ // Ignore malformed lines — Claude CLI occasionally emits preamble or
44
+ // debug lines that aren't JSON; the structured events we need are
45
+ // always well-formed.
46
+ }
47
+ }
48
+ // Find the final result event. Walk from end to handle any stray events
49
+ // after 'result' (shouldn't happen but be defensive).
50
+ let resultEvent = null;
51
+ for (let i = events.length - 1; i >= 0; i--) {
52
+ if (events[i].type === 'result') {
53
+ resultEvent = events[i];
54
+ break;
55
+ }
56
+ }
57
+ if (!resultEvent)
58
+ return null;
59
+ return {
60
+ result: typeof resultEvent.result === 'string' ? resultEvent.result : '',
61
+ sessionId: typeof resultEvent.session_id === 'string'
62
+ ? resultEvent.session_id
63
+ : null,
64
+ usage: resultEvent.usage && typeof resultEvent.usage === 'object'
65
+ ? resultEvent.usage
66
+ : undefined,
67
+ isError: !!resultEvent.is_error,
68
+ subtype: typeof resultEvent.subtype === 'string'
69
+ ? resultEvent.subtype
70
+ : undefined,
71
+ numTurns: typeof resultEvent.num_turns === 'number'
72
+ ? resultEvent.num_turns
73
+ : undefined,
74
+ eventTypes,
75
+ events,
76
+ };
77
+ }
24
78
  // Kill the process group of a detached child. Playwright MCP and any Chromium
25
79
  // children sit under the claude subprocess; without process-group kill they
26
80
  // linger after we SIGTERM the parent and accumulate on the host.
@@ -54,6 +108,15 @@ export async function runClaude(opts) {
54
108
  // detached:true puts the child in its own process group, so killGroup
55
109
  // can SIGTERM the whole tree (Playwright MCP, Chromium, etc.) at once.
56
110
  detached: true,
111
+ // ANTHROPIC_LOG=debug surfaces the SDK's HTTP layer to stderr:
112
+ // request URLs, status codes, retries, rate-limit notices. We
113
+ // capture stderr and put a truncated copy into the promptlog so
114
+ // we can diagnose API hangs/rate-limits post-mortem instead of
115
+ // staring at "Claude subprocess is idle, why?".
116
+ env: {
117
+ ...process.env,
118
+ ANTHROPIC_LOG: process.env.ANTHROPIC_LOG ?? 'debug',
119
+ },
57
120
  });
58
121
  let stdout = '';
59
122
  let stderr = '';
@@ -106,7 +169,7 @@ export async function runClaude(opts) {
106
169
  logFail(`exit ${code}: ${stderr.slice(0, 500)}`);
107
170
  return rejectPromise(new ClaudeSpawnError(caller, `claude exited with code ${code}: ${stderr.slice(0, 500)}`));
108
171
  }
109
- resolvePromise({ stdout, durationMs });
172
+ resolvePromise({ stdout, stderr, durationMs });
110
173
  });
111
174
  child.stdin.write(input);
112
175
  child.stdin.end();
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, writeFileSync, } from 'fs';
2
2
  import { dirname, resolve } from 'path';
3
3
  import { mkdirSync } from 'fs';
4
- import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
4
+ import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
5
5
  import { config } from '../config.js';
6
6
  import { logger } from '../logger.js';
7
7
  import { logPrompt } from '../promptlog.js';
@@ -224,28 +224,25 @@ async function spawnGenerator(prompt) {
224
224
  const args = [
225
225
  '-p',
226
226
  '--output-format',
227
- 'json',
227
+ 'stream-json',
228
228
  '--model',
229
229
  config.claude.model,
230
230
  '--permission-mode',
231
231
  'acceptEdits',
232
232
  ];
233
- const { stdout, durationMs } = await runClaude({
233
+ const { stdout, stderr, durationMs } = await runClaude({
234
234
  args,
235
235
  input: prompt,
236
236
  timeoutMs: TIMEOUT_MS.background,
237
237
  caller: 'compressed',
238
238
  });
239
239
  const startedAt = Date.now() - durationMs;
240
- let parsed;
241
- try {
242
- parsed = JSON.parse(stdout);
243
- }
244
- catch (err) {
245
- throw new Error(`compressed parse failed: ${err.message}`);
240
+ const parsed = parseStreamJson(stdout);
241
+ if (!parsed) {
242
+ throw new Error(`compressed stream-json produced no result event: ${stdout.slice(0, 200)}`);
246
243
  }
247
- if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
248
- throw new Error(`compressed bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
244
+ if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
245
+ throw new Error(`compressed bad output: ${parsed.result || stdout.slice(0, 200)}`);
249
246
  }
250
247
  const output = parsed.result.trim();
251
248
  void logPrompt({
@@ -255,6 +252,8 @@ async function spawnGenerator(prompt) {
255
252
  input: prompt,
256
253
  output,
257
254
  durationMs,
255
+ stderr,
256
+ eventTypes: parsed.eventTypes,
258
257
  });
259
258
  return output;
260
259
  }
@@ -1,4 +1,4 @@
1
- import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
1
+ import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
2
2
  import { config } from '../config.js';
3
3
  import { logger } from '../logger.js';
4
4
  import { logPrompt } from '../promptlog.js';
@@ -13,28 +13,25 @@ async function spawnDigester(prompt) {
13
13
  const args = [
14
14
  '-p',
15
15
  '--output-format',
16
- 'json',
16
+ 'stream-json',
17
17
  '--model',
18
18
  config.claude.model,
19
19
  '--permission-mode',
20
20
  'acceptEdits',
21
21
  ];
22
- const { stdout, durationMs } = await runClaude({
22
+ const { stdout, stderr, durationMs } = await runClaude({
23
23
  args,
24
24
  input: prompt,
25
25
  timeoutMs: TIMEOUT_MS.background,
26
26
  caller: 'digester',
27
27
  });
28
28
  const startedAt = Date.now() - durationMs;
29
- let parsed;
30
- try {
31
- parsed = JSON.parse(stdout);
32
- }
33
- catch (err) {
34
- throw new Error(`digester parse failed: ${err.message}`);
29
+ const parsed = parseStreamJson(stdout);
30
+ if (!parsed) {
31
+ throw new Error(`digester stream-json produced no result event: ${stdout.slice(0, 200)}`);
35
32
  }
36
- if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
37
- throw new Error(`digester bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
33
+ if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
34
+ throw new Error(`digester bad output: ${parsed.result || stdout.slice(0, 200)}`);
38
35
  }
39
36
  const output = parsed.result.trim();
40
37
  void logPrompt({
@@ -44,6 +41,8 @@ async function spawnDigester(prompt) {
44
41
  input: prompt,
45
42
  output,
46
43
  durationMs,
44
+ stderr,
45
+ eventTypes: parsed.eventTypes,
47
46
  });
48
47
  return output;
49
48
  }
@@ -1,4 +1,4 @@
1
- import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
1
+ import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
2
2
  import { config } from '../config.js';
3
3
  import { initiate } from '../gateway/outgoing.js';
4
4
  import { logger } from '../logger.js';
@@ -18,28 +18,25 @@ async function spawnComposer(prompt) {
18
18
  const args = [
19
19
  '-p',
20
20
  '--output-format',
21
- 'json',
21
+ 'stream-json',
22
22
  '--model',
23
23
  config.claude.model,
24
24
  '--permission-mode',
25
25
  'acceptEdits',
26
26
  ];
27
- const { stdout, durationMs } = await runClaude({
27
+ const { stdout, stderr, durationMs } = await runClaude({
28
28
  args,
29
29
  input: prompt,
30
30
  timeoutMs: TIMEOUT_MS.background,
31
31
  caller: 'journal-nudger',
32
32
  });
33
33
  const startedAt = Date.now() - durationMs;
34
- let parsed;
35
- try {
36
- parsed = JSON.parse(stdout);
37
- }
38
- catch (err) {
39
- throw new Error(`nudger parse failed: ${err.message}`);
34
+ const parsed = parseStreamJson(stdout);
35
+ if (!parsed) {
36
+ throw new Error(`nudger stream-json produced no result event: ${stdout.slice(0, 200)}`);
40
37
  }
41
- if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
42
- throw new Error(`nudger bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
38
+ if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
39
+ throw new Error(`nudger bad output: ${parsed.result || stdout.slice(0, 200)}`);
43
40
  }
44
41
  const output = parsed.result.trim();
45
42
  void logPrompt({
@@ -49,6 +46,8 @@ async function spawnComposer(prompt) {
49
46
  input: prompt,
50
47
  output,
51
48
  durationMs,
49
+ stderr,
50
+ eventTypes: parsed.eventTypes,
52
51
  });
53
52
  return output;
54
53
  }
@@ -1,4 +1,4 @@
1
- import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
1
+ import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
2
2
  import { config } from '../config.js';
3
3
  import { logger } from '../logger.js';
4
4
  import { logPrompt } from '../promptlog.js';
@@ -14,28 +14,25 @@ async function spawnObserver(prompt) {
14
14
  const args = [
15
15
  '-p',
16
16
  '--output-format',
17
- 'json',
17
+ 'stream-json',
18
18
  '--model',
19
19
  config.claude.model,
20
20
  '--permission-mode',
21
21
  'acceptEdits',
22
22
  ];
23
- const { stdout, durationMs } = await runClaude({
23
+ const { stdout, stderr, durationMs } = await runClaude({
24
24
  args,
25
25
  input: prompt,
26
26
  timeoutMs: TIMEOUT_MS.background,
27
27
  caller: 'journal-observer',
28
28
  });
29
29
  const startedAt = Date.now() - durationMs;
30
- let parsed;
31
- try {
32
- parsed = JSON.parse(stdout);
33
- }
34
- catch (err) {
35
- throw new Error(`journal observer parse failed: ${err.message}`);
30
+ const parsed = parseStreamJson(stdout);
31
+ if (!parsed) {
32
+ throw new Error(`journal observer stream-json produced no result event: ${stdout.slice(0, 200)}`);
36
33
  }
37
- if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
38
- throw new Error(`journal observer bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
34
+ if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
35
+ throw new Error(`journal observer bad output: ${parsed.result || stdout.slice(0, 200)}`);
39
36
  }
40
37
  const output = parsed.result.trim();
41
38
  void logPrompt({
@@ -45,6 +42,8 @@ async function spawnObserver(prompt) {
45
42
  input: prompt,
46
43
  output,
47
44
  durationMs,
45
+ stderr,
46
+ eventTypes: parsed.eventTypes,
48
47
  });
49
48
  return output;
50
49
  }
package/dist/promptlog.js CHANGED
@@ -1,6 +1,21 @@
1
1
  import { appendFile, mkdir, readdir, unlink } from 'fs/promises';
2
2
  import { resolve } from 'path';
3
3
  import { config } from './config.js';
4
+ // Hard caps on fields that can grow unbounded. Prevents promptlog entries
5
+ // from exploding when a run produces huge stdout/stderr. prunePrompts
6
+ // still handles multi-day retention separately.
7
+ const STDOUT_MAX_BYTES = 100_000;
8
+ const STDERR_MAX_BYTES = 50_000;
9
+ const INPUT_MAX_BYTES = 200_000;
10
+ function truncateWithMarker(s, maxBytes) {
11
+ if (!s)
12
+ return s;
13
+ // Rough byte size via length — fine for mostly-ASCII prompt payloads.
14
+ if (s.length <= maxBytes)
15
+ return s;
16
+ const extra = s.length - maxBytes;
17
+ return s.slice(0, maxBytes) + `\n… [truncated ${extra} bytes]`;
18
+ }
4
19
  let dirReady = false;
5
20
  function promptsDir() {
6
21
  return resolve(process.cwd(), 'storage/prompts');
@@ -18,7 +33,17 @@ function logFilePath() {
18
33
  export async function logPrompt(entry) {
19
34
  try {
20
35
  await ensureDir();
21
- await appendFile(logFilePath(), JSON.stringify(entry) + '\n', 'utf-8');
36
+ const capped = {
37
+ ...entry,
38
+ input: truncateWithMarker(entry.input, INPUT_MAX_BYTES),
39
+ };
40
+ if (entry.output !== undefined) {
41
+ capped.output = truncateWithMarker(entry.output, STDOUT_MAX_BYTES);
42
+ }
43
+ if (entry.stderr !== undefined) {
44
+ capped.stderr = truncateWithMarker(entry.stderr, STDERR_MAX_BYTES);
45
+ }
46
+ await appendFile(logFilePath(), JSON.stringify(capped) + '\n', 'utf-8');
22
47
  }
23
48
  catch {
24
49
  // logging must never break the main flow
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { dirname, resolve } from 'path';
3
- import { runClaude, TIMEOUT_MS } from '../ai/spawn.js';
3
+ import { parseStreamJson, runClaude, TIMEOUT_MS } from '../ai/spawn.js';
4
4
  import { config } from '../config.js';
5
5
  import fastq from 'fastq';
6
6
  import { initiate } from '../gateway/outgoing.js';
@@ -113,7 +113,7 @@ function buildArgs(task) {
113
113
  const args = [
114
114
  '-p',
115
115
  '--output-format',
116
- 'json',
116
+ 'stream-json',
117
117
  '--model',
118
118
  config.claude.model,
119
119
  '--permission-mode',
@@ -135,22 +135,19 @@ function buildArgs(task) {
135
135
  }
136
136
  async function spawnClaudeForTask(task, prompt) {
137
137
  const args = buildArgs(task);
138
- const { stdout, durationMs } = await runClaude({
138
+ const { stdout, stderr, durationMs } = await runClaude({
139
139
  args,
140
140
  input: prompt,
141
141
  timeoutMs: TIMEOUT_MS.async,
142
142
  caller: 'async-task',
143
143
  });
144
144
  const startedAt = Date.now() - durationMs;
145
- let parsed;
146
- try {
147
- parsed = JSON.parse(stdout);
148
- }
149
- catch (err) {
150
- throw new Error(`async task parse failed: ${err.message}`);
145
+ const parsed = parseStreamJson(stdout);
146
+ if (!parsed) {
147
+ throw new Error(`async task stream-json produced no result event: ${stdout.slice(0, 200)}`);
151
148
  }
152
- if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
153
- throw new Error(`async task bad output: ${parsed.result ?? stdout.slice(0, 200)}`);
149
+ if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
150
+ throw new Error(`async task bad output: ${parsed.result || stdout.slice(0, 200)}`);
154
151
  }
155
152
  const output = parsed.result.trim();
156
153
  void logPrompt({
@@ -160,6 +157,8 @@ async function spawnClaudeForTask(task, prompt) {
160
157
  input: prompt,
161
158
  output,
162
159
  durationMs,
160
+ stderr,
161
+ eventTypes: parsed.eventTypes,
163
162
  });
164
163
  return output;
165
164
  }
@@ -398,7 +397,7 @@ function buildBrowserArgs(task, sessionId) {
398
397
  const args = [
399
398
  '-p',
400
399
  '--output-format',
401
- 'json',
400
+ 'stream-json',
402
401
  '--model',
403
402
  config.claude.model,
404
403
  '--permission-mode',
@@ -435,6 +434,7 @@ async function runBrowserTask(task) {
435
434
  const startedAtMs = Date.now();
436
435
  const elapsedLog = () => `${Math.round((Date.now() - task.startedAt * 1000) / 1000)}s`;
437
436
  let stdout;
437
+ let stderr;
438
438
  let durationMs;
439
439
  try {
440
440
  const result = await runClaude({
@@ -444,6 +444,7 @@ async function runBrowserTask(task) {
444
444
  caller: 'browser-task',
445
445
  });
446
446
  stdout = result.stdout;
447
+ stderr = result.stderr;
447
448
  durationMs = result.durationMs;
448
449
  }
449
450
  catch (err) {
@@ -454,20 +455,17 @@ async function runBrowserTask(task) {
454
455
  });
455
456
  return;
456
457
  }
457
- let parsed;
458
- try {
459
- parsed = JSON.parse(stdout);
460
- }
461
- catch (err) {
462
- logger.error({ err, id: task.id }, 'browser task: failed to parse claude output');
458
+ const parsed = parseStreamJson(stdout);
459
+ if (!parsed) {
460
+ logger.error({ id: task.id }, 'browser task stream-json produced no result event');
463
461
  await initiate({
464
462
  jid: task.jid,
465
463
  text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an unparseable response.`,
466
464
  });
467
465
  return;
468
466
  }
469
- if (parsed.is_error || parsed.subtype !== 'success' || !parsed.result) {
470
- logger.error({ parsed, id: task.id }, 'browser task bad output');
467
+ if (parsed.isError || parsed.subtype !== 'success' || !parsed.result) {
468
+ logger.error({ id: task.id, subtype: parsed.subtype, isError: parsed.isError }, 'browser task bad output');
471
469
  await initiate({
472
470
  jid: task.jid,
473
471
  text: `Heads up: the browser task "${truncate(task.description, 80)}" returned an error.`,
@@ -476,7 +474,7 @@ async function runBrowserTask(task) {
476
474
  }
477
475
  // Persist the session id. On first call Claude returns the new sessionId;
478
476
  // on resume it may return the same or a rotated one.
479
- const returnedSessionId = parsed.session_id ?? null;
477
+ const returnedSessionId = parsed.sessionId;
480
478
  if (returnedSessionId) {
481
479
  const now = Math.floor(Date.now() / 1000);
482
480
  saveBrowserSession({
@@ -494,6 +492,8 @@ async function runBrowserTask(task) {
494
492
  output: parsed.result,
495
493
  sessionId: returnedSessionId ?? undefined,
496
494
  durationMs,
495
+ stderr,
496
+ eventTypes: parsed.eventTypes,
497
497
  });
498
498
  // Route markers the same way the general async lane does.
499
499
  const { extractFlags } = await import('../memory/digest-flag.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "WhatsApp AI bot powered by Claude with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",