twinclaw 1.1.7 → 1.2.0
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/core/cli.js +28 -5
- package/dist/core/onboarding.js +88 -83
- package/dist/core/persona-cli.js +78 -0
- package/dist/index.js +17 -9
- package/dist/services/embedding-service.js +4 -1
- package/dist/services/model-router.js +15 -0
- package/dist/services/proactive-notifier.js +3 -2
- package/dist/services/semantic-memory.js +34 -26
- package/package.json +1 -1
package/dist/core/cli.js
CHANGED
|
@@ -164,12 +164,34 @@ export const handleVersionCli = async (argv) => {
|
|
|
164
164
|
};
|
|
165
165
|
/**
|
|
166
166
|
* Handle the `start` command.
|
|
167
|
-
* Starts the gateway.
|
|
167
|
+
* Starts the gateway in a new terminal window.
|
|
168
168
|
*/
|
|
169
|
-
export const handleStartCli = async (argv) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
export const handleStartCli = async (argv, isChatMode = false) => {
|
|
170
|
+
if (isChatMode) {
|
|
171
|
+
global.TWINCLAW_CHAT_MODE = true;
|
|
172
|
+
}
|
|
173
|
+
if (process.platform === 'win32') {
|
|
174
|
+
const { spawn } = await import('node:child_process');
|
|
175
|
+
console.log('[TwinClaw] Starting gateway in new window...');
|
|
176
|
+
const entryScript = process.execPath;
|
|
177
|
+
const appPath = process.argv[1] || process.argv[0];
|
|
178
|
+
const args = [
|
|
179
|
+
'/c',
|
|
180
|
+
'start',
|
|
181
|
+
'""',
|
|
182
|
+
'cmd.exe',
|
|
183
|
+
'/k',
|
|
184
|
+
`"${entryScript}" chat`
|
|
185
|
+
];
|
|
186
|
+
spawn('cmd.exe', args, {
|
|
187
|
+
detached: true,
|
|
188
|
+
stdio: 'ignore',
|
|
189
|
+
windowsHide: false,
|
|
190
|
+
});
|
|
191
|
+
console.log('[TwinClaw] Gateway started in new terminal window.');
|
|
192
|
+
console.log('[TwinClaw] You can now use this terminal for other commands.');
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
173
195
|
return 0;
|
|
174
196
|
};
|
|
175
197
|
/**
|
|
@@ -239,6 +261,7 @@ export function handleUnknownCommand(argv) {
|
|
|
239
261
|
'version',
|
|
240
262
|
'start',
|
|
241
263
|
'stop',
|
|
264
|
+
'chat',
|
|
242
265
|
'help'
|
|
243
266
|
]);
|
|
244
267
|
if (KNOWN_COMMANDS.has(command) || command.startsWith('--') || command.startsWith('-')) {
|
package/dist/core/onboarding.js
CHANGED
|
@@ -33,30 +33,25 @@ export function startBasicREPL(gateway) {
|
|
|
33
33
|
}
|
|
34
34
|
const WIZARD_SECTIONS = [
|
|
35
35
|
{
|
|
36
|
-
id: '
|
|
37
|
-
label: '
|
|
36
|
+
id: 'channel',
|
|
37
|
+
label: 'STEP 1: Choose Your Messaging Channel',
|
|
38
|
+
fields: [], // Special handling for channel selection
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'security',
|
|
42
|
+
label: 'STEP 2: Security',
|
|
38
43
|
fields: ['API_SECRET', 'API_PORT'],
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
46
|
id: 'models',
|
|
42
|
-
label: '
|
|
47
|
+
label: 'STEP 3: AI Models',
|
|
43
48
|
fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', 'GITHUB_TOKEN'],
|
|
44
49
|
},
|
|
45
50
|
{
|
|
46
51
|
id: 'model_selection',
|
|
47
|
-
label: 'Select Models',
|
|
52
|
+
label: 'STEP 4: Select Models',
|
|
48
53
|
fields: ['SELECT_MODELS'],
|
|
49
54
|
},
|
|
50
|
-
{
|
|
51
|
-
id: 'messaging',
|
|
52
|
-
label: 'Messaging & Channels',
|
|
53
|
-
fields: ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_USER_ID', 'WHATSAPP_PHONE_NUMBER'],
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 'memory',
|
|
57
|
-
label: 'Memory & Workspace Defaults',
|
|
58
|
-
fields: ['EMBEDDING_PROVIDER'],
|
|
59
|
-
},
|
|
60
55
|
];
|
|
61
56
|
const EMBEDDING_PROVIDERS = new Set(['openai', 'ollama']);
|
|
62
57
|
const COPILOT_DEVICE_FLOW_TRIGGERS = new Set(['device', 'auth', 'login', 'copilot']);
|
|
@@ -719,100 +714,110 @@ async function promptFieldValue(field, currentValue, prompter, logger) {
|
|
|
719
714
|
}
|
|
720
715
|
async function collectInteractiveUpdates(existing, logger, prompter) {
|
|
721
716
|
const workspaceDir = getWorkspaceDir();
|
|
722
|
-
logger.log('\
|
|
723
|
-
logger.log('
|
|
724
|
-
logger.log(
|
|
725
|
-
logger.log(
|
|
726
|
-
logger.log("Secret prompts are masked. Type '-' to clear an existing optional value.\n");
|
|
717
|
+
logger.log('\n═══════════════════════════════════════════════════════════');
|
|
718
|
+
logger.log(' 🎉 Welcome to TwinClaw Setup!');
|
|
719
|
+
logger.log('═══════════════════════════════════════════════════════════');
|
|
720
|
+
logger.log(`\n📁 Workspace: ${workspaceDir}`);
|
|
727
721
|
const updates = {};
|
|
728
722
|
let candidate = cloneConfig(existing);
|
|
729
723
|
for (const section of WIZARD_SECTIONS) {
|
|
730
|
-
|
|
731
|
-
|
|
724
|
+
logger.log('\n═══════════════════════════════════════════════════════════');
|
|
725
|
+
logger.log(` ${section.label}`);
|
|
726
|
+
logger.log('═══════════════════════════════════════════════════════════\n');
|
|
727
|
+
if (section.id === 'channel') {
|
|
728
|
+
logger.log('How would you like to connect with TwinClaw?');
|
|
732
729
|
logger.log('──────────────────────────────────────────────────');
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
730
|
+
logger.log(' 1. Terminal Only (Chat directly in terminal)');
|
|
731
|
+
logger.log(' 2. Telegram');
|
|
732
|
+
logger.log(' 3. WhatsApp');
|
|
733
|
+
logger.log(' 4. Both Telegram & WhatsApp');
|
|
734
|
+
const choice = await prompter.prompt('\nEnter number: ');
|
|
735
|
+
if (choice === '2' || choice === '4') {
|
|
736
|
+
const token = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'TELEGRAM_BOT_TOKEN'), '', prompter, logger);
|
|
737
|
+
if (token) {
|
|
738
|
+
updates['TELEGRAM_BOT_TOKEN'] = token;
|
|
739
|
+
candidate = applyUpdates(candidate, { 'TELEGRAM_BOT_TOKEN': token });
|
|
740
|
+
const userId = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'TELEGRAM_USER_ID'), '', prompter, logger);
|
|
741
|
+
if (userId) {
|
|
742
|
+
updates['TELEGRAM_USER_ID'] = userId;
|
|
743
|
+
candidate = applyUpdates(candidate, { 'TELEGRAM_USER_ID': userId });
|
|
744
|
+
}
|
|
737
745
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
746
|
+
}
|
|
747
|
+
if (choice === '3' || choice === '4') {
|
|
748
|
+
const phone = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'WHATSAPP_PHONE_NUMBER'), '', prompter, logger);
|
|
749
|
+
if (phone) {
|
|
750
|
+
updates['WHATSAPP_PHONE_NUMBER'] = phone;
|
|
751
|
+
candidate = applyUpdates(candidate, { 'WHATSAPP_PHONE_NUMBER': phone });
|
|
743
752
|
}
|
|
744
753
|
}
|
|
745
|
-
|
|
746
|
-
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (section.id === 'models') {
|
|
757
|
+
logger.log('You need at least one AI model API key.');
|
|
758
|
+
logger.log('Free keys available at:');
|
|
759
|
+
logger.log(' - OpenRouter: https://openrouter.ai/keys');
|
|
760
|
+
logger.log(' - Google AI: https://aistudio.google.com/app/apikey');
|
|
761
|
+
logger.log(' - Modal: https://modal.com');
|
|
762
|
+
}
|
|
763
|
+
for (const fieldKey of section.fields) {
|
|
764
|
+
const field = ONBOARD_FIELDS.find((item) => item.key === fieldKey);
|
|
765
|
+
if (!field) {
|
|
747
766
|
continue;
|
|
748
767
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
768
|
+
const current = readOnboardConfigValue(candidate, field.key);
|
|
769
|
+
const nextValue = await promptFieldValue(field, current, prompter, logger);
|
|
770
|
+
if (nextValue !== undefined) {
|
|
771
|
+
updates[field.key] = nextValue;
|
|
772
|
+
candidate = applyUpdates(candidate, { [field.key]: nextValue });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (section.id === 'model_selection') {
|
|
776
|
+
// Special logic to ensure model selection was actually handled if skipped in loop
|
|
777
|
+
if (!updates['SELECT_MODELS']) {
|
|
778
|
+
const field = ONBOARD_FIELDS.find(f => f.key === 'SELECT_MODELS');
|
|
779
|
+
const nextValue = await promptFieldValue(field, '', prompter, logger);
|
|
780
|
+
if (nextValue) {
|
|
781
|
+
updates['SELECT_MODELS'] = nextValue;
|
|
782
|
+
candidate = applyUpdates(candidate, { 'SELECT_MODELS': nextValue });
|
|
759
783
|
}
|
|
760
784
|
}
|
|
761
|
-
|
|
785
|
+
}
|
|
786
|
+
if (section.id === 'models' && !hasAnyModelKey(candidate)) {
|
|
787
|
+
logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini).');
|
|
788
|
+
// Force retry this section
|
|
789
|
+
// In a real loop we'd handle this better, but for simplicity here:
|
|
790
|
+
const orKey = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'OPENROUTER_API_KEY'), '', prompter, logger);
|
|
791
|
+
if (orKey) {
|
|
792
|
+
updates['OPENROUTER_API_KEY'] = orKey;
|
|
793
|
+
candidate = applyUpdates(candidate, { 'OPENROUTER_API_KEY': orKey });
|
|
794
|
+
}
|
|
762
795
|
}
|
|
763
796
|
}
|
|
764
797
|
while (true) {
|
|
798
|
+
logger.log('\n═══════════════════════════════════════════════════════════');
|
|
799
|
+
logger.log(' ✅ Setup Complete!');
|
|
800
|
+
logger.log('═══════════════════════════════════════════════════════════');
|
|
765
801
|
logger.log('\nSummary of Configuration:');
|
|
766
802
|
logger.log('──────────────────────────────────────────────────');
|
|
767
803
|
for (const field of ONBOARD_FIELDS) {
|
|
768
804
|
const val = readOnboardConfigValue(candidate, field.key);
|
|
769
|
-
|
|
770
|
-
? val
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
: val || '(not set)';
|
|
774
|
-
logger.log(` ${field.label}: ${displayVal}`);
|
|
805
|
+
if (val || field.required) {
|
|
806
|
+
const displayVal = (field.secret || SECRET_KEYS.has(field.key)) ? (val ? '********' : '(not set)') : (val || '(not set)');
|
|
807
|
+
logger.log(` ${field.label}: ${displayVal}`);
|
|
808
|
+
}
|
|
775
809
|
}
|
|
776
810
|
const choice = await prompter.prompt('\nConfirm configuration? [y]es, [n]o (cancel), [e]dit: ');
|
|
777
811
|
const lower = choice.toLowerCase();
|
|
778
812
|
if (lower === 'y' || lower === 'yes') {
|
|
779
|
-
if (!hasAnyModelKey(candidate)) {
|
|
780
|
-
logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini, or GitHub Copilot).');
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
813
|
return updates;
|
|
784
814
|
}
|
|
785
815
|
if (lower === 'n' || lower === 'no') {
|
|
786
816
|
throw new OnboardingCancelledError();
|
|
787
817
|
}
|
|
788
818
|
if (lower === 'e' || lower === 'edit') {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
logger.log(` ${i + 1}. ${s.label}`);
|
|
792
|
-
});
|
|
793
|
-
const sectionIdxStr = await prompter.prompt('\nSection number: ');
|
|
794
|
-
const sectionIdx = Number.parseInt(sectionIdxStr, 10) - 1;
|
|
795
|
-
if (WIZARD_SECTIONS[sectionIdx]) {
|
|
796
|
-
const section = WIZARD_SECTIONS[sectionIdx];
|
|
797
|
-
logger.log(`\nEditing Section: ${section.label}`);
|
|
798
|
-
logger.log('──────────────────────────────────────────────────');
|
|
799
|
-
for (const fieldKey of section.fields) {
|
|
800
|
-
const field = ONBOARD_FIELDS.find((item) => item.key === fieldKey);
|
|
801
|
-
if (!field) {
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
const current = readOnboardConfigValue(candidate, field.key);
|
|
805
|
-
const nextValue = await promptFieldValue(field, current, prompter, logger);
|
|
806
|
-
if (nextValue !== undefined) {
|
|
807
|
-
updates[field.key] = nextValue;
|
|
808
|
-
candidate = applyUpdates(candidate, { [field.key]: nextValue });
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
logger.warn('Invalid section number.');
|
|
814
|
-
}
|
|
815
|
-
continue;
|
|
819
|
+
// Simple re-run for now to keep it clean
|
|
820
|
+
return collectInteractiveUpdates(existing, logger, prompter);
|
|
816
821
|
}
|
|
817
822
|
logger.warn("Please enter 'y', 'n', or 'e'.");
|
|
818
823
|
}
|
|
@@ -882,9 +887,9 @@ function printWarnings(warnings, logger) {
|
|
|
882
887
|
}
|
|
883
888
|
function printNextActions(logger) {
|
|
884
889
|
logger.log('\nNext actions:');
|
|
885
|
-
logger.log(' 1.
|
|
886
|
-
logger.log(' 2.
|
|
887
|
-
logger.log(' 3.
|
|
890
|
+
logger.log(' 1. twinclaw doctor');
|
|
891
|
+
logger.log(' 2. twinclaw channels login whatsapp');
|
|
892
|
+
logger.log(' 3. twinclaw pairing approve <channel> <CODE>');
|
|
888
893
|
}
|
|
889
894
|
export function parseOnboardArgs(args) {
|
|
890
895
|
const parsed = {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getPersonaStateService } from '../services/persona-state.js';
|
|
2
|
+
import { formatHelp } from './help-formatter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Handle 'persona' command.
|
|
5
|
+
* Allows viewing and updating the agent's soul, identity, and user context.
|
|
6
|
+
*/
|
|
7
|
+
export const handlePersonaCli = async (argv) => {
|
|
8
|
+
const service = getPersonaStateService();
|
|
9
|
+
const subcommand = argv[1];
|
|
10
|
+
if (!subcommand || subcommand === 'show' || subcommand === 'view') {
|
|
11
|
+
const state = await service.getState();
|
|
12
|
+
console.log(', TwinClaw, Persona, State, ');, console.log('──────────────────────────────────────────────────'));
|
|
13
|
+
console.log(`Revision: ${state.revision}`);
|
|
14
|
+
console.log(`Updated: ${state.updatedAt}`);
|
|
15
|
+
console.log(', -- - SOUL-- - ');, console.log(state.soul || '(empty)'));
|
|
16
|
+
console.log(', -- - IDENTITY-- - ');, console.log(state.identity || '(empty)'));
|
|
17
|
+
console.log(', -- - USER, CONTEXT-- - ');, console.log(state.user || '(empty)'));
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
if (subcommand === 'edit' || subcommand === 'update') {
|
|
21
|
+
const key = argv[2];
|
|
22
|
+
if (!['soul', 'identity', 'user'].includes(key)) {
|
|
23
|
+
console.error('Invalid document key. Use: soul, identity, or user.');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const newContent = argv.slice(3).join(' ');
|
|
27
|
+
if (!newContent) {
|
|
28
|
+
console.error(`Please provide the new content for ${key}.`);
|
|
29
|
+
console.log(`Usage: twinclaw persona edit ${key} "your content here"`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const currentState = await service.getState();
|
|
34
|
+
const result = await service.updateState({
|
|
35
|
+
expectedRevision: currentState.revision,
|
|
36
|
+
[key]: newContent,
|
|
37
|
+
// Keep other fields as is
|
|
38
|
+
soul: key === 'soul' ? newContent : currentState.soul,
|
|
39
|
+
identity: key === 'identity' ? newContent : currentState.identity,
|
|
40
|
+
user: key === 'user' ? newContent : currentState.user,
|
|
41
|
+
});
|
|
42
|
+
console.log(`✅ Successfully updated ${key}.`);
|
|
43
|
+
console.log(`New revision: ${result.state.revision}`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error(`❌ Failed to update persona: ${err instanceof Error ? err.message : String(err)}`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (subcommand === 'help' || argv.includes('--help') || argv.includes('-h')) {
|
|
52
|
+
console.log(formatHelp({
|
|
53
|
+
command: 'persona',
|
|
54
|
+
description: "View or modify the agent's core persona (soul, identity, user context).",
|
|
55
|
+
usage: 'twinclaw persona [subcommand] [args]',
|
|
56
|
+
sections: [
|
|
57
|
+
{
|
|
58
|
+
title: 'Subcommands',
|
|
59
|
+
content: {
|
|
60
|
+
'show, view': 'Display current persona state (default)',
|
|
61
|
+
'edit, update': 'Update a specific part of the persona'
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: 'Update Usage',
|
|
66
|
+
content: {
|
|
67
|
+
'twinclaw persona edit soul "text"': 'Update the agent soul',
|
|
68
|
+
'twinclaw persona edit identity "text"': 'Update agent identity',
|
|
69
|
+
'twinclaw persona edit user "text"': 'Update user context'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
console.error(`Unknown persona subcommand: ${subcommand}`);
|
|
77
|
+
return 1;
|
|
78
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -67,6 +67,7 @@ const CLI_HANDLERS = [
|
|
|
67
67
|
{ command: ['version', '--version', '-v'], handler: handleVersionCli, priority: CLI_HANDLER_PRIORITY.VERSION, description: 'Show version' },
|
|
68
68
|
{ command: 'start', handler: handleStartCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Start gateway' },
|
|
69
69
|
{ command: 'stop', handler: handleStopCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Stop gateway' },
|
|
70
|
+
{ command: 'chat', handler: async (args) => { return await handleStartCli(['start', ...args.slice(1)], true); }, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Open terminal chat' },
|
|
70
71
|
{ command: 'secret', handler: handleSecretCommandCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage secrets' },
|
|
71
72
|
{ command: 'config', handler: handleConfigCommandCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Validate runtime config' },
|
|
72
73
|
{ command: 'pairing', handler: async (args) => { return await handlePairingCli(args, pairingService) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage pairings' },
|
|
@@ -122,7 +123,6 @@ async function tryAutoSetup() {
|
|
|
122
123
|
printPreStartValidationFailure(postSetupValidation.blockingIssues);
|
|
123
124
|
return false;
|
|
124
125
|
}
|
|
125
|
-
console.log("\n[TwinClaw] Setup complete. Initializing Gateway...\n");
|
|
126
126
|
return true;
|
|
127
127
|
}
|
|
128
128
|
async function main() {
|
|
@@ -137,11 +137,19 @@ async function main() {
|
|
|
137
137
|
}
|
|
138
138
|
// Sort handlers by priority
|
|
139
139
|
const sortedHandlers = [...CLI_HANDLERS].sort((a, b) => b.priority - a.priority);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
140
|
+
if (command) {
|
|
141
|
+
let commandHandled = false;
|
|
142
|
+
for (const def of sortedHandlers) {
|
|
143
|
+
const commands = Array.isArray(def.command) ? def.command : [def.command];
|
|
144
|
+
if (commands.includes(command)) {
|
|
145
|
+
commandHandled = true;
|
|
146
|
+
const exitCode = await def.handler(argv);
|
|
147
|
+
process.exit(exitCode);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!commandHandled && !command.startsWith('-')) {
|
|
151
|
+
handleUnknownCommand(argv);
|
|
152
|
+
process.exit(1);
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
// Handle help flags early if no command matched
|
|
@@ -149,9 +157,6 @@ async function main() {
|
|
|
149
157
|
await handleHelpCli(argv);
|
|
150
158
|
process.exit(0);
|
|
151
159
|
}
|
|
152
|
-
if (command && handleUnknownCommand(argv)) {
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
160
|
checkAndMigrateWorkspace();
|
|
156
161
|
// If no command provided, try to start the gateway
|
|
157
162
|
if (!(await tryAutoSetup())) {
|
|
@@ -248,6 +253,9 @@ async function main() {
|
|
|
248
253
|
deny: parseToolSelectors(getConfigValue('TOOLS_DENY')),
|
|
249
254
|
},
|
|
250
255
|
});
|
|
256
|
+
if (global.TWINCLAW_CHAT_MODE) {
|
|
257
|
+
startBasicREPL(gateway);
|
|
258
|
+
}
|
|
251
259
|
const telegramBotToken = secretVault.readSecret('TELEGRAM_BOT_TOKEN') ?? getConfigValue('TELEGRAM_BOT_TOKEN');
|
|
252
260
|
const twinClawConfig = await readConfig();
|
|
253
261
|
const telegramUserIdRaw = getConfigValue('TELEGRAM_USER_ID');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getSecretVaultService } from './secret-vault.js';
|
|
2
2
|
import { getConfigValue } from '../config/json-config.js';
|
|
3
|
+
import { logThoughtThrottled } from '../utils/logger.js';
|
|
3
4
|
const DEFAULT_OPENAI_URL = 'https://api.openai.com/v1/embeddings';
|
|
4
5
|
const DEFAULT_OPENAI_MODEL = 'text-embedding-3-small';
|
|
5
6
|
const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
|
|
@@ -81,12 +82,14 @@ export class EmbeddingService {
|
|
|
81
82
|
}
|
|
82
83
|
return null;
|
|
83
84
|
}
|
|
84
|
-
debugLog(message) {
|
|
85
|
+
async debugLog(message) {
|
|
85
86
|
const debugFlag = process.env.DEBUG?.trim().toLowerCase();
|
|
86
87
|
const debugEnabled = Boolean(debugFlag && debugFlag !== '0' && debugFlag !== 'false');
|
|
87
88
|
if (debugEnabled) {
|
|
88
89
|
console.warn(message);
|
|
89
90
|
}
|
|
91
|
+
// Always log to thought log, but throttled to prevent spam
|
|
92
|
+
await logThoughtThrottled('embedding_error', message, 30000); // 30s throttle
|
|
90
93
|
}
|
|
91
94
|
getProviderOrder() {
|
|
92
95
|
const configured = (getConfigValue('EMBEDDING_PROVIDER') ?? '').toLowerCase().trim();
|
|
@@ -980,8 +980,23 @@ export class ModelRouter {
|
|
|
980
980
|
this.metrics.lastError = scrubSensitiveText(message);
|
|
981
981
|
this.metrics.lastFailureAt = new Date(this.nowFn()).toISOString();
|
|
982
982
|
}
|
|
983
|
+
getApiKeyEnvName(provider) {
|
|
984
|
+
const providerKeyMap = {
|
|
985
|
+
'openai': 'OPENAI_API_KEY',
|
|
986
|
+
'anthropic': 'ANTHROPIC_API_KEY',
|
|
987
|
+
'google': 'GEMINI_API_KEY',
|
|
988
|
+
'openrouter': 'OPENROUTER_API_KEY',
|
|
989
|
+
'groq': 'GROQ_API_KEY',
|
|
990
|
+
'modal': 'MODAL_API_KEY',
|
|
991
|
+
'copilot': 'GITHUB_TOKEN',
|
|
992
|
+
'github': 'GITHUB_TOKEN',
|
|
993
|
+
'ollama': 'OLLAMA_API_KEY',
|
|
994
|
+
};
|
|
995
|
+
return providerKeyMap[provider.toLowerCase()] || 'MODAL_API_KEY';
|
|
996
|
+
}
|
|
983
997
|
loadModelsFromConfig() {
|
|
984
998
|
const configModels = [];
|
|
999
|
+
// Check if PRIMARY_MODEL is set - use config.models.primaryModel if available
|
|
985
1000
|
const primaryModelId = getConfigValue('PRIMARY_MODEL');
|
|
986
1001
|
const modalApiKey = getConfigValue('MODAL_API_KEY');
|
|
987
1002
|
const openRouterApiKey = getConfigValue('OPENROUTER_API_KEY');
|
|
@@ -91,8 +91,9 @@ export class ProactiveNotifier {
|
|
|
91
91
|
return;
|
|
92
92
|
if (event.type !== 'add' && event.type !== 'change')
|
|
93
93
|
return;
|
|
94
|
-
|
|
95
|
-
const
|
|
94
|
+
// Normalize path for robust comparison (handles Windows backslashes)
|
|
95
|
+
const normalizedPath = event.path.replace(/\\/g, '/').toLowerCase();
|
|
96
|
+
const isIgnoredFile = this.#fileAlertIgnoreSubstrings.some((segment) => normalizedPath.includes(segment.replace(/\\/g, '/').toLowerCase()));
|
|
96
97
|
if (isIgnoredFile) {
|
|
97
98
|
await logThought(`[ProactiveNotifier] Ignored modification event for system file: ${event.path}`);
|
|
98
99
|
return;
|
|
@@ -9,34 +9,42 @@ const TASK_HINT_PATTERN = /\b(todo|task|implement|fix|build|create|refactor|ship
|
|
|
9
9
|
const MAX_RELATION_RECONCILIATION = 12;
|
|
10
10
|
const MAX_GRAPH_TRAVERSAL_DEPTH = 2;
|
|
11
11
|
export async function indexConversationTurn(sessionId, role, content) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const chunks = chunkText(normalized);
|
|
17
|
-
let previousNodeId = null;
|
|
18
|
-
for (const chunk of chunks) {
|
|
19
|
-
const taggedChunk = `${role.toUpperCase()}: ${chunk}`;
|
|
20
|
-
const embedding = await embeddingService.embedText(taggedChunk);
|
|
21
|
-
if (!embedding) {
|
|
22
|
-
continue;
|
|
12
|
+
try {
|
|
13
|
+
const normalized = content.trim();
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
return;
|
|
23
16
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
const chunks = chunkText(normalized);
|
|
18
|
+
let previousNodeId = null;
|
|
19
|
+
// Limit indexing to first 2 chunks to prevent long hangs during indexing
|
|
20
|
+
const limitedChunks = chunks.slice(0, 2);
|
|
21
|
+
for (const chunk of limitedChunks) {
|
|
22
|
+
const taggedChunk = `${role.toUpperCase()}: ${chunk}`;
|
|
23
|
+
const embedding = await embeddingService.embedText(taggedChunk);
|
|
24
|
+
if (!embedding) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const memoryRowId = saveMemoryEmbedding(sessionId, taggedChunk, embedding);
|
|
28
|
+
const node = buildReasoningNode(sessionId, role, taggedChunk);
|
|
29
|
+
upsertReasoningNode(node);
|
|
30
|
+
linkMemoryProvenance(memoryRowId, node.nodeId, sessionId);
|
|
31
|
+
if (previousNodeId && previousNodeId !== node.nodeId) {
|
|
32
|
+
upsertReasoningEdge({
|
|
33
|
+
edgeId: stableId('edge', `${previousNodeId}|${node.nodeId}|derived_from`),
|
|
34
|
+
fromNodeId: previousNodeId,
|
|
35
|
+
toNodeId: node.nodeId,
|
|
36
|
+
relation: 'derived_from',
|
|
37
|
+
weight: 0.55,
|
|
38
|
+
provenance: `session:${sessionId}:turn-sequence`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
previousNodeId = node.nodeId;
|
|
42
|
+
reconcileClaimRelations(node);
|
|
37
43
|
}
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
console.warn(`[SemanticMemory] Failed to index turn for session ${sessionId}: ${message}`);
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
export async function retrieveEvidenceAwareMemoryContext(sessionId, prompt, topK = 5) {
|