twinclaw 1.1.7 → 1.1.8

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 CHANGED
@@ -166,10 +166,13 @@ export const handleVersionCli = async (argv) => {
166
166
  * Handle the `start` command.
167
167
  * Starts the gateway.
168
168
  */
169
- export const handleStartCli = async (argv) => {
170
- console.log('[TwinClaw] Starting gateway...');
171
- console.log('[TwinClaw] Use "twinclaw gateway start" for more options');
172
- console.log('[TwinClaw] Use "twinclaw gateway install" to install as Windows service');
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
+ if (isChatMode) {
174
+ global.TWINCLAW_CHAT_MODE = true;
175
+ }
173
176
  return 0;
174
177
  };
175
178
  /**
@@ -239,6 +242,7 @@ export function handleUnknownCommand(argv) {
239
242
  'version',
240
243
  'start',
241
244
  'stop',
245
+ 'chat',
242
246
  'help'
243
247
  ]);
244
248
  if (KNOWN_COMMANDS.has(command) || command.startsWith('--') || command.startsWith('-')) {
@@ -33,29 +33,19 @@ export function startBasicREPL(gateway) {
33
33
  }
34
34
  const WIZARD_SECTIONS = [
35
35
  {
36
- id: 'runtime',
37
- label: 'Runtime & Security',
38
- fields: ['API_SECRET', 'API_PORT'],
39
- },
40
- {
41
- id: 'models',
42
- label: 'Intelligence & Models',
43
- fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', 'GITHUB_TOKEN'],
36
+ id: 'channel',
37
+ label: 'STEP 1: Choose Your Messaging Channel',
38
+ fields: [], // Special handling for channel selection
44
39
  },
45
40
  {
46
- id: 'model_selection',
47
- label: 'Select Models',
48
- fields: ['SELECT_MODELS'],
49
- },
50
- {
51
- id: 'messaging',
52
- label: 'Messaging & Channels',
53
- fields: ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_USER_ID', 'WHATSAPP_PHONE_NUMBER'],
41
+ id: 'security',
42
+ label: 'STEP 2: Security',
43
+ fields: ['API_SECRET', 'API_PORT'],
54
44
  },
55
45
  {
56
- id: 'memory',
57
- label: 'Memory & Workspace Defaults',
58
- fields: ['EMBEDDING_PROVIDER'],
46
+ id: 'models',
47
+ label: 'STEP 3: AI Models',
48
+ fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY'],
59
49
  },
60
50
  ];
61
51
  const EMBEDDING_PROVIDERS = new Set(['openai', 'ollama']);
@@ -719,100 +709,99 @@ async function promptFieldValue(field, currentValue, prompter, logger) {
719
709
  }
720
710
  async function collectInteractiveUpdates(existing, logger, prompter) {
721
711
  const workspaceDir = getWorkspaceDir();
722
- logger.log('\nTwinClaw Onboarding Wizard v2.0');
723
- logger.log('──────────────────────────────────────────────────');
724
- logger.log(`Workspace: ${workspaceDir}`);
725
- logger.log('Model keys, channel preferences, and workspace defaults will be configured.');
726
- logger.log("Secret prompts are masked. Type '-' to clear an existing optional value.\n");
712
+ logger.log('\n═══════════════════════════════════════════════════════════');
713
+ logger.log(' 🎉 Welcome to TwinClaw Setup!');
714
+ logger.log('═══════════════════════════════════════════════════════════');
715
+ logger.log(`\n📁 Workspace: ${workspaceDir}`);
727
716
  const updates = {};
728
717
  let candidate = cloneConfig(existing);
729
718
  for (const section of WIZARD_SECTIONS) {
730
- while (true) {
731
- logger.log(`\nSection: ${section.label}`);
719
+ logger.log('\n═══════════════════════════════════════════════════════════');
720
+ logger.log(` ${section.label}`);
721
+ logger.log('═══════════════════════════════════════════════════════════\n');
722
+ if (section.id === 'channel') {
723
+ logger.log('How would you like to connect with TwinClaw?');
732
724
  logger.log('──────────────────────────────────────────────────');
733
- for (const fieldKey of section.fields) {
734
- const field = ONBOARD_FIELDS.find((item) => item.key === fieldKey);
735
- if (!field) {
736
- continue;
725
+ logger.log(' 1. Terminal Only (Chat directly in terminal)');
726
+ logger.log(' 2. Telegram');
727
+ logger.log(' 3. WhatsApp');
728
+ logger.log(' 4. Both Telegram & WhatsApp');
729
+ const choice = await prompter.prompt('\nEnter number: ');
730
+ if (choice === '2' || choice === '4') {
731
+ const token = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'TELEGRAM_BOT_TOKEN'), '', prompter, logger);
732
+ if (token) {
733
+ updates['TELEGRAM_BOT_TOKEN'] = token;
734
+ candidate = applyUpdates(candidate, { 'TELEGRAM_BOT_TOKEN': token });
735
+ const userId = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'TELEGRAM_USER_ID'), '', prompter, logger);
736
+ if (userId) {
737
+ updates['TELEGRAM_USER_ID'] = userId;
738
+ candidate = applyUpdates(candidate, { 'TELEGRAM_USER_ID': userId });
739
+ }
737
740
  }
738
- const current = readOnboardConfigValue(candidate, field.key);
739
- const nextValue = await promptFieldValue(field, current, prompter, logger);
740
- if (nextValue !== undefined) {
741
- updates[field.key] = nextValue;
742
- candidate = applyUpdates(candidate, { [field.key]: nextValue });
741
+ }
742
+ if (choice === '3' || choice === '4') {
743
+ const phone = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'WHATSAPP_PHONE_NUMBER'), '', prompter, logger);
744
+ if (phone) {
745
+ updates['WHATSAPP_PHONE_NUMBER'] = phone;
746
+ candidate = applyUpdates(candidate, { 'WHATSAPP_PHONE_NUMBER': phone });
743
747
  }
744
748
  }
745
- if (section.id === 'models' && !hasAnyModelKey(candidate)) {
746
- logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini, or GitHub Copilot).');
749
+ continue;
750
+ }
751
+ if (section.id === 'models') {
752
+ logger.log('You need at least one AI model API key.');
753
+ logger.log('Free keys available at:');
754
+ logger.log(' - OpenRouter: https://openrouter.ai/keys');
755
+ logger.log(' - Google AI: https://aistudio.google.com/app/apikey');
756
+ logger.log(' - Modal: https://modal.com');
757
+ }
758
+ for (const fieldKey of section.fields) {
759
+ const field = ONBOARD_FIELDS.find((item) => item.key === fieldKey);
760
+ if (!field) {
747
761
  continue;
748
762
  }
749
- if (section.id === 'messaging') {
750
- const telegramToken = (candidate.messaging.telegram.botToken ?? '').trim();
751
- const telegramUserId = candidate.messaging.telegram.userId;
752
- if (hasValue(telegramToken) && !telegramUserId) {
753
- logger.warn('\nTELEGRAM_USER_ID is required when TELEGRAM_BOT_TOKEN is provided.');
754
- continue;
755
- }
756
- if (!hasValue(telegramToken) && telegramUserId) {
757
- logger.warn('\nTELEGRAM_BOT_TOKEN is required when TELEGRAM_USER_ID is provided.');
758
- continue;
759
- }
763
+ const current = readOnboardConfigValue(candidate, field.key);
764
+ const nextValue = await promptFieldValue(field, current, prompter, logger);
765
+ if (nextValue !== undefined) {
766
+ updates[field.key] = nextValue;
767
+ candidate = applyUpdates(candidate, { [field.key]: nextValue });
768
+ }
769
+ }
770
+ if (section.id === 'models' && !hasAnyModelKey(candidate)) {
771
+ logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini).');
772
+ // Force retry this section
773
+ // In a real loop we'd handle this better, but for simplicity here:
774
+ const orKey = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'OPENROUTER_API_KEY'), '', prompter, logger);
775
+ if (orKey) {
776
+ updates['OPENROUTER_API_KEY'] = orKey;
777
+ candidate = applyUpdates(candidate, { 'OPENROUTER_API_KEY': orKey });
760
778
  }
761
- break;
762
779
  }
763
780
  }
764
781
  while (true) {
782
+ logger.log('\n═══════════════════════════════════════════════════════════');
783
+ logger.log(' ✅ Setup Complete!');
784
+ logger.log('═══════════════════════════════════════════════════════════');
765
785
  logger.log('\nSummary of Configuration:');
766
786
  logger.log('──────────────────────────────────────────────────');
767
787
  for (const field of ONBOARD_FIELDS) {
768
788
  const val = readOnboardConfigValue(candidate, field.key);
769
- const displayVal = field.secret || SECRET_KEYS.has(field.key)
770
- ? val
771
- ? '********'
772
- : '(not set)'
773
- : val || '(not set)';
774
- logger.log(` ${field.label}: ${displayVal}`);
789
+ if (val || field.required) {
790
+ const displayVal = (field.secret || SECRET_KEYS.has(field.key)) ? (val ? '********' : '(not set)') : (val || '(not set)');
791
+ logger.log(` ${field.label}: ${displayVal}`);
792
+ }
775
793
  }
776
794
  const choice = await prompter.prompt('\nConfirm configuration? [y]es, [n]o (cancel), [e]dit: ');
777
795
  const lower = choice.toLowerCase();
778
796
  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
797
  return updates;
784
798
  }
785
799
  if (lower === 'n' || lower === 'no') {
786
800
  throw new OnboardingCancelledError();
787
801
  }
788
802
  if (lower === 'e' || lower === 'edit') {
789
- logger.log('\nSelect section to edit:');
790
- WIZARD_SECTIONS.forEach((s, i) => {
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;
803
+ // Simple re-run for now to keep it clean
804
+ return collectInteractiveUpdates(existing, logger, prompter);
816
805
  }
817
806
  logger.warn("Please enter 'y', 'n', or 'e'.");
818
807
  }
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
- for (const def of sortedHandlers) {
141
- const commands = Array.isArray(def.command) ? def.command : [def.command];
142
- if (commands.includes(command)) {
143
- const exitCode = await def.handler(argv);
144
- process.exit(exitCode);
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();
@@ -91,8 +91,9 @@ export class ProactiveNotifier {
91
91
  return;
92
92
  if (event.type !== 'add' && event.type !== 'change')
93
93
  return;
94
- const normalizedPath = event.path.toLowerCase();
95
- const isIgnoredFile = this.#fileAlertIgnoreSubstrings.some((segment) => normalizedPath.includes(segment.toLowerCase()));
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
- const normalized = content.trim();
13
- if (!normalized) {
14
- return;
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 memoryRowId = saveMemoryEmbedding(sessionId, taggedChunk, embedding);
25
- const node = buildReasoningNode(sessionId, role, taggedChunk);
26
- upsertReasoningNode(node);
27
- linkMemoryProvenance(memoryRowId, node.nodeId, sessionId);
28
- if (previousNodeId && previousNodeId !== node.nodeId) {
29
- upsertReasoningEdge({
30
- edgeId: stableId('edge', `${previousNodeId}|${node.nodeId}|derived_from`),
31
- fromNodeId: previousNodeId,
32
- toNodeId: node.nodeId,
33
- relation: 'derived_from',
34
- weight: 0.55,
35
- provenance: `session:${sessionId}:turn-sequence`,
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
- previousNodeId = node.nodeId;
39
- reconcileClaimRelations(node);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twinclaw",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Eagle-eyed agentic AI gateway with multi-modal hooks and proactive memory.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {