@keyoku/openclaw 1.2.11 → 1.2.13
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/context.d.ts +2 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +129 -83
- package/dist/context.js.map +1 -1
- package/dist/heartbeat-migration.d.ts +47 -0
- package/dist/heartbeat-migration.d.ts.map +1 -0
- package/dist/heartbeat-migration.js +178 -0
- package/dist/heartbeat-migration.js.map +1 -0
- package/dist/heartbeat-setup.d.ts.map +1 -1
- package/dist/heartbeat-setup.js +21 -13
- package/dist/heartbeat-setup.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +83 -4
- package/dist/hooks.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +390 -97
- package/dist/init.js.map +1 -1
- package/dist/migration.d.ts +12 -0
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +1 -1
- package/dist/migration.js.map +1 -1
- package/dist/service.d.ts +13 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +4 -4
- package/dist/service.js.map +1 -1
- package/package.json +3 -3
package/dist/init.js
CHANGED
|
@@ -22,6 +22,8 @@ import { pipeline } from 'node:stream/promises';
|
|
|
22
22
|
import { KeyokuClient } from '@keyoku/memory';
|
|
23
23
|
import { importMemoryFiles } from './migration.js';
|
|
24
24
|
import { migrateAllVectorStores, discoverVectorDbs } from './migrate-vector-store.js';
|
|
25
|
+
import { extractHeartbeatRules, migrateHeartbeatRules } from './heartbeat-migration.js';
|
|
26
|
+
import { findKeyokuBinary, loadKeyokuEnv, waitForHealthy } from './service.js';
|
|
25
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
28
|
const HOME = process.env.HOME ?? '';
|
|
27
29
|
const OPENCLAW_CONFIG_PATH = join(HOME, '.openclaw', 'openclaw.json');
|
|
@@ -48,7 +50,7 @@ const c = {
|
|
|
48
50
|
};
|
|
49
51
|
// ── Output Helpers ───────────────────────────────────────────────────────
|
|
50
52
|
let currentStep = 0;
|
|
51
|
-
const totalSteps =
|
|
53
|
+
const totalSteps = 11;
|
|
52
54
|
function stepHeader(label) {
|
|
53
55
|
currentStep++;
|
|
54
56
|
console.log('');
|
|
@@ -352,72 +354,79 @@ function installSkill() {
|
|
|
352
354
|
cpSync(bundledSkillPath, join(workspaceSkillDir, 'SKILL.md'));
|
|
353
355
|
success(`SKILL.md installed → ${c.dim}~/.openclaw/skills/keyoku-memory/${c.reset}`);
|
|
354
356
|
}
|
|
357
|
+
// ── HEARTBEAT.md Installation ────────────────────────────────────────────
|
|
358
|
+
/**
|
|
359
|
+
* Install HEARTBEAT.md to the OpenClaw workspace directory.
|
|
360
|
+
* Must be done at init time (not at plugin register time) because OpenClaw's
|
|
361
|
+
* heartbeat system overwrites the file with a default placeholder after plugins register.
|
|
362
|
+
*
|
|
363
|
+
* If an existing HEARTBEAT.md has user content, preserves it and appends keyoku section.
|
|
364
|
+
* Returns extracted heartbeat rules for migration into Keyoku.
|
|
365
|
+
*/
|
|
366
|
+
function installHeartbeatMd() {
|
|
367
|
+
const workspaceDir = join(HOME, '.openclaw', 'workspace');
|
|
368
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
369
|
+
const heartbeatPath = join(workspaceDir, 'HEARTBEAT.md');
|
|
370
|
+
const KEYOKU_SECTION = `<!-- keyoku-heartbeat-start -->
|
|
371
|
+
## Keyoku Memory Heartbeat
|
|
372
|
+
|
|
373
|
+
You have been checked in on. Your memory system has reviewed your recent activity and surfaced anything that needs your attention. The signals are injected into your context automatically — look for the <heartbeat-signals> block.
|
|
374
|
+
|
|
375
|
+
### How to respond
|
|
376
|
+
|
|
377
|
+
IMPORTANT: If the signals contain \`should_act: true\` or a "Tell the User" section with ANY content, you MUST write a message to the user. Do NOT reply HEARTBEAT_OK in that case. Say something — even one sentence is fine.
|
|
378
|
+
|
|
379
|
+
1. Read the signals carefully. Check urgency, mode, and should_act.
|
|
380
|
+
2. If \`should_act\` is true — you MUST send a message. Use the "Tell the User" or "Action Brief" section as guidance for what to say. Keep it natural and brief.
|
|
381
|
+
3. If mode is \`act\` — take action immediately. Do what the signal says.
|
|
382
|
+
4. If mode is \`suggest\` and urgency is not \`none\` — surface the suggestion naturally.
|
|
383
|
+
5. ONLY reply HEARTBEAT_OK if \`should_act\` is false AND there is truly nothing in the signals worth mentioning.
|
|
384
|
+
|
|
385
|
+
Do not repeat old tasks from prior conversations. Only act on what the signals say right now.
|
|
386
|
+
<!-- keyoku-heartbeat-end -->`;
|
|
387
|
+
let extractedRules = [];
|
|
388
|
+
if (existsSync(heartbeatPath)) {
|
|
389
|
+
const existing = readFileSync(heartbeatPath, 'utf-8');
|
|
390
|
+
// Extract user rules before modifying the file
|
|
391
|
+
extractedRules = extractHeartbeatRules(existing);
|
|
392
|
+
if (extractedRules.length > 0) {
|
|
393
|
+
info(`Found ${extractedRules.length} heartbeat rule(s) to migrate`);
|
|
394
|
+
}
|
|
395
|
+
if (existing.includes('keyoku-heartbeat-start')) {
|
|
396
|
+
success('HEARTBEAT.md already configured');
|
|
397
|
+
return extractedRules;
|
|
398
|
+
}
|
|
399
|
+
// Preserve existing user content, append keyoku section
|
|
400
|
+
const content = existing.trimEnd() + `\n\n---\n\n${KEYOKU_SECTION}\n`;
|
|
401
|
+
writeFileSync(heartbeatPath, content, 'utf-8');
|
|
402
|
+
success(`HEARTBEAT.md updated (preserved existing content) → ${c.dim}~/.openclaw/workspace/${c.reset}`);
|
|
403
|
+
return extractedRules;
|
|
404
|
+
}
|
|
405
|
+
// No existing file — write full template
|
|
406
|
+
writeFileSync(heartbeatPath, `# Heartbeat Check\n\n${KEYOKU_SECTION}\n`, 'utf-8');
|
|
407
|
+
success(`HEARTBEAT.md installed → ${c.dim}~/.openclaw/workspace/${c.reset}`);
|
|
408
|
+
return extractedRules;
|
|
409
|
+
}
|
|
355
410
|
// ── LLM Provider Setup ──────────────────────────────────────────────────
|
|
356
411
|
/**
|
|
357
412
|
* Set up LLM provider and API keys.
|
|
358
|
-
*
|
|
413
|
+
*
|
|
414
|
+
* Flow:
|
|
415
|
+
* 5a. Embedding provider (Gemini or OpenAI only — Anthropic has no embedding models)
|
|
416
|
+
* 5b. Extraction provider (Gemini, OpenAI, or Anthropic)
|
|
417
|
+
* 5c. Extraction model (depends on provider, shows benchmark notes)
|
|
418
|
+
* 5d. API key(s) for each unique provider selected
|
|
359
419
|
*/
|
|
360
420
|
async function setupLlmProvider() {
|
|
361
421
|
// Check existing env vars
|
|
362
422
|
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
363
423
|
const hasGemini = !!process.env.GEMINI_API_KEY;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// Auto-detect best available provider
|
|
371
|
-
if (hasGemini) {
|
|
372
|
-
appendToEnvFile('KEYOKU_EXTRACTION_PROVIDER', 'gemini');
|
|
373
|
-
appendToEnvFile('KEYOKU_EXTRACTION_MODEL', 'gemini-2.5-flash');
|
|
374
|
-
appendToEnvFile('KEYOKU_EMBEDDING_PROVIDER', 'gemini');
|
|
375
|
-
appendToEnvFile('KEYOKU_EMBEDDING_MODEL', 'gemini-embedding-001');
|
|
376
|
-
success(`Auto-configured: ${c.bold}Gemini${c.reset} for extraction + embeddings`);
|
|
377
|
-
}
|
|
378
|
-
else if (hasOpenAI) {
|
|
379
|
-
appendToEnvFile('KEYOKU_EXTRACTION_PROVIDER', 'openai');
|
|
380
|
-
appendToEnvFile('KEYOKU_EXTRACTION_MODEL', 'gpt-5-mini');
|
|
381
|
-
appendToEnvFile('KEYOKU_EMBEDDING_PROVIDER', 'openai');
|
|
382
|
-
appendToEnvFile('KEYOKU_EMBEDDING_MODEL', 'text-embedding-3-small');
|
|
383
|
-
success(`Auto-configured: ${c.bold}OpenAI${c.reset} for extraction + embeddings`);
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
// No API key detected — prompt for provider
|
|
387
|
-
const provider = await choose('Which LLM provider?', [
|
|
388
|
-
{ label: 'OpenAI', value: 'openai', desc: 'extraction + embeddings' },
|
|
389
|
-
{ label: 'Gemini', value: 'gemini', desc: 'extraction + embeddings' },
|
|
390
|
-
]);
|
|
391
|
-
if (provider === 'gemini') {
|
|
392
|
-
const key = await prompt('Gemini API key:');
|
|
393
|
-
if (key) {
|
|
394
|
-
appendToEnvFile('GEMINI_API_KEY', key);
|
|
395
|
-
appendToEnvFile('KEYOKU_EXTRACTION_PROVIDER', 'gemini');
|
|
396
|
-
appendToEnvFile('KEYOKU_EXTRACTION_MODEL', 'gemini-2.5-flash');
|
|
397
|
-
appendToEnvFile('KEYOKU_EMBEDDING_PROVIDER', 'gemini');
|
|
398
|
-
appendToEnvFile('KEYOKU_EMBEDDING_MODEL', 'gemini-embedding-001');
|
|
399
|
-
success('Gemini configured');
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
warn('No key provided — set GEMINI_API_KEY manually');
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
// Default: OpenAI
|
|
407
|
-
const key = await prompt('OpenAI API key (sk-...):');
|
|
408
|
-
if (key && key.startsWith('sk-')) {
|
|
409
|
-
appendToEnvFile('OPENAI_API_KEY', key);
|
|
410
|
-
appendToEnvFile('KEYOKU_EXTRACTION_PROVIDER', 'openai');
|
|
411
|
-
appendToEnvFile('KEYOKU_EXTRACTION_MODEL', 'gpt-5-mini');
|
|
412
|
-
appendToEnvFile('KEYOKU_EMBEDDING_PROVIDER', 'openai');
|
|
413
|
-
appendToEnvFile('KEYOKU_EMBEDDING_MODEL', 'text-embedding-3-small');
|
|
414
|
-
success('OpenAI configured');
|
|
415
|
-
}
|
|
416
|
-
else {
|
|
417
|
-
warn('Invalid key — set OPENAI_API_KEY manually');
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
424
|
+
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
425
|
+
// Show current config if already set (but still allow reconfiguration)
|
|
426
|
+
const currentExtraction = process.env.KEYOKU_EXTRACTION_PROVIDER;
|
|
427
|
+
const currentEmbedding = process.env.KEYOKU_EMBEDDING_PROVIDER;
|
|
428
|
+
if (currentExtraction && currentEmbedding) {
|
|
429
|
+
info(`Current: ${c.dim}${currentExtraction}/${process.env.KEYOKU_EXTRACTION_MODEL || 'default'} + ${currentEmbedding}/${process.env.KEYOKU_EMBEDDING_MODEL || 'default'}${c.reset}`);
|
|
421
430
|
}
|
|
422
431
|
// Show detected API keys
|
|
423
432
|
const detected = [];
|
|
@@ -425,9 +434,92 @@ async function setupLlmProvider() {
|
|
|
425
434
|
detected.push('OpenAI');
|
|
426
435
|
if (hasGemini)
|
|
427
436
|
detected.push('Gemini');
|
|
437
|
+
if (hasAnthropic)
|
|
438
|
+
detected.push('Anthropic');
|
|
428
439
|
if (detected.length > 0) {
|
|
429
440
|
success(`API keys detected: ${c.bold}${detected.join(', ')}${c.reset}`);
|
|
430
441
|
}
|
|
442
|
+
// ── 5a: Embedding provider ──────────────────────────────────────────
|
|
443
|
+
console.log('');
|
|
444
|
+
log('Embeddings convert text into vectors for semantic search.');
|
|
445
|
+
log(`${c.yellow}Anthropic does not offer embedding models.${c.reset}`);
|
|
446
|
+
const embeddingProvider = await choose('Embedding provider?', [
|
|
447
|
+
{ label: 'Gemini', value: 'gemini', desc: 'gemini-embedding-001' },
|
|
448
|
+
{ label: 'OpenAI', value: 'openai', desc: 'text-embedding-3-small' },
|
|
449
|
+
]);
|
|
450
|
+
if (embeddingProvider === 'gemini') {
|
|
451
|
+
appendToEnvFile('KEYOKU_EMBEDDING_PROVIDER', 'gemini'); // engine uses "gemini"
|
|
452
|
+
appendToEnvFile('KEYOKU_EMBEDDING_MODEL', 'gemini-embedding-001');
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
appendToEnvFile('KEYOKU_EMBEDDING_PROVIDER', 'openai');
|
|
456
|
+
appendToEnvFile('KEYOKU_EMBEDDING_MODEL', 'text-embedding-3-small');
|
|
457
|
+
}
|
|
458
|
+
success(`Embedding → ${c.bold}${embeddingProvider}${c.reset}`);
|
|
459
|
+
// ── 5b: Extraction provider ─────────────────────────────────────────
|
|
460
|
+
const extractionProvider = await choose('Extraction provider?', [
|
|
461
|
+
{ label: 'Gemini (recommended)', value: 'gemini', desc: 'Best price-to-quality ratio' },
|
|
462
|
+
{ label: 'OpenAI', value: 'openai', desc: 'Reliable, widely supported' },
|
|
463
|
+
{ label: 'Anthropic', value: 'anthropic', desc: 'Highest quality, no embeddings (uses your embedding provider)' },
|
|
464
|
+
]);
|
|
465
|
+
appendToEnvFile('KEYOKU_EXTRACTION_PROVIDER', extractionProvider);
|
|
466
|
+
// ── 5c: Extraction model ────────────────────────────────────────────
|
|
467
|
+
let extractionModel;
|
|
468
|
+
if (extractionProvider === 'gemini') {
|
|
469
|
+
extractionModel = await choose('Extraction model?', [
|
|
470
|
+
{ label: 'gemini-3.1-flash-lite-preview (recommended)', value: 'gemini-3.1-flash-lite-preview', desc: 'Cheapest and fastest, near-perfect quality' },
|
|
471
|
+
{ label: 'gemini-2.5-flash', value: 'gemini-2.5-flash', desc: 'Thinking model, highest accuracy, slower' },
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
else if (extractionProvider === 'openai') {
|
|
475
|
+
extractionModel = await choose('Extraction model?', [
|
|
476
|
+
{ label: 'gpt-4.1-mini', value: 'gpt-4.1-mini', desc: 'Balanced speed and quality' },
|
|
477
|
+
{ label: 'gpt-4.1-nano', value: 'gpt-4.1-nano', desc: 'Cheapest, slightly less reliable on complex schemas' },
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// Anthropic — single model
|
|
482
|
+
extractionModel = 'claude-haiku-4-5-20251001';
|
|
483
|
+
info(`Extraction model: ${c.bold}claude-haiku-4-5-20251001${c.reset} ${c.dim}Fast, top-tier quality${c.reset}`);
|
|
484
|
+
}
|
|
485
|
+
appendToEnvFile('KEYOKU_EXTRACTION_MODEL', extractionModel);
|
|
486
|
+
success(`Extraction → ${c.bold}${extractionProvider}/${extractionModel}${c.reset}`);
|
|
487
|
+
log(`${c.dim}More models coming soon — re-run init to update.${c.reset}`);
|
|
488
|
+
// ── 5d: API keys ────────────────────────────────────────────────────
|
|
489
|
+
// Collect the unique providers that need keys
|
|
490
|
+
const neededProviders = new Set([embeddingProvider, extractionProvider]);
|
|
491
|
+
for (const provider of neededProviders) {
|
|
492
|
+
if (provider === 'gemini' && !hasGemini) {
|
|
493
|
+
const key = await prompt('Gemini API key:');
|
|
494
|
+
if (key) {
|
|
495
|
+
appendToEnvFile('GEMINI_API_KEY', key);
|
|
496
|
+
success('Gemini API key saved');
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
warn('No key provided — set GEMINI_API_KEY manually');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else if (provider === 'openai' && !hasOpenAI) {
|
|
503
|
+
const key = await prompt('OpenAI API key (sk-...):');
|
|
504
|
+
if (key && key.startsWith('sk-')) {
|
|
505
|
+
appendToEnvFile('OPENAI_API_KEY', key);
|
|
506
|
+
success('OpenAI API key saved');
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
warn('Invalid key — set OPENAI_API_KEY manually');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else if (provider === 'anthropic' && !hasAnthropic) {
|
|
513
|
+
const key = await prompt('Anthropic API key (sk-ant-...):');
|
|
514
|
+
if (key && key.startsWith('sk-ant-')) {
|
|
515
|
+
appendToEnvFile('ANTHROPIC_API_KEY', key);
|
|
516
|
+
success('Anthropic API key saved');
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
warn('Invalid key — set ANTHROPIC_API_KEY manually');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
431
523
|
}
|
|
432
524
|
// ── Environment File Management ──────────────────────────────────────────
|
|
433
525
|
/**
|
|
@@ -565,6 +657,174 @@ async function setupTimezoneAndQuietHours() {
|
|
|
565
657
|
}
|
|
566
658
|
success(`Quiet hours → ${c.bold}${isNaN(start) ? 23 : start}:00 – ${isNaN(end) ? 7 : end}:00${c.reset} (${timezone})`);
|
|
567
659
|
}
|
|
660
|
+
// ── Heartbeat Delivery Setup ─────────────────────────────────────────────
|
|
661
|
+
/**
|
|
662
|
+
* Platform-specific instructions for finding group chat IDs.
|
|
663
|
+
*/
|
|
664
|
+
const GROUP_ID_HINTS = {
|
|
665
|
+
telegram: 'Add @RawDataBot to your group — it replies with the chat ID (a negative number like -4970078838)',
|
|
666
|
+
discord: 'Right-click your channel → Copy Channel ID (enable Developer Mode in Settings → Advanced first)',
|
|
667
|
+
slack: 'Open channel details → scroll to the bottom → Channel ID',
|
|
668
|
+
whatsapp: 'Group JID (shown in WhatsApp Web URL or API logs)',
|
|
669
|
+
googlechat: 'Space ID from the URL (spaces/<id>)',
|
|
670
|
+
msteams: 'Channel ID from Teams admin or Graph API',
|
|
671
|
+
signal: 'Group ID from Signal API logs',
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Set up heartbeat delivery in openclaw.json.
|
|
675
|
+
*
|
|
676
|
+
* Detects configured channels, asks for a group chat ID,
|
|
677
|
+
* and writes agents.defaults.heartbeat with target + to.
|
|
678
|
+
*/
|
|
679
|
+
async function setupHeartbeatDelivery(config) {
|
|
680
|
+
// Detect configured channels from openclaw.json
|
|
681
|
+
const channels = config.channels;
|
|
682
|
+
const configuredChannels = channels
|
|
683
|
+
? Object.keys(channels).filter((k) => k !== 'defaults')
|
|
684
|
+
: [];
|
|
685
|
+
if (configuredChannels.length === 0) {
|
|
686
|
+
log('No messaging channels configured in openclaw.json');
|
|
687
|
+
log('Heartbeat will run but messages won\'t be delivered externally');
|
|
688
|
+
log(`${c.dim}Configure a channel (telegram, discord, etc.) and re-run init${c.reset}`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
info(`Detected channel(s): ${c.bold}${configuredChannels.join(', ')}${c.reset}`);
|
|
692
|
+
console.log('');
|
|
693
|
+
log('Heartbeat lets your agent proactively message you when something needs attention.');
|
|
694
|
+
log(`${c.yellow}Heartbeats deliver to group chats only — DMs are blocked by OpenClaw.${c.reset}`);
|
|
695
|
+
const enableDelivery = await choose('Set up heartbeat delivery now?', [
|
|
696
|
+
{ label: 'Yes', value: 'yes', desc: 'configure group chat delivery' },
|
|
697
|
+
{ label: 'Skip', value: 'no', desc: 'heartbeat runs but no messages sent' },
|
|
698
|
+
]);
|
|
699
|
+
if (enableDelivery === 'no') {
|
|
700
|
+
// Write target: "none" explicitly so it's clear in config
|
|
701
|
+
ensureAgentsDefaults(config);
|
|
702
|
+
config.agents.defaults.heartbeat = {
|
|
703
|
+
...(config.agents?.defaults?.heartbeat || {}),
|
|
704
|
+
every: '30m',
|
|
705
|
+
target: 'none',
|
|
706
|
+
};
|
|
707
|
+
writeOpenClawConfig(config);
|
|
708
|
+
success('Heartbeat delivery → none (runs silently)');
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// Pick channel
|
|
712
|
+
let targetChannel;
|
|
713
|
+
if (configuredChannels.length === 1) {
|
|
714
|
+
targetChannel = configuredChannels[0];
|
|
715
|
+
info(`Using ${c.bold}${targetChannel}${c.reset} (only configured channel)`);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
const channelOptions = configuredChannels.map((ch) => ({
|
|
719
|
+
label: ch.charAt(0).toUpperCase() + ch.slice(1),
|
|
720
|
+
value: ch,
|
|
721
|
+
}));
|
|
722
|
+
targetChannel = await choose('Which channel should receive heartbeat messages?', channelOptions);
|
|
723
|
+
}
|
|
724
|
+
// Get group chat ID
|
|
725
|
+
const hint = GROUP_ID_HINTS[targetChannel];
|
|
726
|
+
console.log('');
|
|
727
|
+
log(`Enter the ${c.bold}group chat ID${c.reset} for ${targetChannel}.`);
|
|
728
|
+
if (hint) {
|
|
729
|
+
log(`${c.dim}Tip: ${hint}${c.reset}`);
|
|
730
|
+
}
|
|
731
|
+
log(`${c.yellow}Must be a group/channel — DMs will not work.${c.reset}`);
|
|
732
|
+
console.log('');
|
|
733
|
+
const groupId = await prompt(`${targetChannel} group chat ID:`);
|
|
734
|
+
if (!groupId) {
|
|
735
|
+
warn('No group ID provided — heartbeat delivery disabled');
|
|
736
|
+
ensureAgentsDefaults(config);
|
|
737
|
+
config.agents.defaults.heartbeat = {
|
|
738
|
+
...(config.agents?.defaults?.heartbeat || {}),
|
|
739
|
+
every: '30m',
|
|
740
|
+
target: 'none',
|
|
741
|
+
};
|
|
742
|
+
writeOpenClawConfig(config);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// Pick interval
|
|
746
|
+
const interval = await choose('Heartbeat interval?', [
|
|
747
|
+
{ label: '15 minutes', value: '15m', desc: 'balanced' },
|
|
748
|
+
{ label: '30 minutes', value: '30m', desc: 'default, lower cost' },
|
|
749
|
+
{ label: '1 hour', value: '1h', desc: 'minimal cost' },
|
|
750
|
+
{ label: '5 minutes', value: '5m', desc: 'frequent, higher cost' },
|
|
751
|
+
]);
|
|
752
|
+
// Write to openclaw.json
|
|
753
|
+
ensureAgentsDefaults(config);
|
|
754
|
+
config.agents.defaults.heartbeat = {
|
|
755
|
+
...(config.agents?.defaults?.heartbeat || {}),
|
|
756
|
+
every: interval,
|
|
757
|
+
target: targetChannel,
|
|
758
|
+
to: groupId,
|
|
759
|
+
};
|
|
760
|
+
writeOpenClawConfig(config);
|
|
761
|
+
success(`Heartbeat → ${c.bold}${targetChannel}${c.reset} (group: ${c.dim}${groupId}${c.reset}, every ${interval})`);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Ensure agents.defaults exists in config.
|
|
765
|
+
*/
|
|
766
|
+
function ensureAgentsDefaults(config) {
|
|
767
|
+
if (!config.agents)
|
|
768
|
+
config.agents = {};
|
|
769
|
+
if (!config.agents.defaults)
|
|
770
|
+
config.agents.defaults = {};
|
|
771
|
+
if (!config.agents.defaults.heartbeat)
|
|
772
|
+
config.agents.defaults.heartbeat = {};
|
|
773
|
+
}
|
|
774
|
+
// ── Start Keyoku for Migration ──────────────────────────────────────────
|
|
775
|
+
/**
|
|
776
|
+
* Temporarily start the keyoku binary so migration can call the API.
|
|
777
|
+
* Returns a cleanup function to kill the process afterward.
|
|
778
|
+
*/
|
|
779
|
+
async function startKeyokuForMigration() {
|
|
780
|
+
const { spawn } = await import('node:child_process');
|
|
781
|
+
const { randomBytes } = await import('node:crypto');
|
|
782
|
+
// Check if already running
|
|
783
|
+
if (await waitForHealthy('http://localhost:18900', 2000, 500)) {
|
|
784
|
+
info('Keyoku already running');
|
|
785
|
+
return () => { }; // no-op cleanup
|
|
786
|
+
}
|
|
787
|
+
const binary = findKeyokuBinary();
|
|
788
|
+
if (!binary) {
|
|
789
|
+
warn('Keyoku binary not found — migration will be skipped');
|
|
790
|
+
return () => { };
|
|
791
|
+
}
|
|
792
|
+
const keyokuEnv = loadKeyokuEnv();
|
|
793
|
+
const env = { ...keyokuEnv, ...process.env };
|
|
794
|
+
if (!env.KEYOKU_SESSION_TOKEN) {
|
|
795
|
+
env.KEYOKU_SESSION_TOKEN = randomBytes(16).toString('hex');
|
|
796
|
+
}
|
|
797
|
+
process.env.KEYOKU_SESSION_TOKEN = env.KEYOKU_SESSION_TOKEN;
|
|
798
|
+
if (!env.KEYOKU_DB_PATH) {
|
|
799
|
+
const dbDir = join(HOME, '.keyoku', 'data');
|
|
800
|
+
mkdirSync(dbDir, { recursive: true });
|
|
801
|
+
env.KEYOKU_DB_PATH = join(dbDir, 'keyoku.db');
|
|
802
|
+
}
|
|
803
|
+
info('Starting keyoku for migration...');
|
|
804
|
+
const proc = spawn(binary, [], {
|
|
805
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
806
|
+
detached: false,
|
|
807
|
+
env,
|
|
808
|
+
});
|
|
809
|
+
// Suppress output during migration
|
|
810
|
+
proc.stdout?.resume();
|
|
811
|
+
proc.stderr?.resume();
|
|
812
|
+
const healthy = await waitForHealthy('http://localhost:18900', 10000, 500);
|
|
813
|
+
if (healthy) {
|
|
814
|
+
info('Keyoku ready for migration');
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
warn('Keyoku health check timed out — migration may fail');
|
|
818
|
+
}
|
|
819
|
+
return () => {
|
|
820
|
+
try {
|
|
821
|
+
proc.kill('SIGTERM');
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
// Process already exited
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
568
828
|
// ── Health Check ─────────────────────────────────────────────────────────
|
|
569
829
|
/**
|
|
570
830
|
* Run a health check against keyoku-engine to verify the install works.
|
|
@@ -681,63 +941,92 @@ export async function init() {
|
|
|
681
941
|
// Step 7: Timezone & quiet hours
|
|
682
942
|
stepHeader('Timezone & Quiet Hours');
|
|
683
943
|
await setupTimezoneAndQuietHours();
|
|
684
|
-
// Step 8:
|
|
944
|
+
// Step 8: Heartbeat delivery
|
|
945
|
+
stepHeader('Heartbeat Delivery');
|
|
946
|
+
await setupHeartbeatDelivery(config);
|
|
947
|
+
// Step 9: SKILL.md
|
|
685
948
|
stepHeader('Install Skill Guide');
|
|
686
949
|
installSkill();
|
|
687
|
-
// Step
|
|
950
|
+
// Step 9b: HEARTBEAT.md (extract user rules before writing keyoku section)
|
|
951
|
+
const heartbeatRules = installHeartbeatMd();
|
|
952
|
+
// Step 10: Migration
|
|
688
953
|
const memoryMdPath = join(HOME, '.openclaw', 'MEMORY.md');
|
|
689
954
|
const hasMemoryMd = existsSync(memoryMdPath);
|
|
690
955
|
const vectorDbs = discoverVectorDbs(OPENCLAW_MEMORY_DIR);
|
|
691
956
|
const hasVectorStores = vectorDbs.length > 0;
|
|
692
|
-
|
|
957
|
+
const hasHeartbeatRules = heartbeatRules.length > 0;
|
|
958
|
+
if (hasMemoryMd || hasVectorStores || hasHeartbeatRules) {
|
|
693
959
|
stepHeader('Migrate Memories');
|
|
694
960
|
if (hasMemoryMd)
|
|
695
961
|
info('Found MEMORY.md');
|
|
696
962
|
if (hasVectorStores)
|
|
697
963
|
info(`Found ${vectorDbs.length} vector store(s)`);
|
|
964
|
+
if (hasHeartbeatRules)
|
|
965
|
+
info(`Found ${heartbeatRules.length} heartbeat rule(s)`);
|
|
698
966
|
const migrate = await choose('Migrate existing memories into Keyoku?', [
|
|
699
967
|
{ label: 'Yes', value: 'yes', desc: 'import everything now' },
|
|
700
968
|
{ label: 'No', value: 'no', desc: 'skip for now' },
|
|
701
969
|
]);
|
|
702
970
|
if (migrate === 'yes') {
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
971
|
+
// Start keyoku temporarily for migration
|
|
972
|
+
const cleanup = await startKeyokuForMigration();
|
|
973
|
+
try {
|
|
974
|
+
const client = new KeyokuClient({
|
|
975
|
+
baseUrl: 'http://localhost:18900',
|
|
976
|
+
token: process.env.KEYOKU_SESSION_TOKEN,
|
|
977
|
+
timeout: 60000,
|
|
978
|
+
});
|
|
979
|
+
const entityId = 'default';
|
|
980
|
+
// Migrate markdown files
|
|
981
|
+
if (hasMemoryMd) {
|
|
982
|
+
try {
|
|
983
|
+
const mdResult = await importMemoryFiles({
|
|
984
|
+
client,
|
|
985
|
+
entityId,
|
|
986
|
+
workspaceDir: join(HOME, '.openclaw'),
|
|
987
|
+
logger: console,
|
|
988
|
+
});
|
|
989
|
+
success(`Markdown: ${mdResult.imported} imported, ${mdResult.skipped} skipped`);
|
|
990
|
+
}
|
|
991
|
+
catch (err) {
|
|
992
|
+
warn(`Markdown migration failed: ${String(err)}`);
|
|
993
|
+
}
|
|
724
994
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
995
|
+
// Migrate vector stores
|
|
996
|
+
if (hasVectorStores) {
|
|
997
|
+
try {
|
|
998
|
+
const vsResult = await migrateAllVectorStores({
|
|
999
|
+
client,
|
|
1000
|
+
entityId,
|
|
1001
|
+
memoryDir: OPENCLAW_MEMORY_DIR,
|
|
1002
|
+
logger: console,
|
|
1003
|
+
});
|
|
1004
|
+
success(`Vector stores: ${vsResult.imported} imported, ${vsResult.skipped} skipped`);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
warn(`Vector store migration failed: ${String(err)}`);
|
|
1008
|
+
}
|
|
736
1009
|
}
|
|
737
|
-
|
|
738
|
-
|
|
1010
|
+
// Migrate heartbeat rules (preferences, rules, scheduled tasks)
|
|
1011
|
+
if (hasHeartbeatRules) {
|
|
1012
|
+
try {
|
|
1013
|
+
const hbResult = await migrateHeartbeatRules({
|
|
1014
|
+
client,
|
|
1015
|
+
entityId,
|
|
1016
|
+
agentId: 'default',
|
|
1017
|
+
rules: heartbeatRules,
|
|
1018
|
+
logger: console,
|
|
1019
|
+
});
|
|
1020
|
+
success(`Heartbeat: ${hbResult.preferences} preferences, ${hbResult.rules} rules, ${hbResult.schedules} schedules`);
|
|
1021
|
+
}
|
|
1022
|
+
catch (err) {
|
|
1023
|
+
warn(`Heartbeat migration failed: ${String(err)}`);
|
|
1024
|
+
}
|
|
739
1025
|
}
|
|
740
1026
|
}
|
|
1027
|
+
finally {
|
|
1028
|
+
cleanup();
|
|
1029
|
+
}
|
|
741
1030
|
}
|
|
742
1031
|
else {
|
|
743
1032
|
log('Skipping — you can re-run init later to migrate');
|
|
@@ -747,7 +1036,7 @@ export async function init() {
|
|
|
747
1036
|
stepHeader('Migrate Memories');
|
|
748
1037
|
log('No existing memories found — nothing to migrate');
|
|
749
1038
|
}
|
|
750
|
-
// Step
|
|
1039
|
+
// Step 11: Health check
|
|
751
1040
|
stepHeader('Health Check');
|
|
752
1041
|
await healthCheck();
|
|
753
1042
|
// Close readline before exiting
|
|
@@ -772,6 +1061,10 @@ export async function init() {
|
|
|
772
1061
|
console.log(` ${c.dim}openclaw memory status${c.reset} ${c.dim}check memory index status${c.reset}`);
|
|
773
1062
|
console.log(` ${c.dim}openclaw memory search${c.reset} ${c.dim}search stored memories${c.reset}`);
|
|
774
1063
|
console.log('');
|
|
1064
|
+
console.log(` ${c.gray}3.${c.reset} ${c.yellow}Heartbeat requires a group chat${c.reset}`);
|
|
1065
|
+
console.log(` ${c.dim}OpenClaw delivers heartbeats to group chats only (not DMs).${c.reset}`);
|
|
1066
|
+
console.log(` ${c.dim}Add your bot to a Telegram/Discord/WhatsApp group to receive proactive check-ins.${c.reset}`);
|
|
1067
|
+
console.log('');
|
|
775
1068
|
console.log(` ${c.indigo}${'━'.repeat(52)}${c.reset}`);
|
|
776
1069
|
console.log('');
|
|
777
1070
|
}
|