@calliopelabs/cli 2.3.0 → 2.5.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.
Files changed (186) hide show
  1. package/README.md +17 -0
  2. package/dist/agents/agent-config-loader.js +1 -1
  3. package/dist/agents/agent-config-presets.js +13 -13
  4. package/dist/agents/agent-config-presets.js.map +1 -1
  5. package/dist/agents/agent-config-types.d.ts +1 -1
  6. package/dist/agents/agent-config-types.d.ts.map +1 -1
  7. package/dist/agents/dynamic-tools.d.ts.map +1 -1
  8. package/dist/agents/dynamic-tools.js +39 -10
  9. package/dist/agents/dynamic-tools.js.map +1 -1
  10. package/dist/agents/sdk-backend.js +1 -1
  11. package/dist/agents/sdk-backend.js.map +1 -1
  12. package/dist/api-server.d.ts +9 -0
  13. package/dist/api-server.d.ts.map +1 -1
  14. package/dist/api-server.js +74 -3
  15. package/dist/api-server.js.map +1 -1
  16. package/dist/auto-checkpoint.d.ts.map +1 -1
  17. package/dist/auto-checkpoint.js +50 -17
  18. package/dist/auto-checkpoint.js.map +1 -1
  19. package/dist/auto-compressor.d.ts.map +1 -1
  20. package/dist/auto-compressor.js +9 -5
  21. package/dist/auto-compressor.js.map +1 -1
  22. package/dist/bin.d.ts +8 -0
  23. package/dist/bin.d.ts.map +1 -1
  24. package/dist/bin.js +59 -4
  25. package/dist/bin.js.map +1 -1
  26. package/dist/branching.d.ts.map +1 -1
  27. package/dist/branching.js +14 -1
  28. package/dist/branching.js.map +1 -1
  29. package/dist/checkpoint.d.ts.map +1 -1
  30. package/dist/checkpoint.js +13 -1
  31. package/dist/checkpoint.js.map +1 -1
  32. package/dist/cli/agent.d.ts.map +1 -1
  33. package/dist/cli/agent.js +19 -3
  34. package/dist/cli/agent.js.map +1 -1
  35. package/dist/cli/commands.d.ts.map +1 -1
  36. package/dist/cli/commands.js +99 -0
  37. package/dist/cli/commands.js.map +1 -1
  38. package/dist/cli/index.d.ts.map +1 -1
  39. package/dist/cli/index.js +32 -1
  40. package/dist/cli/index.js.map +1 -1
  41. package/dist/cli/types.js +1 -1
  42. package/dist/cli/types.js.map +1 -1
  43. package/dist/config.js +2 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/diff.d.ts.map +1 -1
  46. package/dist/diff.js +42 -4
  47. package/dist/diff.js.map +1 -1
  48. package/dist/errors.d.ts.map +1 -1
  49. package/dist/errors.js +30 -3
  50. package/dist/errors.js.map +1 -1
  51. package/dist/headless.d.ts.map +1 -1
  52. package/dist/headless.js +56 -2
  53. package/dist/headless.js.map +1 -1
  54. package/dist/hooks.d.ts +8 -2
  55. package/dist/hooks.d.ts.map +1 -1
  56. package/dist/hooks.js +97 -11
  57. package/dist/hooks.js.map +1 -1
  58. package/dist/idle-eviction.d.ts.map +1 -1
  59. package/dist/idle-eviction.js +8 -1
  60. package/dist/idle-eviction.js.map +1 -1
  61. package/dist/markdown.d.ts.map +1 -1
  62. package/dist/markdown.js +32 -10
  63. package/dist/markdown.js.map +1 -1
  64. package/dist/mcp.d.ts +35 -5
  65. package/dist/mcp.d.ts.map +1 -1
  66. package/dist/mcp.js +186 -12
  67. package/dist/mcp.js.map +1 -1
  68. package/dist/model-detection.d.ts +14 -1
  69. package/dist/model-detection.d.ts.map +1 -1
  70. package/dist/model-detection.js +307 -114
  71. package/dist/model-detection.js.map +1 -1
  72. package/dist/model-router.js +7 -7
  73. package/dist/model-router.js.map +1 -1
  74. package/dist/parallel-tools.d.ts +9 -1
  75. package/dist/parallel-tools.d.ts.map +1 -1
  76. package/dist/parallel-tools.js +6 -5
  77. package/dist/parallel-tools.js.map +1 -1
  78. package/dist/plugins.d.ts +37 -0
  79. package/dist/plugins.d.ts.map +1 -1
  80. package/dist/plugins.js +87 -0
  81. package/dist/plugins.js.map +1 -1
  82. package/dist/providers/anthropic.d.ts.map +1 -1
  83. package/dist/providers/anthropic.js +36 -2
  84. package/dist/providers/anthropic.js.map +1 -1
  85. package/dist/providers/bedrock.d.ts.map +1 -1
  86. package/dist/providers/bedrock.js +81 -17
  87. package/dist/providers/bedrock.js.map +1 -1
  88. package/dist/providers/index.d.ts.map +1 -1
  89. package/dist/providers/index.js +2 -0
  90. package/dist/providers/index.js.map +1 -1
  91. package/dist/providers/types.d.ts.map +1 -1
  92. package/dist/providers/types.js +19 -10
  93. package/dist/providers/types.js.map +1 -1
  94. package/dist/risk.d.ts.map +1 -1
  95. package/dist/risk.js +15 -5
  96. package/dist/risk.js.map +1 -1
  97. package/dist/sandbox-native.d.ts +1 -0
  98. package/dist/sandbox-native.d.ts.map +1 -1
  99. package/dist/sandbox-native.js +37 -5
  100. package/dist/sandbox-native.js.map +1 -1
  101. package/dist/scope.d.ts +10 -0
  102. package/dist/scope.d.ts.map +1 -1
  103. package/dist/scope.js +75 -15
  104. package/dist/scope.js.map +1 -1
  105. package/dist/scuttlebot/client.d.ts +83 -0
  106. package/dist/scuttlebot/client.d.ts.map +1 -0
  107. package/dist/scuttlebot/client.js +350 -0
  108. package/dist/scuttlebot/client.js.map +1 -0
  109. package/dist/scuttlebot/config.d.ts +28 -0
  110. package/dist/scuttlebot/config.d.ts.map +1 -0
  111. package/dist/scuttlebot/config.js +91 -0
  112. package/dist/scuttlebot/config.js.map +1 -0
  113. package/dist/scuttlebot/http-client.d.ts +63 -0
  114. package/dist/scuttlebot/http-client.d.ts.map +1 -0
  115. package/dist/scuttlebot/http-client.js +124 -0
  116. package/dist/scuttlebot/http-client.js.map +1 -0
  117. package/dist/scuttlebot/index.d.ts +13 -0
  118. package/dist/scuttlebot/index.d.ts.map +1 -0
  119. package/dist/scuttlebot/index.js +10 -0
  120. package/dist/scuttlebot/index.js.map +1 -0
  121. package/dist/scuttlebot/irc-client.d.ts +124 -0
  122. package/dist/scuttlebot/irc-client.d.ts.map +1 -0
  123. package/dist/scuttlebot/irc-client.js +599 -0
  124. package/dist/scuttlebot/irc-client.js.map +1 -0
  125. package/dist/skills.d.ts +19 -0
  126. package/dist/skills.d.ts.map +1 -1
  127. package/dist/skills.js +98 -10
  128. package/dist/skills.js.map +1 -1
  129. package/dist/smart-router.js +4 -4
  130. package/dist/smart-router.js.map +1 -1
  131. package/dist/storage.d.ts +0 -4
  132. package/dist/storage.d.ts.map +1 -1
  133. package/dist/storage.js +81 -5
  134. package/dist/storage.js.map +1 -1
  135. package/dist/tools.d.ts.map +1 -1
  136. package/dist/tools.js +232 -38
  137. package/dist/tools.js.map +1 -1
  138. package/dist/trust.d.ts +16 -3
  139. package/dist/trust.d.ts.map +1 -1
  140. package/dist/trust.js +23 -4
  141. package/dist/trust.js.map +1 -1
  142. package/dist/types.d.ts.map +1 -1
  143. package/dist/types.js +13 -4
  144. package/dist/types.js.map +1 -1
  145. package/dist/ui/agent.d.ts +1 -1
  146. package/dist/ui/agent.d.ts.map +1 -1
  147. package/dist/ui/agent.js +35 -44
  148. package/dist/ui/agent.js.map +1 -1
  149. package/dist/ui/chat-input.d.ts +3 -1
  150. package/dist/ui/chat-input.d.ts.map +1 -1
  151. package/dist/ui/chat-input.js +82 -17
  152. package/dist/ui/chat-input.js.map +1 -1
  153. package/dist/ui/commands.d.ts +2 -0
  154. package/dist/ui/commands.d.ts.map +1 -1
  155. package/dist/ui/commands.js +318 -10
  156. package/dist/ui/commands.js.map +1 -1
  157. package/dist/ui/index.d.ts.map +1 -1
  158. package/dist/ui/index.js +236 -46
  159. package/dist/ui/index.js.map +1 -1
  160. package/dist/ui/input-utils.d.ts +20 -0
  161. package/dist/ui/input-utils.d.ts.map +1 -0
  162. package/dist/ui/input-utils.js +35 -0
  163. package/dist/ui/input-utils.js.map +1 -0
  164. package/dist/ui/messages.d.ts +6 -2
  165. package/dist/ui/messages.d.ts.map +1 -1
  166. package/dist/ui/messages.js +42 -11
  167. package/dist/ui/messages.js.map +1 -1
  168. package/dist/ui/modals.d.ts +21 -1
  169. package/dist/ui/modals.d.ts.map +1 -1
  170. package/dist/ui/modals.js +67 -5
  171. package/dist/ui/modals.js.map +1 -1
  172. package/dist/ui/status-bar.d.ts +4 -1
  173. package/dist/ui/status-bar.d.ts.map +1 -1
  174. package/dist/ui/status-bar.js +12 -1
  175. package/dist/ui/status-bar.js.map +1 -1
  176. package/dist/ui/types.d.ts +3 -0
  177. package/dist/ui/types.d.ts.map +1 -1
  178. package/package.json +4 -7
  179. package/dist/completion.d.ts +0 -75
  180. package/dist/completion.d.ts.map +0 -1
  181. package/dist/completion.js +0 -234
  182. package/dist/completion.js.map +0 -1
  183. package/dist/keyboard.d.ts +0 -57
  184. package/dist/keyboard.d.ts.map +0 -1
  185. package/dist/keyboard.js +0 -265
  186. package/dist/keyboard.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwsCH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CA+DjD;AAED,wBAAsB,WAAW,CAAC,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBrH"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AA45CH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CA+DjD;AAED,wBAAsB,WAAW,CAAC,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBrH"}
package/dist/ui/index.js CHANGED
@@ -29,10 +29,11 @@ import * as recording from '../terminal-recording.js';
29
29
  import * as sessionTimeout from '../session-timeout.js';
30
30
  import * as idleEviction from '../idle-eviction.js';
31
31
  import { isTmux, getTmuxInfo } from '../tmux.js';
32
+ import { scuttlebotClient } from '../scuttlebot/index.js';
32
33
  import { ErrorBoundary } from './error-boundary.js';
33
34
  import { ThinkingDisplay, ProcessingIndicator, StreamingIndicator, StateTransition } from './components.js';
34
35
  import { MessageHistory } from './messages.js';
35
- import { ModelSelector, SessionSelector, UpgradePrompt, ComplexityWarning, SessionResumePrompt, KeybindingsModal, } from './modals.js';
36
+ import { ModelSelector, SessionSelector, UpgradePrompt, ComplexityWarning, SessionResumePrompt, KeybindingsModal, ProviderSelector, ApiKeySetup, } from './modals.js';
36
37
  import { ThemePicker } from './theme-picker.js';
37
38
  import { PackPicker } from './pack-picker.js';
38
39
  import { applyThemePack, getCurrentPack, getCompanionMode, getThemePack } from '../hud/theme-packs/api.js';
@@ -45,6 +46,23 @@ import { resolveIterationLimit } from '../iteration-limit.js';
45
46
  setEmojiConfig(config);
46
47
  // Module-level state for agterm mode
47
48
  let moduleAgtermEnabled = false;
49
+ let pendingRestartArgs = null;
50
+ function requestSelfRestart(args = process.argv.slice(1)) {
51
+ pendingRestartArgs = [...args];
52
+ }
53
+ async function spawnPendingRestart() {
54
+ if (!pendingRestartArgs) {
55
+ return;
56
+ }
57
+ const restartArgs = pendingRestartArgs;
58
+ pendingRestartArgs = null;
59
+ const { spawn } = await import('child_process');
60
+ const child = spawn(process.argv[0], restartArgs, {
61
+ stdio: 'inherit',
62
+ detached: true,
63
+ });
64
+ child.unref();
65
+ }
48
66
  // Debug logging for flow control issues
49
67
  let debugEnabled = process.env.CALLIOPE_DEBUG === '1';
50
68
  const debugLog = (label, ...args) => {
@@ -70,7 +88,18 @@ function TerminalChat() {
70
88
  return () => { process.stdout.off('resize', onResize); };
71
89
  }, [stdout]);
72
90
  // Core state
73
- const [input, setInput] = useState('');
91
+ // Input value is held primarily in a ref so that keystrokes don't force
92
+ // a full parent re-render (which cascades through modals, layouts, status bar
93
+ // and causes visible paint lag on every character). We only bump `inputVersion`
94
+ // when we need the parent to re-render with a programmatic value change
95
+ // (history nav, clear on submit) — keystrokes never trigger a parent render.
96
+ const inputRef = useRef('');
97
+ const [inputVersion, setInputVersion] = useState(0);
98
+ const setInputValue = useCallback((value) => {
99
+ inputRef.current = value;
100
+ setInputVersion(v => v + 1);
101
+ }, []);
102
+ const input = inputRef.current;
74
103
  const [suggestions, setSuggestions] = useState([]);
75
104
  const [messages, setMessages] = useState([]);
76
105
  const [isProcessing, setIsProcessing] = useState(false);
@@ -107,15 +136,17 @@ function TerminalChat() {
107
136
  }
108
137
  });
109
138
  const recentCommands = React.useMemo(() => inputHistory.filter(cmd => cmd.startsWith('/')).slice(-10), [inputHistory]);
110
- // Clear suggestions when input changes significantly
139
+ // Handle changes coming from the ChatInput. During typing, we update the
140
+ // ref only — ChatInput paints itself synchronously from its own ref, so we
141
+ // don't need to re-render the parent. We still want to clear stale slash
142
+ // suggestions and reset history navigation, but those setters bail out
143
+ // via functional updates when the value is already current.
111
144
  const handleInputChange = useCallback((newValue) => {
112
- setInput(newValue);
113
- // Clear suggestions if user clears input or submits
145
+ inputRef.current = newValue;
114
146
  if (!newValue || !newValue.startsWith('/')) {
115
- setSuggestions([]);
147
+ setSuggestions(prev => prev.length > 0 ? [] : prev);
116
148
  }
117
- // Reset history navigation when user types
118
- setHistoryIndex(-1);
149
+ setHistoryIndex(prev => prev === -1 ? prev : -1);
119
150
  }, []);
120
151
  // Navigate input history
121
152
  const navigateHistory = useCallback((direction) => {
@@ -124,13 +155,13 @@ function TerminalChat() {
124
155
  if (direction === 'up') {
125
156
  if (historyIndex === -1) {
126
157
  // Save current input before navigating
127
- setSavedInput(input);
158
+ setSavedInput(inputRef.current);
128
159
  setHistoryIndex(inputHistory.length - 1);
129
- setInput(inputHistory[inputHistory.length - 1]);
160
+ setInputValue(inputHistory[inputHistory.length - 1]);
130
161
  }
131
162
  else if (historyIndex > 0) {
132
163
  setHistoryIndex(historyIndex - 1);
133
- setInput(inputHistory[historyIndex - 1]);
164
+ setInputValue(inputHistory[historyIndex - 1]);
134
165
  }
135
166
  }
136
167
  else {
@@ -138,15 +169,15 @@ function TerminalChat() {
138
169
  return;
139
170
  if (historyIndex < inputHistory.length - 1) {
140
171
  setHistoryIndex(historyIndex + 1);
141
- setInput(inputHistory[historyIndex + 1]);
172
+ setInputValue(inputHistory[historyIndex + 1]);
142
173
  }
143
174
  else {
144
175
  // Return to saved input
145
176
  setHistoryIndex(-1);
146
- setInput(savedInput);
177
+ setInputValue(savedInput);
147
178
  }
148
179
  }
149
- }, [inputHistory, historyIndex, input, savedInput]);
180
+ }, [inputHistory, historyIndex, savedInput, setInputValue]);
150
181
  // Add to history when submitting
151
182
  const addToHistory = useCallback((value) => {
152
183
  if (value.trim() && (inputHistory.length === 0 || inputHistory[inputHistory.length - 1] !== value)) {
@@ -171,6 +202,8 @@ function TerminalChat() {
171
202
  }));
172
203
  // Modal state
173
204
  const [modalMode, setModalMode] = useState('none');
205
+ const [providerEntries, setProviderEntries] = useState([]);
206
+ const [pendingSetupProvider, setPendingSetupProvider] = useState(null);
174
207
  const [pendingComplexPrompt, setPendingComplexPrompt] = useState(null);
175
208
  const [previousSession, setPreviousSession] = useState(null);
176
209
  const [pendingToolCall, setPendingToolCall] = useState(null);
@@ -194,6 +227,14 @@ function TerminalChat() {
194
227
  useEffect(() => {
195
228
  queuedMessagesRef.current = queuedMessages;
196
229
  }, [queuedMessages]);
230
+ // Refs for scuttlebot polling — avoids stale closures across re-renders
231
+ const isProcessingRef = useRef(false);
232
+ const handleSubmitRef = useRef(async () => { });
233
+ const openProviderPickerRef = useRef(null);
234
+ // Keep isProcessingRef in sync
235
+ useEffect(() => {
236
+ isProcessingRef.current = isProcessing;
237
+ }, [isProcessing]);
197
238
  // Undo/Redo history - stores snapshots of conversation state
198
239
  const undoStack = useRef([]);
199
240
  const redoStack = useRef([]);
@@ -299,21 +340,8 @@ function TerminalChat() {
299
340
  // Initialize session and load memory on mount
300
341
  useEffect(() => {
301
342
  const cwd = process.cwd();
302
- // Check for existing session with messages
303
- const existingSessions = storage.listSessions(5);
304
- const recentSession = existingSessions.find(s => s.projectPath === cwd &&
305
- s.messageCount > 0 &&
306
- Date.now() - new Date(s.lastAccessedAt).getTime() < 24 * 60 * 60 * 1000 // Within 24 hours
307
- );
308
- if (recentSession && !sessionRef.current) {
309
- // Offer to resume
310
- setPreviousSession({
311
- projectName: recentSession.projectName,
312
- lastAccessedAt: recentSession.lastAccessedAt,
313
- messageCount: recentSession.messageCount,
314
- });
315
- setModalMode('session-resume');
316
- }
343
+ // Always start fresh session - skip resume dialog
344
+ // Note: Previous session data is still available via storage APIs if needed
317
345
  const session = storage.getOrCreateSession(cwd);
318
346
  sessionRef.current = session;
319
347
  ledgerRef.current.setRetentionLimit(config.get('sessionLogLimit') ?? 0);
@@ -355,6 +383,28 @@ function TerminalChat() {
355
383
  model: model || DEFAULT_MODELS[selectProvider(provider)],
356
384
  cwd: cwdMem,
357
385
  });
386
+ // Initialize scuttlebot integration
387
+ scuttlebotClient.initialize(session.id, cwdMem).then((enabled) => {
388
+ if (enabled) {
389
+ const status = scuttlebotClient.getStatus();
390
+ debugLog('scuttlebot', `enabled, nick=${status.nick}, irc=${status.config?.ircAddr}`);
391
+ // Show nick in system messages so operators know how to address calliope
392
+ addMessage('system', `IRC connected — address me as: ${status.nick}`);
393
+ scuttlebotClient.postOnline().catch(() => { });
394
+ scuttlebotClient.postMessage(`connected — address me as: ${status.nick}`).catch(() => { });
395
+ // Route incoming IRC instructions into the agent loop
396
+ scuttlebotClient.startPolling((instruction) => {
397
+ if (isProcessingRef.current) {
398
+ setQueuedMessages(prev => [...prev, instruction]);
399
+ }
400
+ else {
401
+ void handleSubmitRef.current(instruction);
402
+ }
403
+ });
404
+ }
405
+ }).catch((err) => {
406
+ debugLog('scuttlebot', 'initialization failed:', err instanceof Error ? err.message : err);
407
+ });
358
408
  // Configure session timeout (opt-in via config)
359
409
  const timeoutMs = config.get('sessionTimeoutMs');
360
410
  if (timeoutMs) {
@@ -406,11 +456,15 @@ function TerminalChat() {
406
456
  const actualModel = model || DEFAULT_MODELS[actualProvider];
407
457
  const isModalActive = modalMode !== 'none';
408
458
  // Add message helper
409
- const addMessage = useCallback((type, content) => {
459
+ const addMessage = useCallback((type, content, isError) => {
410
460
  setMessages(prev => [...prev, {
411
461
  id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
412
462
  type,
413
- content
463
+ content,
464
+ // isError is plumbed through to the renderer (messages.tsx) so the tool
465
+ // status icon is driven by the authoritative executeTool flag rather than
466
+ // string-matching the output. Omitted (undefined) for non-tool messages.
467
+ ...(isError !== undefined ? { isError } : {}),
414
468
  }]);
415
469
  // Persist user and assistant messages to storage for session history
416
470
  if (type === 'user' || type === 'assistant') {
@@ -537,7 +591,7 @@ function TerminalChat() {
537
591
  setThinkingState,
538
592
  setStreamingResponse,
539
593
  setQueuedMessages,
540
- setInput,
594
+ setInput: setInputValue,
541
595
  setBookmarks,
542
596
  setTemplates,
543
597
  setContextTokens,
@@ -555,6 +609,17 @@ function TerminalChat() {
555
609
  runAgent,
556
610
  runLoop,
557
611
  exit,
612
+ startScuttlebotPolling: () => {
613
+ scuttlebotClient.startPolling((instruction) => {
614
+ if (isProcessingRef.current) {
615
+ setQueuedMessages(prev => [...prev, instruction]);
616
+ }
617
+ else {
618
+ void handleSubmitRef.current(instruction);
619
+ }
620
+ });
621
+ },
622
+ openProviderPicker: () => openProviderPickerRef.current?.(),
558
623
  }), [actualProvider, actualModel, provider, model, persona, mode, confirmMode, autoRoute, smartRouteActive,
559
624
  layout, density, collapseSettings, messages, stats, loopActive, isProcessing,
560
625
  thinkingState, streamingResponse, queuedMessages, bookmarks, templates, modalMode,
@@ -575,7 +640,7 @@ function TerminalChat() {
575
640
  recording.recordEvent('input', trimmed);
576
641
  // Add to history for up/down arrow navigation
577
642
  addToHistory(trimmed);
578
- setInput('');
643
+ setInputValue('');
579
644
  if (trimmed.startsWith('/')) {
580
645
  await handleCommandWrapped(trimmed);
581
646
  return;
@@ -626,6 +691,10 @@ function TerminalChat() {
626
691
  else {
627
692
  addMessage('user', trimmed);
628
693
  }
694
+ // Mirror user input to IRC so observers see what prompted each agent run
695
+ if (scuttlebotClient.isEnabled()) {
696
+ scuttlebotClient.postMessage(cleanText || trimmed).catch(() => { });
697
+ }
629
698
  setIsProcessing(true);
630
699
  try {
631
700
  // Build message content (with file/image support)
@@ -649,7 +718,11 @@ function TerminalChat() {
649
718
  setThinkingState(null);
650
719
  setStreamingResponse('');
651
720
  }
652
- }, [isProcessing, handleCommandWrapped, runAgent, addMessage, provider, model, saveUndoState, addToHistory, mode]);
721
+ }, [isProcessing, handleCommandWrapped, runAgent, addMessage, provider, model, saveUndoState, addToHistory, mode, setInputValue]);
722
+ // Keep handleSubmitRef current so scuttlebot polling never captures a stale closure
723
+ useEffect(() => {
724
+ handleSubmitRef.current = handleSubmit;
725
+ }, [handleSubmit]);
653
726
  // Modal handlers
654
727
  const handleModelSelect = useCallback((selectedModel) => {
655
728
  setModel(selectedModel);
@@ -670,13 +743,9 @@ function TerminalChat() {
670
743
  const success = await performUpgrade();
671
744
  if (success) {
672
745
  addMessage('system', 'Upgrade complete! Restarting...');
673
- const { spawn } = await import('child_process');
674
- const child = spawn(process.argv[0], process.argv.slice(1), {
675
- stdio: 'inherit',
676
- detached: true,
677
- });
678
- child.unref();
679
- process.exit(0);
746
+ requestSelfRestart(process.argv.slice(1));
747
+ exit();
748
+ return;
680
749
  }
681
750
  else {
682
751
  addMessage('error', 'Upgrade failed. Try: npm install -g @calliopelabs/cli@latest');
@@ -705,7 +774,7 @@ function TerminalChat() {
705
774
  setStreamingResponse('');
706
775
  setLoopActive(false);
707
776
  setEditingQueueIndex(null);
708
- addMessage('system', '⏹ Operation cancelled. Use /exit to quit.');
777
+ addMessage('system', '⏹ Operation cancelled. Press Ctrl+C again to quit.');
709
778
  }
710
779
  else if (modalMode !== 'none') {
711
780
  // Close any open modal
@@ -713,10 +782,113 @@ function TerminalChat() {
713
782
  setPendingComplexPrompt(null);
714
783
  }
715
784
  else {
716
- // Not processing - show hint instead of exiting
717
- addMessage('system', '💡 Use /exit to quit, or Ctrl+C.');
785
+ // Not processing - show hint. Second Ctrl+C within 2s will actually exit.
786
+ addMessage('system', '💡 Press Ctrl+C again to quit, or /exit.');
718
787
  }
719
788
  }, [isProcessing, modalMode, addMessage]);
789
+ const handleExit = useCallback(() => {
790
+ exit();
791
+ }, [exit]);
792
+ // Build the list of providers with their configuration status for the picker.
793
+ // Ordering: Ollama first (recommended onboarding), then others by rough popularity.
794
+ const buildProviderEntries = useCallback(() => {
795
+ const hasBedrock = !!(config.getApiKey('bedrock') || config.getBaseUrl('bedrock')
796
+ || process.env.AWS_ACCESS_KEY_ID || process.env.AWS_PROFILE);
797
+ const entries = [
798
+ { id: 'ollama', label: 'Ollama', configured: !!config.getBaseUrl('ollama'), configHint: 'OLLAMA_BASE_URL', recommended: true, note: 'local, free' },
799
+ { id: 'anthropic', label: 'Anthropic', configured: !!config.getApiKey('anthropic'), configHint: 'ANTHROPIC_API_KEY' },
800
+ { id: 'openai', label: 'OpenAI', configured: !!config.getApiKey('openai'), configHint: 'OPENAI_API_KEY' },
801
+ { id: 'google', label: 'Google', configured: !!config.getApiKey('google'), configHint: 'GOOGLE_API_KEY' },
802
+ { id: 'mistral', label: 'Mistral', configured: !!config.getApiKey('mistral'), configHint: 'MISTRAL_API_KEY' },
803
+ { id: 'openrouter', label: 'OpenRouter', configured: !!config.getApiKey('openrouter'), configHint: 'OPENROUTER_API_KEY' },
804
+ { id: 'together', label: 'Together', configured: !!config.getApiKey('together'), configHint: 'TOGETHER_API_KEY' },
805
+ { id: 'groq', label: 'Groq', configured: !!config.getApiKey('groq'), configHint: 'GROQ_API_KEY' },
806
+ { id: 'fireworks', label: 'Fireworks', configured: !!config.getApiKey('fireworks'), configHint: 'FIREWORKS_API_KEY' },
807
+ { id: 'ai21', label: 'AI21', configured: !!config.getApiKey('ai21'), configHint: 'AI21_API_KEY' },
808
+ { id: 'huggingface', label: 'HuggingFace', configured: !!config.getApiKey('huggingface'), configHint: 'HUGGINGFACE_API_KEY' },
809
+ { id: 'bedrock', label: 'AWS Bedrock', configured: hasBedrock, configHint: 'AWS_PROFILE or AWS_ACCESS_KEY_ID', note: 'AWS credentials' },
810
+ { id: 'litellm', label: 'LiteLLM', configured: !!config.getBaseUrl('litellm'), configHint: 'LITELLM_BASE_URL' },
811
+ ];
812
+ return entries;
813
+ }, []);
814
+ const openProviderPicker = useCallback(() => {
815
+ setProviderEntries(buildProviderEntries());
816
+ setModalMode('provider');
817
+ }, [buildProviderEntries]);
818
+ const handleProviderSelect = useCallback((entry) => {
819
+ if (entry.configured) {
820
+ setProvider(entry.id);
821
+ addMessage('system', `Provider: ${entry.label}${entry.id === 'ollama' ? ' (local)' : ''}`);
822
+ setModalMode('none');
823
+ setProviderEntries([]);
824
+ return;
825
+ }
826
+ // Not configured — open inline setup.
827
+ setPendingSetupProvider(entry);
828
+ setModalMode('api-key-setup');
829
+ }, [addMessage, setProvider]);
830
+ const handleApiKeySubmit = useCallback((value) => {
831
+ const entry = pendingSetupProvider;
832
+ if (!entry) {
833
+ setModalMode('none');
834
+ return;
835
+ }
836
+ try {
837
+ // Map provider → config key. Bedrock/Ollama/LiteLLM are special (base URL or AWS).
838
+ if (entry.id === 'ollama') {
839
+ config.set('ollamaBaseUrl', value);
840
+ }
841
+ else if (entry.id === 'litellm') {
842
+ config.set('litellmBaseUrl', value);
843
+ }
844
+ else if (entry.id === 'bedrock') {
845
+ // Simplest path: user enters AWS_PROFILE name. Write to env for this session;
846
+ // persistence is user's responsibility (profile lives in ~/.aws).
847
+ process.env.AWS_PROFILE = value;
848
+ // Clear any stale env-var credentials (e.g. from a prior `aws sso login`
849
+ // export that's since expired) so the profile actually wins.
850
+ delete process.env.AWS_ACCESS_KEY_ID;
851
+ delete process.env.AWS_SECRET_ACCESS_KEY;
852
+ delete process.env.AWS_SESSION_TOKEN;
853
+ addMessage('system', `AWS_PROFILE=${value} set for this session. Add to shell rc to persist.`);
854
+ }
855
+ else {
856
+ const keyMap = {
857
+ anthropic: 'anthropicApiKey',
858
+ openai: 'openaiApiKey',
859
+ google: 'googleApiKey',
860
+ mistral: 'mistralApiKey',
861
+ openrouter: 'openrouterApiKey',
862
+ together: 'togetherApiKey',
863
+ groq: 'groqApiKey',
864
+ fireworks: 'fireworksApiKey',
865
+ ai21: 'ai21ApiKey',
866
+ huggingface: 'huggingfaceApiKey',
867
+ };
868
+ const configKey = keyMap[entry.id];
869
+ if (!configKey)
870
+ throw new Error(`No config mapping for ${entry.id}`);
871
+ // Cast through any — the mapping above guarantees a valid ApiKey field.
872
+ config.set(configKey, value);
873
+ }
874
+ setProvider(entry.id);
875
+ addMessage('system', `✓ Configured ${entry.label}. Provider switched.`);
876
+ }
877
+ catch (e) {
878
+ addMessage('error', `Failed to configure ${entry.label}: ${e instanceof Error ? e.message : String(e)}`);
879
+ }
880
+ setPendingSetupProvider(null);
881
+ setModalMode('none');
882
+ }, [pendingSetupProvider, addMessage, setProvider]);
883
+ const handleApiKeyCancel = useCallback(() => {
884
+ setPendingSetupProvider(null);
885
+ setModalMode('none');
886
+ }, []);
887
+ // Forward-declared ref so buildCommandContext (defined earlier) can open the
888
+ // provider picker without a TDZ on the openProviderPicker callback.
889
+ useEffect(() => {
890
+ openProviderPickerRef.current = openProviderPicker;
891
+ }, [openProviderPicker]);
720
892
  // Handle direct send (Shift+Enter) - interrupts current operation and sends immediately
721
893
  const handleDirectSend = useCallback((msg) => {
722
894
  // Stop current processing
@@ -830,7 +1002,16 @@ function TerminalChat() {
830
1002
  setModalMode('none');
831
1003
  setPendingComplexPrompt(null);
832
1004
  addMessage('system', 'Operation cancelled.');
833
- } })), modalMode === 'keys' && (_jsx(KeybindingsModal, { onClose: () => setModalMode('none') })), modalMode === 'theme-picker' && (_jsx(ThemePicker, { currentLayout: layout, currentSkin: getCurrentSkin().name, currentPalette: getCurrentPalette().name, currentCompanion: getCurrentCompanion().name, onApply: (selection) => {
1005
+ } })), modalMode === 'keys' && (_jsx(KeybindingsModal, { onClose: () => setModalMode('none') })), modalMode === 'provider' && providerEntries.length > 0 && (_jsx(ProviderSelector, { providers: providerEntries, onSelect: handleProviderSelect, onCancel: () => {
1006
+ setModalMode('none');
1007
+ setProviderEntries([]);
1008
+ } })), modalMode === 'api-key-setup' && pendingSetupProvider && (_jsx(ApiKeySetup, { provider: pendingSetupProvider.id, configHint: pendingSetupProvider.configHint, onSubmit: handleApiKeySubmit, onCancel: handleApiKeyCancel, extraInstructions: pendingSetupProvider.id === 'ollama'
1009
+ ? 'e.g. http://localhost:11434 (start with: ollama serve)'
1010
+ : pendingSetupProvider.id === 'bedrock'
1011
+ ? 'Enter your AWS profile name. Ensure it exists in ~/.aws/credentials or ~/.aws/config.'
1012
+ : pendingSetupProvider.id === 'litellm'
1013
+ ? 'e.g. http://localhost:4000'
1014
+ : undefined })), modalMode === 'theme-picker' && (_jsx(ThemePicker, { currentLayout: layout, currentSkin: getCurrentSkin().name, currentPalette: getCurrentPalette().name, currentCompanion: getCurrentCompanion().name, onApply: (selection) => {
834
1015
  // Apply all selections
835
1016
  setLayout(selection.layout);
836
1017
  config.set('layout', selection.layout);
@@ -874,7 +1055,7 @@ function TerminalChat() {
874
1055
  ` Skin: ${pack.skin.name}, Palette: ${pack.palette.name}, Companion: ${companion.name}\n` +
875
1056
  ` "${companion.greeting}"`);
876
1057
  }
877
- }, onCancel: () => setModalMode('none') })), _jsx(ChatInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onEscape: handleEscape, onCycleMode: cycleMode, disabled: isModalActive, isProcessing: isProcessing, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, editingQueueIndex: editingQueueIndex, onQueueMessage: (msg) => {
1058
+ }, onCancel: () => setModalMode('none') })), _jsx(ChatInput, { value: input, valueVersion: inputVersion, onChange: handleInputChange, onSubmit: handleSubmit, onEscape: handleEscape, onExit: handleExit, onCycleMode: cycleMode, disabled: isModalActive, isProcessing: isProcessing, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, editingQueueIndex: editingQueueIndex, onQueueMessage: (msg) => {
878
1059
  setQueuedMessages(prev => [...prev, msg]);
879
1060
  addMessage('system', `📨 Queued: "${msg.substring(0, 50)}${msg.length > 50 ? '...' : ''}"`);
880
1061
  }, onEditQueuedMessage: handleEditQueuedMessage, onSetEditingQueueIndex: setEditingQueueIndex, onDirectSend: handleDirectSend, cwd: process.cwd(), suggestions: suggestions, onSuggestionsChange: setSuggestions, onNavigateHistory: navigateHistory,
@@ -959,8 +1140,17 @@ export async function startInkCLI(options = {}) {
959
1140
  });
960
1141
  await waitUntilExit();
961
1142
  // Session cleanup
1143
+ if (scuttlebotClient.isEnabled()) {
1144
+ await scuttlebotClient.postOffline().catch(() => {
1145
+ // Silent fail
1146
+ });
1147
+ await scuttlebotClient.disconnect().catch(() => {
1148
+ // Silent fail
1149
+ });
1150
+ }
962
1151
  recording.stopRecording();
963
1152
  sessionTimeout.clearTimers();
964
1153
  idleEviction.stopMonitor();
1154
+ await spawnPendingRestart();
965
1155
  }
966
1156
  //# sourceMappingURL=index.js.map