@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/README.md +40 -248
- package/config/access.example.json +12 -2
- package/config/config.example.json +16 -0
- package/config/memory-instructions.md +1 -1
- package/config/personalities/casual.md +1 -1
- package/config/personalities/professional.md +1 -1
- package/config/personalities/sharp.md +2 -2
- package/dist/ai/claude.js +1 -0
- package/dist/ai/codex.js +1 -0
- package/dist/ai/grok.js +310 -0
- package/dist/ai/provider.js +5 -5
- package/dist/ai/providers.js +2 -0
- package/dist/ai/sessions.js +5 -1
- package/dist/boot.js +15 -6
- package/dist/channels/index.js +2 -1
- package/dist/channels/runtime.js +1 -0
- package/dist/channels/telegram.js +393 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/setup.js +168 -70
- package/dist/cli/start.js +25 -4
- package/dist/config.js +41 -6
- package/dist/db/address.js +13 -0
- package/dist/db/identity-sync.js +8 -0
- package/dist/gateway/bootstrap.js +15 -22
- package/dist/gateway/commands.js +13 -15
- package/dist/gateway/incoming.js +107 -254
- package/dist/gateway/ingest.js +240 -0
- package/dist/gateway/outgoing.js +3 -5
- package/dist/gateway/triggers.js +7 -40
- package/dist/memory/digest.js +5 -5
- package/dist/queue/async-tasks.js +11 -4
- package/dist/queue/browser-worker.js +5 -3
- package/dist/queue/cron-dispatch.js +6 -7
- package/dist/queue/job-address.js +4 -0
- package/dist/queue/outbound-postsend.js +4 -7
- package/dist/queue/worker.js +11 -5
- package/dist/wa/whitelist.js +40 -5
- package/package.json +3 -2
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
|
-
// ──
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
}
|
|
319
|
-
}
|
|
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('
|
|
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
|
|
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
|
|
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
|
-
|
|
428
|
+
const hasGrok = !!which('grok');
|
|
429
|
+
if (alreadyRunning.ok && alreadyRunning.output.includes('Browser')) {
|
|
344
430
|
p.log.success('Chrome already running (localhost:9222)');
|
|
345
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
addResult.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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 {
|
|
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
|
-
|
|
27
|
+
execFileSync('which', [cli.bin], { stdio: 'pipe' });
|
|
7
28
|
}
|
|
8
29
|
catch {
|
|
9
|
-
console.error(
|
|
10
|
-
|
|
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
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
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(
|
|
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:
|
|
170
|
+
enabled: true,
|
|
136
171
|
preamblePerChat: 5,
|
|
137
172
|
maxActivePerChat: 10,
|
|
138
173
|
hotnessCapOnCreate: 70,
|
package/dist/db/address.js
CHANGED
|
@@ -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;
|
package/dist/db/identity-sync.js
CHANGED
|
@@ -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,
|
|
7
|
-
const
|
|
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(
|
|
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: "${
|
|
26
|
-
if (
|
|
27
|
-
lines.push(`Members: ${
|
|
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(`
|
|
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) {
|
package/dist/gateway/commands.js
CHANGED
|
@@ -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
|
|
26
|
+
await ctx.reply(reply);
|
|
28
27
|
return true;
|
|
29
28
|
}
|
|
30
29
|
if (config.commands.status.includes(cmd)) {
|
|
31
|
-
const
|
|
30
|
+
const provider = getProvider();
|
|
31
|
+
const info = getSessionInfo(ctx.jid, provider.name);
|
|
32
32
|
if (!info) {
|
|
33
|
-
await
|
|
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 =
|
|
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
|
|
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
|
|
56
|
+
await ctx.reply(reply);
|
|
57
57
|
return true;
|
|
58
58
|
}
|
|
59
59
|
if (cmd === 'digest') {
|
|
60
|
-
await
|
|
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
|
|
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(
|
|
82
|
-
await
|
|
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
|
|
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
|
|
89
|
+
await ctx.reply(handleThreadsCommand(ctx.jid, args));
|
|
92
90
|
return true;
|
|
93
91
|
}
|
|
94
92
|
return false;
|