@c4t4/heyamigo 0.10.0 → 0.10.2

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/cli/setup.js CHANGED
@@ -65,6 +65,32 @@ function setConfigOwnerNumber(configPath, number) {
65
65
  }
66
66
  catch { }
67
67
  }
68
+ function readConfigObject(configPath) {
69
+ try {
70
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ function getConfiguredProvider(configPath) {
77
+ const cfg = readConfigObject(configPath);
78
+ const ai = cfg?.ai;
79
+ if (ai && typeof ai === 'object') {
80
+ const provider = ai.provider;
81
+ if (provider === 'claude' || provider === 'codex' || provider === 'grok') {
82
+ return provider;
83
+ }
84
+ }
85
+ return 'claude';
86
+ }
87
+ function setConfiguredProvider(configPath, provider) {
88
+ const cfg = readConfigObject(configPath);
89
+ if (!cfg)
90
+ return;
91
+ cfg.ai = { ...(cfg.ai ?? {}), provider };
92
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
93
+ }
68
94
  function findPackageDir() {
69
95
  // __pkgRoot = two levels up from dist/cli/ = package root
70
96
  if (existsSync(resolve(__pkgRoot, 'config', 'config.example.json'))) {
@@ -236,27 +262,55 @@ export async function runSetup() {
236
262
  else {
237
263
  p.log.info('access.json already exists');
238
264
  }
239
- // ── Claude CLI (critical — bot cannot work without this) ─────
240
- const claudePath = which('claude');
241
- if (!claudePath) {
242
- p.cancel('Claude CLI is required but was not found.\n' +
243
- 'Install it first, then re-run setup:\n\n' +
244
- ' npm install -g @anthropic-ai/claude-code\n\n' +
245
- 'For other install methods see: https://docs.anthropic.com/en/docs/claude-code');
246
- process.exit(1);
247
- }
248
- p.log.success('Claude CLI found');
249
- // Auth (critical — bot uses your Claude subscription, not API)
250
- const authenticated = run('claude auth status').ok;
251
- if (!authenticated) {
252
- p.cancel('Claude is not logged in.\n' +
253
- 'Run claude in your terminal and follow the login instructions:\n\n' +
254
- ' claude\n\n' +
255
- 'Once logged in, re-run: npx @c4t4/heyamigo setup');
256
- process.exit(1);
257
- }
258
- p.log.success('Claude authenticated');
259
- {
265
+ // ── AI provider ───────────────────────────────────────────────
266
+ const currentProvider = getConfiguredProvider(configPath);
267
+ const providerChoice = await p.select({
268
+ message: 'Choose AI provider',
269
+ options: [
270
+ {
271
+ value: 'claude',
272
+ label: 'Claude',
273
+ hint: 'Claude Code CLI',
274
+ },
275
+ {
276
+ value: 'grok',
277
+ label: 'Grok Build',
278
+ hint: 'xAI Grok Build CLI',
279
+ },
280
+ {
281
+ value: 'codex',
282
+ label: 'Codex',
283
+ hint: 'OpenAI Codex CLI',
284
+ },
285
+ ],
286
+ initialValue: currentProvider,
287
+ });
288
+ const provider = p.isCancel(providerChoice)
289
+ ? currentProvider
290
+ : providerChoice;
291
+ setConfiguredProvider(configPath, provider);
292
+ p.log.success(`AI provider: ${provider}`);
293
+ if (provider === 'claude') {
294
+ // ── Claude CLI (critical — bot cannot work without this) ─────
295
+ const claudePath = which('claude');
296
+ if (!claudePath) {
297
+ p.cancel('Claude CLI is required but was not found.\n' +
298
+ 'Install it first, then re-run setup:\n\n' +
299
+ ' npm install -g @anthropic-ai/claude-code\n\n' +
300
+ 'For other install methods see: https://docs.anthropic.com/en/docs/claude-code');
301
+ process.exit(1);
302
+ }
303
+ p.log.success('Claude CLI found');
304
+ // Auth (critical — bot uses your Claude subscription, not API)
305
+ const authenticated = run('claude auth status').ok;
306
+ if (!authenticated) {
307
+ p.cancel('Claude is not logged in.\n' +
308
+ 'Run claude in your terminal and follow the login instructions:\n\n' +
309
+ ' claude\n\n' +
310
+ 'Once logged in, re-run: npx @c4t4/heyamigo setup');
311
+ process.exit(1);
312
+ }
313
+ p.log.success('Claude authenticated');
260
314
  // Tool permissions — write .claude/settings.json in project root.
261
315
  p.log.info('Claude needs tool permissions to browse the web, read files, and control the browser. ' +
262
316
  'This writes a .claude/settings.json file in the project directory.');
@@ -315,15 +369,44 @@ export async function runSetup() {
315
369
  catch {
316
370
  // Non-critical, trust prompt will appear on first run
317
371
  }
318
- } // end grant permissions
319
- } // end claude cli block
372
+ }
373
+ }
374
+ else if (provider === 'grok') {
375
+ // ── Grok Build CLI ──────────────────────────────────────────
376
+ const grokPath = which('grok');
377
+ if (!grokPath) {
378
+ p.cancel('Grok Build CLI is required but was not found.\n' +
379
+ 'Install it first, then re-run setup:\n\n' +
380
+ ' curl -fsSL https://x.ai/cli/install.sh | bash');
381
+ process.exit(1);
382
+ }
383
+ p.log.success('Grok Build CLI found');
384
+ if (!process.env.XAI_API_KEY) {
385
+ p.log.info('If Grok is not logged in on this machine yet, run:\n\n' +
386
+ ' grok login\n\n' +
387
+ 'Headless servers can also use XAI_API_KEY.');
388
+ }
389
+ }
390
+ else {
391
+ // ── Codex CLI ───────────────────────────────────────────────
392
+ const codexPath = which('codex');
393
+ if (!codexPath) {
394
+ p.cancel('Codex CLI is required but was not found.\n' +
395
+ 'Install it first, then re-run setup:\n\n' +
396
+ ' npm install -g @openai/codex');
397
+ process.exit(1);
398
+ }
399
+ p.log.success('Codex CLI found');
400
+ p.log.info('If Codex is not logged in on this machine yet, run:\n\n' +
401
+ ' codex login');
402
+ }
320
403
  // ── Shared browser (optional) ──────────────────────────────────
321
- p.log.info('Claude can control a real Chrome browser to browse websites, ' +
404
+ p.log.info('The AI provider can control a real Chrome browser to browse websites, ' +
322
405
  'fill forms, take screenshots, and interact with web apps. ' +
323
406
  'Everything runs on localhost only, nothing is exposed publicly. ' +
324
407
  'You can connect to watch the browser via a secure SSH tunnel.');
325
408
  const wantBrowser = await p.confirm({
326
- message: 'Enable browser control for Claude?',
409
+ message: 'Enable browser control?',
327
410
  initialValue: false,
328
411
  });
329
412
  if (!p.isCancel(wantBrowser) && wantBrowser) {
@@ -332,17 +415,22 @@ export async function runSetup() {
332
415
  p.log.warning('Automated browser setup is available on Linux only. ' +
333
416
  'On macOS/Windows: start Chrome with --remote-debugging-port=9222 manually, ' +
334
417
  'then for Claude: claude mcp add playwright -- npx @playwright/mcp@latest --cdp-endpoint "http://localhost:9222"; ' +
335
- 'for Codex: add [mcp_servers.playwright] to ~/.codex/config.toml.');
418
+ 'for Codex: add [mcp_servers.playwright] to ~/.codex/config.toml; ' +
419
+ 'for Grok: use grok mcp to add the same Playwright MCP server.');
336
420
  }
337
421
  else {
338
422
  // ── Check if already running ─────────────────────────────
339
423
  const cdpUrl = 'http://localhost:9222';
340
424
  const alreadyRunning = run(`curl -s '${cdpUrl}/json/version'`);
341
- const mcpConfigured = run('claude mcp list 2>/dev/null').output.includes('playwright');
425
+ const hasClaude = !!which('claude');
426
+ const mcpConfigured = hasClaude && run('claude mcp list 2>/dev/null').output.includes('playwright');
342
427
  const hasCodex = !!which('codex');
343
- if (alreadyRunning.ok && alreadyRunning.output.includes('Browser') && mcpConfigured) {
428
+ const hasGrok = !!which('grok');
429
+ if (alreadyRunning.ok && alreadyRunning.output.includes('Browser')) {
344
430
  p.log.success('Chrome already running (localhost:9222)');
345
- p.log.success('Claude already connected to Chrome');
431
+ if (hasClaude && mcpConfigured) {
432
+ p.log.success('Claude already connected to Chrome');
433
+ }
346
434
  if (hasCodex) {
347
435
  if (addPlaywrightToCodexConfig(cdpUrl)) {
348
436
  p.log.success('Codex connected to Chrome (~/.codex/config.toml)');
@@ -351,6 +439,9 @@ export async function runSetup() {
351
439
  p.log.warning('Could not write ~/.codex/config.toml — add [mcp_servers.playwright] manually');
352
440
  }
353
441
  }
442
+ if (hasGrok) {
443
+ p.log.info('For Grok, add Playwright MCP with grok mcp if it is not already configured.');
444
+ }
354
445
  p.log.info('View browser (SSH tunnel):\n' +
355
446
  ` ssh -L 6090:127.0.0.1:6090 ${process.env.USER || 'root'}@<server-ip>\n` +
356
447
  ' Then open: http://localhost:6090/vnc.html');
@@ -426,18 +517,20 @@ export async function runSetup() {
426
517
  const cdpCheck = run(`curl -s '${cdpUrl}/json/version'`);
427
518
  if (cdpCheck.ok && cdpCheck.output.includes('Browser')) {
428
519
  p.log.success('Chrome running (localhost:9222, not public)');
429
- // Connect Claude to Chrome via CDP
430
- const sc = p.spinner();
431
- sc.start('Connecting Claude to Chrome');
432
- run('claude mcp remove playwright');
433
- const addResult = run(`claude mcp add playwright -- npx @playwright/mcp@latest --cdp-endpoint "${cdpUrl}"`);
434
- if (addResult.ok ||
435
- addResult.output.includes('already exists')) {
436
- sc.stop('Claude connected to Chrome');
437
- }
438
- else {
439
- sc.stop('Connection failed');
440
- p.log.warning('Run manually: claude mcp add playwright -- npx @playwright/mcp@latest --cdp-endpoint "http://localhost:9222"');
520
+ if (hasClaude) {
521
+ // Connect Claude to Chrome via CDP
522
+ const sc = p.spinner();
523
+ sc.start('Connecting Claude to Chrome');
524
+ run('claude mcp remove playwright');
525
+ const addResult = run(`claude mcp add playwright -- npx @playwright/mcp@latest --cdp-endpoint "${cdpUrl}"`);
526
+ if (addResult.ok ||
527
+ addResult.output.includes('already exists')) {
528
+ sc.stop('Claude connected to Chrome');
529
+ }
530
+ else {
531
+ sc.stop('Connection failed');
532
+ p.log.warning('Run manually: claude mcp add playwright -- npx @playwright/mcp@latest --cdp-endpoint "http://localhost:9222"');
533
+ }
441
534
  }
442
535
  // Mirror the MCP entry into Codex if it's installed, so the same
443
536
  // browser lane works when ai.provider is flipped to codex.
@@ -449,6 +542,9 @@ export async function runSetup() {
449
542
  p.log.warning('Could not write ~/.codex/config.toml — add [mcp_servers.playwright] manually');
450
543
  }
451
544
  }
545
+ if (hasGrok) {
546
+ p.log.info('For Grok, add Playwright MCP with grok mcp if it is not already configured.');
547
+ }
452
548
  if (vncInstalled) {
453
549
  p.log.info('Watch the browser (localhost only, via SSH tunnel):\n' +
454
550
  ` ssh -L 6090:127.0.0.1:6090 ${process.env.USER || 'root'}@<server-ip>\n` +
@@ -663,35 +759,37 @@ export async function runSetup() {
663
759
  }
664
760
  catch { }
665
761
  }
666
- // ── Claude model ─────────────────────────────────────────────
667
- const model = await p.select({
668
- message: 'Choose a Claude model',
669
- options: [
670
- {
671
- value: 'claude-opus-4-7',
672
- label: 'Opus',
673
- hint: 'highest quality, recommended (default)',
674
- },
675
- {
676
- value: 'claude-sonnet-4-6',
677
- label: 'Sonnet',
678
- hint: 'faster, lower cost',
679
- },
680
- ],
681
- initialValue: 'claude-opus-4-7',
682
- });
683
- if (!p.isCancel(model)) {
684
- const configPath = resolve(cwd, 'config/config.json');
685
- if (existsSync(configPath)) {
686
- let cfg = readFileSync(configPath, 'utf-8');
687
- cfg = cfg.replace(/"model":\s*"[^"]*"/, `"model": "${model}"`);
688
- writeFileSync(configPath, cfg);
689
- const label = model === 'claude-sonnet-4-6'
690
- ? 'Sonnet'
691
- : model === 'claude-opus-4-7'
692
- ? 'Opus'
693
- : 'Haiku';
694
- p.log.success(`Model: ${label}`);
762
+ if (provider === 'claude') {
763
+ // ── Claude model ─────────────────────────────────────────────
764
+ const model = await p.select({
765
+ message: 'Choose a Claude model',
766
+ options: [
767
+ {
768
+ value: 'claude-opus-4-7',
769
+ label: 'Opus',
770
+ hint: 'highest quality, recommended (default)',
771
+ },
772
+ {
773
+ value: 'claude-sonnet-4-6',
774
+ label: 'Sonnet',
775
+ hint: 'faster, lower cost',
776
+ },
777
+ ],
778
+ initialValue: 'claude-opus-4-7',
779
+ });
780
+ if (!p.isCancel(model)) {
781
+ const configPath = resolve(cwd, 'config/config.json');
782
+ if (existsSync(configPath)) {
783
+ let cfg = readFileSync(configPath, 'utf-8');
784
+ cfg = cfg.replace(/"model":\s*"[^"]*"/, `"model": "${model}"`);
785
+ writeFileSync(configPath, cfg);
786
+ const label = model === 'claude-sonnet-4-6'
787
+ ? 'Sonnet'
788
+ : model === 'claude-opus-4-7'
789
+ ? 'Opus'
790
+ : 'Haiku';
791
+ p.log.success(`Model: ${label}`);
792
+ }
695
793
  }
696
794
  }
697
795
  // ── Personality ──────────────────────────────────────────────
package/dist/cli/start.js CHANGED
@@ -1,13 +1,34 @@
1
- import { execSync } from 'child_process';
1
+ import { execFileSync } from 'child_process';
2
2
  import { bootBot, installShutdownSignals } from '../boot.js';
3
+ import { config } from '../config.js';
3
4
  import { logger } from '../logger.js';
5
+ function requiredCli() {
6
+ switch (config.ai.provider) {
7
+ case 'claude':
8
+ return {
9
+ bin: 'claude',
10
+ install: 'npm install -g @anthropic-ai/claude-code',
11
+ };
12
+ case 'codex':
13
+ return {
14
+ bin: 'codex',
15
+ install: 'npm install -g @openai/codex',
16
+ };
17
+ case 'grok':
18
+ return {
19
+ bin: config.grok.bin,
20
+ install: 'curl -fsSL https://x.ai/cli/install.sh | bash',
21
+ };
22
+ }
23
+ }
4
24
  export async function main() {
25
+ const cli = requiredCli();
5
26
  try {
6
- execSync('which claude', { stdio: 'pipe' });
27
+ execFileSync('which', [cli.bin], { stdio: 'pipe' });
7
28
  }
8
29
  catch {
9
- console.error('Claude CLI not found. Install it first:\n\n' +
10
- ' npm install -g @anthropic-ai/claude-code\n');
30
+ console.error(`${config.ai.provider} CLI not found. Install it first:\n\n` +
31
+ ` ${cli.install}\n`);
11
32
  process.exit(1);
12
33
  }
13
34
  installShutdownSignals();
package/dist/config.js CHANGED
@@ -4,9 +4,20 @@ import { z } from 'zod';
4
4
  const TriggerModeSchema = z.enum(['all', 'mention', 'command', 'off']);
5
5
  const ConfigSchema = z.object({
6
6
  whatsapp: z.object({
7
+ enabled: z.boolean().default(true),
7
8
  authDir: z.string(),
8
9
  browserName: z.string(),
9
10
  }),
11
+ telegram: z
12
+ .object({
13
+ enabled: z.boolean().default(false),
14
+ botToken: z.string().optional(),
15
+ pollIntervalMs: z.number().int().positive().default(1000),
16
+ })
17
+ .default({
18
+ enabled: false,
19
+ pollIntervalMs: 1000,
20
+ }),
10
21
  owner: z.object({
11
22
  number: z.string(),
12
23
  treatAsAllowedEverywhere: z.boolean(),
@@ -26,7 +37,7 @@ const ConfigSchema = z.object({
26
37
  }),
27
38
  ai: z
28
39
  .object({
29
- provider: z.enum(['claude', 'codex']).default('claude'),
40
+ provider: z.enum(['claude', 'codex', 'grok']).default('claude'),
30
41
  })
31
42
  .default({ provider: 'claude' }),
32
43
  claude: z.object({
@@ -57,6 +68,7 @@ const ConfigSchema = z.object({
57
68
  // Optional model override. If unset, Codex uses its default. Passed
58
69
  // as `-m <model>` to `codex exec`.
59
70
  model: z.string().optional(),
71
+ contextWindow: z.number().int().positive().default(200000),
60
72
  // Emits --yolo, which bundles no-approvals + full sandbox + skip-
61
73
  // trust-check. The narrower verbose flag does not subsume the trust
62
74
  // gate on all versions and hangs the process, so --yolo is the safe
@@ -72,6 +84,27 @@ const ConfigSchema = z.object({
72
84
  extraArgs: z.array(z.string()).default([]),
73
85
  })
74
86
  .default({}),
87
+ grok: z
88
+ .object({
89
+ // Binary name or absolute path. xAI's installer puts `grok` on PATH,
90
+ // but some desktop installs expose it from an app bundle.
91
+ bin: z.string().default('grok'),
92
+ // Optional model override. If unset, Grok Build uses its configured
93
+ // default. Passed as `-m <model>`.
94
+ model: z.string().optional(),
95
+ contextWindow: z.number().int().positive().default(1000000),
96
+ // Headless Grok can prompt for tool approvals. In the bot runtime there
97
+ // is no human TUI, so auto-approval is the practical default for write
98
+ // modes; read-only tasks still use plan/read-only settings.
99
+ alwaysApprove: z.boolean().default(true),
100
+ // Keep Grok's own cross-session memory out of heyamigo's explicit memory
101
+ // files unless the operator opts in.
102
+ memory: z.boolean().default(false),
103
+ // Appended verbatim to every `grok` invocation. Escape hatch for CLI
104
+ // version drift without changing code.
105
+ extraArgs: z.array(z.string()).default([]),
106
+ })
107
+ .default({}),
75
108
  bootstrap: z.object({
76
109
  historyDepth: z.number(),
77
110
  includeHistory: z.boolean(),
@@ -118,12 +151,14 @@ const ConfigSchema = z.object({
118
151
  promptRetentionDays: z.number(),
119
152
  }),
120
153
  // Threads — AI-curated relevance watchlist. See src/queue/threads.ts.
121
- // Off by default; flip enabled=true to allow the AI to open/track
122
- // loops via [THREAD-*:] tags. Reactive surface only in v1 proactive
123
- // review tick (silent-chat check-ins) deferred.
154
+ // On by default. Reactive surface only in v1: the agent decides
155
+ // when to open loops, brings them up if naturally relevant, never
156
+ // sends unsolicited messages. To turn off, set enabled=false in
157
+ // config.local.json. Proactive review tick (silent-chat check-ins)
158
+ // is the bit that would be default-off if/when it ships.
124
159
  threads: z
125
160
  .object({
126
- enabled: z.boolean().default(false),
161
+ enabled: z.boolean().default(true),
127
162
  preamblePerChat: z.number().int().positive().default(5),
128
163
  // Soft caps used by future cleanup jobs; the worker doesn't read
129
164
  // these yet but they're here so config.json can be authored once.
@@ -132,7 +167,7 @@ const ConfigSchema = z.object({
132
167
  decayPerDay: z.number().int().min(0).default(2),
133
168
  })
134
169
  .default({
135
- enabled: false,
170
+ enabled: true,
136
171
  preamblePerChat: 5,
137
172
  maxActivePerChat: 10,
138
173
  hotnessCapOnCreate: 70,
@@ -53,6 +53,19 @@ export function addressToExternalId(addr) {
53
53
  const a = typeof addr === 'string' ? parseAddress(addr) : addr;
54
54
  return a.externalId;
55
55
  }
56
+ export function addressToChatKey(addr) {
57
+ const a = typeof addr === 'string' ? parseAddress(addr) : addr;
58
+ if (a.channel === 'wa')
59
+ return a.externalId;
60
+ return `${a.channel}_${a.scope}_${a.externalId}`.replace(/[^a-zA-Z0-9_.-]/g, '_');
61
+ }
62
+ export function actorKeyFromAddress(addr) {
63
+ const a = typeof addr === 'string' ? parseAddress(addr) : addr;
64
+ if (a.channel === 'wa') {
65
+ return a.externalId.split('@')[0]?.split(':')[0] ?? a.externalId;
66
+ }
67
+ return `${a.channel}_${a.externalId}`.replace(/[^a-zA-Z0-9_.-]/g, '_');
68
+ }
56
69
  // Convenience predicates.
57
70
  export function isGroup(addr) {
58
71
  const a = typeof addr === 'string' ? parseAddress(addr) : addr;
@@ -24,11 +24,16 @@ import { identities, persons } from './schema.js';
24
24
  import { getAccess } from '../wa/whitelist.js';
25
25
  const OWNER_PERSON_ID = 'person-owner';
26
26
  function personIdForNumber(number) {
27
+ if (number.startsWith('tg_'))
28
+ return `person-${number}`;
27
29
  // Strip non-digits, prefix with 'person-'. Stable + deterministic.
28
30
  const sanitized = number.replace(/\D/g, '');
29
31
  return `person-${sanitized}`;
30
32
  }
31
33
  function dmAddressFor(number) {
34
+ if (number.startsWith('tg_')) {
35
+ return `tg:dm:${number.slice(3)}`;
36
+ }
32
37
  const sanitized = number.replace(/\D/g, '');
33
38
  return formatAddress(jidToAddress(`${sanitized}@s.whatsapp.net`));
34
39
  }
@@ -171,6 +176,9 @@ export function getTimezoneForAddress(address) {
171
176
  export function getTimezoneForSenderNumber(senderNumber) {
172
177
  if (!senderNumber)
173
178
  return config.owner.timezone;
179
+ if (senderNumber.startsWith('tg_')) {
180
+ return getTimezoneForAddress(`tg:dm:${senderNumber.slice(3)}`);
181
+ }
174
182
  const sanitized = senderNumber.replace(/\D/g, '');
175
183
  if (!sanitized)
176
184
  return config.owner.timezone;
@@ -1,35 +1,28 @@
1
- import { isJidGroup } from 'baileys';
2
1
  import { config } from '../config.js';
3
- import { logger } from '../logger.js';
4
2
  import { readLast } from '../store/messages.js';
5
3
  export async function buildInitPayload(params) {
6
- const { jid, sock, userText, userNumber } = params;
7
- const isGroup = isJidGroup(jid) === true;
4
+ const { jid, userText, userNumber } = params;
5
+ const chat = params.chat ?? {
6
+ platform: 'WhatsApp',
7
+ isGroup: jid.endsWith('@g.us'),
8
+ externalId: jid,
9
+ };
8
10
  const lines = [];
9
11
  if (config.bootstrap.includeChatMetadata) {
10
- lines.push('You are the assistant behind a WhatsApp chat.');
11
- if (isGroup) {
12
- let subject = 'unknown';
13
- let participantSummary = '';
14
- try {
15
- const meta = await sock.groupMetadata(jid);
16
- subject = meta.subject || subject;
17
- if (meta.participants?.length) {
18
- participantSummary = `${meta.participants.length} participants`;
19
- }
20
- }
21
- catch (err) {
22
- logger.warn({ err, jid }, 'group metadata fetch failed in bootstrap');
23
- }
12
+ lines.push(`You are the assistant behind a ${chat.platform} chat.`);
13
+ if (chat.isGroup) {
24
14
  lines.push(`Chat type: group`);
25
- lines.push(`Chat name: "${subject}"`);
26
- if (participantSummary)
27
- lines.push(`Members: ${participantSummary}`);
15
+ lines.push(`Chat name: "${chat.chatName || 'unknown'}"`);
16
+ if (chat.memberSummary)
17
+ lines.push(`Members: ${chat.memberSummary}`);
28
18
  }
29
19
  else {
30
20
  lines.push(`Chat type: direct message`);
31
21
  }
32
- lines.push(`JID: ${jid}`);
22
+ lines.push(`Chat key: ${jid}`);
23
+ if (chat.externalId && chat.externalId !== jid) {
24
+ lines.push(`External id: ${chat.externalId}`);
25
+ }
33
26
  lines.push('');
34
27
  }
35
28
  if (config.bootstrap.includeHistory) {
@@ -2,7 +2,6 @@ import { clearSession, getSessionInfo } from '../ai/sessions.js';
2
2
  import { getProvider, reloadAllSystemPrompts } from '../ai/providers.js';
3
3
  import { config } from '../config.js';
4
4
  import { runDigestNow } from '../memory/scheduler.js';
5
- import { sendText } from '../wa/sender.js';
6
5
  // Feature-level commands (/journal, /snooze, /tasks, etc.) are intentionally
7
6
  // absent. Claude is the interface — the owner asks for things in natural
8
7
  // language and Claude acts via markers or by editing files directly.
@@ -24,18 +23,19 @@ export async function tryCommand(ctx) {
24
23
  const reply = existed
25
24
  ? `Session reset. Next message will bootstrap a fresh ${provider.name} session.`
26
25
  : 'No session to reset.';
27
- await sendText(ctx.sock, ctx.jid, reply, ctx.quoted);
26
+ await ctx.reply(reply);
28
27
  return true;
29
28
  }
30
29
  if (config.commands.status.includes(cmd)) {
31
- const info = getSessionInfo(ctx.jid, getProvider().name);
30
+ const provider = getProvider();
31
+ const info = getSessionInfo(ctx.jid, provider.name);
32
32
  if (!info) {
33
- await sendText(ctx.sock, ctx.jid, 'No session yet. Next message will bootstrap one.', ctx.quoted);
33
+ await ctx.reply('No session yet. Next message will bootstrap one.');
34
34
  return true;
35
35
  }
36
36
  const lines = [`Session: ${info.sessionId.slice(0, 8)}…`];
37
37
  if (info.usage) {
38
- const max = config.claude.contextWindow;
38
+ const max = provider.contextWindow;
39
39
  const used = info.usage.totalContextTokens;
40
40
  // Clamp leftPct to [0, 100] so stale or inconsistent data
41
41
  // doesn't surface a negative or >100 percentage.
@@ -44,7 +44,7 @@ export async function tryCommand(ctx) {
44
44
  lines.push(`Context: ${used.toLocaleString()} / ${max.toLocaleString()} (${leftPct}% left, last turn)`);
45
45
  lines.push(`Turns: ${info.usage.numTurns}`);
46
46
  }
47
- await sendText(ctx.sock, ctx.jid, lines.join('\n'), ctx.quoted);
47
+ await ctx.reply(lines.join('\n'));
48
48
  return true;
49
49
  }
50
50
  if (config.commands.reload.includes(cmd)) {
@@ -53,11 +53,11 @@ export async function tryCommand(ctx) {
53
53
  const reply = existed
54
54
  ? 'Personality reloaded and session reset.'
55
55
  : 'Personality reloaded.';
56
- await sendText(ctx.sock, ctx.jid, reply, ctx.quoted);
56
+ await ctx.reply(reply);
57
57
  return true;
58
58
  }
59
59
  if (cmd === 'digest') {
60
- await sendText(ctx.sock, ctx.jid, 'Digesting memory now, this may take a moment.', ctx.quoted);
60
+ await ctx.reply('Digesting memory now, this may take a moment.');
61
61
  runDigestNow({
62
62
  jid: ctx.jid,
63
63
  number: ctx.senderNumber || undefined,
@@ -68,27 +68,25 @@ export async function tryCommand(ctx) {
68
68
  if (cmd === 'queues') {
69
69
  const { takeQueuesSnapshot, formatQueuesSnapshot } = await import('../queue/observability.js');
70
70
  const snap = takeQueuesSnapshot();
71
- await sendText(ctx.sock, ctx.jid, formatQueuesSnapshot(snap), ctx.quoted);
71
+ await ctx.reply(formatQueuesSnapshot(snap));
72
72
  return true;
73
73
  }
74
74
  if (cmd === 'reminders' || cmd === 'crons') {
75
75
  const { listChatSchedules, formatScheduleList } = await import('../queue/schedule-list.js');
76
- const { formatAddress, jidToAddress } = await import('../db/address.js');
77
76
  const { getTimezoneForSenderNumber } = await import('../db/identity-sync.js');
78
- const chatAddress = formatAddress(jidToAddress(ctx.jid));
79
77
  const tz = getTimezoneForSenderNumber(ctx.senderNumber);
80
78
  const onlyKind = cmd === 'reminders' ? 'one-shot' : 'recurring';
81
- const items = listChatSchedules(chatAddress, onlyKind);
82
- await sendText(ctx.sock, ctx.jid, formatScheduleList(items, tz, onlyKind), ctx.quoted);
79
+ const items = listChatSchedules(ctx.address, onlyKind);
80
+ await ctx.reply(formatScheduleList(items, tz, onlyKind));
83
81
  return true;
84
82
  }
85
83
  if (cmd === 'threads') {
86
84
  if (!config.threads?.enabled) {
87
- await sendText(ctx.sock, ctx.jid, 'threads are disabled in config. Set `threads.enabled: true` to turn on.', ctx.quoted);
85
+ await ctx.reply('threads are disabled in config. Set `threads.enabled: true` to turn on.');
88
86
  return true;
89
87
  }
90
88
  const { handleThreadsCommand } = await import('../queue/thread-list.js');
91
- await sendText(ctx.sock, ctx.jid, handleThreadsCommand(ctx.jid, args), ctx.quoted);
89
+ await ctx.reply(handleThreadsCommand(ctx.jid, args));
92
90
  return true;
93
91
  }
94
92
  return false;