twinclaw 1.1.8 → 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 +23 -4
- package/dist/core/onboarding.js +20 -4
- package/dist/core/persona-cli.js +78 -0
- package/dist/services/model-router.js +15 -0
- package/package.json +1 -1
package/dist/core/cli.js
CHANGED
|
@@ -164,15 +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
169
|
export const handleStartCli = async (argv, isChatMode = false) => {
|
|
170
|
-
// We return 0 to tell main() to continue with the startup flow
|
|
171
|
-
// If we wanted to start JUST the gateway and exit, we'd do something else
|
|
172
|
-
// But here, 'start' means 'run the app'.
|
|
173
170
|
if (isChatMode) {
|
|
174
171
|
global.TWINCLAW_CHAT_MODE = true;
|
|
175
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
|
+
}
|
|
176
195
|
return 0;
|
|
177
196
|
};
|
|
178
197
|
/**
|
package/dist/core/onboarding.js
CHANGED
|
@@ -45,7 +45,12 @@ const WIZARD_SECTIONS = [
|
|
|
45
45
|
{
|
|
46
46
|
id: 'models',
|
|
47
47
|
label: 'STEP 3: AI Models',
|
|
48
|
-
fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY'],
|
|
48
|
+
fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', 'GITHUB_TOKEN'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'model_selection',
|
|
52
|
+
label: 'STEP 4: Select Models',
|
|
53
|
+
fields: ['SELECT_MODELS'],
|
|
49
54
|
},
|
|
50
55
|
];
|
|
51
56
|
const EMBEDDING_PROVIDERS = new Set(['openai', 'ollama']);
|
|
@@ -767,6 +772,17 @@ async function collectInteractiveUpdates(existing, logger, prompter) {
|
|
|
767
772
|
candidate = applyUpdates(candidate, { [field.key]: nextValue });
|
|
768
773
|
}
|
|
769
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 });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
770
786
|
if (section.id === 'models' && !hasAnyModelKey(candidate)) {
|
|
771
787
|
logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini).');
|
|
772
788
|
// Force retry this section
|
|
@@ -871,9 +887,9 @@ function printWarnings(warnings, logger) {
|
|
|
871
887
|
}
|
|
872
888
|
function printNextActions(logger) {
|
|
873
889
|
logger.log('\nNext actions:');
|
|
874
|
-
logger.log(' 1.
|
|
875
|
-
logger.log(' 2.
|
|
876
|
-
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>');
|
|
877
893
|
}
|
|
878
894
|
export function parseOnboardArgs(args) {
|
|
879
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
|
+
};
|
|
@@ -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');
|