protoagent 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -61,7 +61,6 @@ protoagent init --project --force
61
61
  ## Interactive Commands
62
62
 
63
63
  - `/help` — Show available slash commands
64
- - `/clear` — Start a fresh conversation in a new session
65
64
  - `/collapse` — Collapse long system and tool output
66
65
  - `/expand` — Expand collapsed messages
67
66
  - `/quit` or `/exit` — Save and exit
package/dist/App.js CHANGED
@@ -43,8 +43,7 @@ Here's how the terminal UI is laid out (showcasing all options at once for demon
43
43
  ├─────────────────────────────────────────┤
44
44
  │ tokens: 1234↓ 56↑ | ctx: 12% | $0.02 │ static-ish, updates after each turn
45
45
  ├─────────────────────────────────────────┤
46
- │ /clear Clear conversation... │ dynamic, shown when typing /
47
- │ /quit — Exit ProtoAgent │
46
+ │ /quit Exit ProtoAgent │ dynamic, shown when typing /
48
47
  ├─────────────────────────────────────────┤
49
48
  │ ⠹ Running read_file... │ dynamic, shown while loading
50
49
  ├─────────────────────────────────────────┤
@@ -59,17 +58,17 @@ Here's how the terminal UI is laid out (showcasing all options at once for demon
59
58
  import { useState, useEffect, useCallback, useRef } from 'react';
60
59
  import { Box, Text, Static, useApp, useInput, useStdout } from 'ink';
61
60
  import { LeftBar } from './components/LeftBar.js';
62
- import { TextInput, Select, PasswordInput } from '@inkjs/ui';
61
+ import { TextInput, Select } from '@inkjs/ui';
63
62
  import { OpenAI } from 'openai';
64
- import { readConfig, writeConfig, resolveApiKey } from './config.js';
65
- import { loadRuntimeConfig } from './runtime-config.js';
66
- import { getAllProviders, getProvider, getModelPricing, getRequestDefaultParams } from './providers.js';
63
+ import { readConfig, writeConfig, writeInitConfig, resolveApiKey, TargetSelection, ModelSelection, ApiKeyInput } from './config.js';
64
+ import { loadRuntimeConfig, getActiveRuntimeConfigPath } from './runtime-config.js';
65
+ import { getProvider, getModelPricing, getRequestDefaultParams } from './providers.js';
67
66
  import { runAgenticLoop, initializeMessages, } from './agentic-loop.js';
68
- import { setDangerouslyAcceptAll, setApprovalHandler, clearApprovalHandler } from './tools/index.js';
67
+ import { setDangerouslySkipPermissions, setApprovalHandler, clearApprovalHandler } from './tools/index.js';
69
68
  import { setLogLevel, LogLevel, initLogFile, logger } from './utils/logger.js';
70
69
  import { createSession, ensureSystemPromptAtTop, saveSession, loadSession, generateTitle, } from './sessions.js';
71
70
  import { clearTodos, getTodosForSession, setTodosForSession } from './tools/todo.js';
72
- import { initializeMcp, closeMcp } from './mcp.js';
71
+ import { initializeMcp, closeMcp, getConnectedMcpServers } from './mcp.js';
73
72
  import { generateSystemPrompt } from './system-prompt.js';
74
73
  // ─── Scrollback helpers ───
75
74
  // These functions append text to the permanent scrollback buffer via the
@@ -84,16 +83,25 @@ function printBanner(addStatic) {
84
83
  '',
85
84
  ].join('\n'));
86
85
  }
87
- function printRuntimeHeader(addStatic, config, session, logFilePath, dangerouslyAcceptAll) {
86
+ function printRuntimeHeader(addStatic, config, session, dangerouslySkipPermissions) {
88
87
  const provider = getProvider(config.provider);
89
88
  let line = `Model: ${provider?.name || config.provider} / ${config.model}`;
90
- if (dangerouslyAcceptAll)
89
+ if (dangerouslySkipPermissions)
91
90
  line += ' (auto-approve all)';
92
91
  if (session)
93
- line += ` | Session: ${session.id.slice(0, 8)}`;
94
- let text = `${line}\n`;
92
+ line += ` | Session: ${session.id}`;
93
+ let text = `\x1b[2m${line}\x1b[0m\n`;
94
+ const logFilePath = logger.getLogFilePath();
95
95
  if (logFilePath) {
96
- text += `Debug logs: ${logFilePath}\n`;
96
+ text += `\x1b[2mDebug logs: ${logFilePath}\x1b[0m\n`;
97
+ }
98
+ const configPath = getActiveRuntimeConfigPath();
99
+ if (configPath) {
100
+ text += `\x1b[2mConfig file: ${configPath}\x1b[0m\n`;
101
+ }
102
+ const mcpServers = getConnectedMcpServers();
103
+ if (mcpServers.length > 0) {
104
+ text += `\x1b[2mMCPs: ${mcpServers.join(', ')}\x1b[0m\n`;
97
105
  }
98
106
  text += '\n';
99
107
  addStatic(text);
@@ -119,6 +127,63 @@ function printMessageToScrollback(addStatic, role, text) {
119
127
  }
120
128
  addStatic(`${normalized}\n\n`);
121
129
  }
130
+ /**
131
+ * Format a sub-agent tool call into a human-readable activity string.
132
+ * Shows what the sub-agent is actually doing, e.g. "Sub-agent reading file package.json"
133
+ */
134
+ function formatSubAgentActivity(tool, args) {
135
+ if (!args || typeof args !== 'object') {
136
+ return `Sub-agent running ${tool}...`;
137
+ }
138
+ const argEntries = Object.entries(args);
139
+ if (argEntries.length === 0) {
140
+ return `Sub-agent running ${tool}...`;
141
+ }
142
+ // Extract the most meaningful argument based on the tool
143
+ let detail = '';
144
+ const firstValue = argEntries[0]?.[1];
145
+ switch (tool) {
146
+ case 'read_file':
147
+ detail = typeof args.file_path === 'string' ? args.file_path : '';
148
+ break;
149
+ case 'write_file':
150
+ detail = typeof args.file_path === 'string' ? args.file_path : '';
151
+ break;
152
+ case 'edit_file':
153
+ detail = typeof args.file_path === 'string' ? args.file_path : '';
154
+ break;
155
+ case 'list_directory':
156
+ detail = typeof args.directory_path === 'string' ? args.directory_path : '(current)';
157
+ break;
158
+ case 'search_files':
159
+ detail = typeof args.search_term === 'string' ? `"${args.search_term}"` : '';
160
+ break;
161
+ case 'bash':
162
+ detail = typeof args.command === 'string'
163
+ ? args.command.split(/\s+/).slice(0, 3).join(' ') + (args.command.split(/\s+/).length > 3 ? '...' : '')
164
+ : '';
165
+ break;
166
+ case 'todo_write':
167
+ detail = Array.isArray(args.todos) ? `${args.todos.length} task(s)` : '';
168
+ break;
169
+ case 'webfetch':
170
+ detail = typeof args.url === 'string' ? new URL(args.url).hostname : '';
171
+ break;
172
+ case 'sub_agent':
173
+ // Nested sub-agent
174
+ detail = 'nested task...';
175
+ break;
176
+ default:
177
+ // Use the first argument value as fallback
178
+ detail = typeof firstValue === 'string'
179
+ ? firstValue.length > 30 ? firstValue.slice(0, 30) + '...' : firstValue
180
+ : '';
181
+ }
182
+ if (detail) {
183
+ return `Sub-agent ${tool.replace(/_/g, ' ')}: ${detail}`;
184
+ }
185
+ return `Sub-agent running ${tool}...`;
186
+ }
122
187
  function replayMessagesToScrollback(addStatic, messages) {
123
188
  for (const message of messages) {
124
189
  const msgAny = message;
@@ -154,7 +219,6 @@ function clipToRows(text, terminalRows) {
154
219
  }
155
220
  // ─── Available slash commands ───
156
221
  const SLASH_COMMANDS = [
157
- { name: '/clear', description: 'Clear conversation and start fresh' },
158
222
  { name: '/help', description: 'Show all available commands' },
159
223
  { name: '/quit', description: 'Exit ProtoAgent' },
160
224
  { name: '/exit', description: 'Alias for /quit' },
@@ -162,7 +226,6 @@ const SLASH_COMMANDS = [
162
226
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
163
227
  const HELP_TEXT = [
164
228
  'Commands:',
165
- ' /clear - Clear conversation and start fresh',
166
229
  ' /help - Show this help',
167
230
  ' /quit - Exit ProtoAgent',
168
231
  ' /exit - Alias for /quit',
@@ -234,44 +297,37 @@ const ApprovalPrompt = ({ request, onRespond }) => {
234
297
  const UsageDisplay = ({ usage, totalCost }) => {
235
298
  if (!usage && totalCost === 0)
236
299
  return null;
237
- return (_jsxs(Box, { marginTop: 1, children: [usage && (_jsxs(Box, { children: [_jsxs(Box, { backgroundColor: "#064e3b", paddingX: 1, children: [_jsx(Text, { color: "black", children: "tokens: " }), _jsxs(Text, { color: "black", bold: true, children: [usage.inputTokens, "\u2193 ", usage.outputTokens, "\u2191"] })] }), _jsxs(Box, { backgroundColor: "#065f46", paddingX: 1, children: [_jsx(Text, { color: "black", children: "ctx: " }), _jsxs(Text, { color: "black", bold: true, children: [usage.contextPercent.toFixed(0), "%"] })] })] })), totalCost > 0 && (_jsxs(Box, { backgroundColor: "#064e3b", paddingX: 1, children: [_jsx(Text, { color: "black", children: "cost: " }), _jsxs(Text, { color: "black", bold: true, children: ["$", totalCost.toFixed(4)] })] }))] }));
300
+ return (_jsxs(Box, { marginTop: 1, children: [usage && (_jsxs(Box, { children: [_jsxs(Box, { backgroundColor: "#064e3b", paddingX: 1, children: [_jsx(Text, { color: "white", children: "tokens: " }), _jsxs(Text, { color: "white", bold: true, children: [usage.inputTokens, "\u2193 ", usage.outputTokens, "\u2191"] })] }), _jsxs(Box, { backgroundColor: "#065f46", paddingX: 1, children: [_jsx(Text, { color: "white", children: "ctx: " }), _jsxs(Text, { color: "white", bold: true, children: [usage.contextPercent.toFixed(0), "%"] })] })] })), totalCost > 0 && (_jsxs(Box, { backgroundColor: "#064e3b", paddingX: 1, children: [_jsx(Text, { color: "black", children: "cost: " }), _jsxs(Text, { color: "black", bold: true, children: ["$", totalCost.toFixed(4)] })] }))] }));
238
301
  };
239
302
  /** Inline setup wizard — shown when no config exists. */
240
303
  const InlineSetup = ({ onComplete }) => {
241
- const [setupStep, setSetupStep] = useState('provider');
304
+ const [setupStep, setSetupStep] = useState('target');
305
+ const [target, setTarget] = useState('project');
242
306
  const [selectedProviderId, setSelectedProviderId] = useState('');
243
307
  const [selectedModelId, setSelectedModelId] = useState('');
244
- const [apiKeyError, setApiKeyError] = useState('');
245
- const providerItems = getAllProviders().flatMap((provider) => provider.models.map((model) => ({
246
- label: `${provider.name} - ${model.name}`,
247
- value: `${provider.id}:::${model.id}`,
248
- })));
308
+ const handleModelSelect = (providerId, modelId) => {
309
+ setSelectedProviderId(providerId);
310
+ setSelectedModelId(modelId);
311
+ setSetupStep('api_key');
312
+ };
313
+ const handleConfigComplete = (config) => {
314
+ writeInitConfig(target);
315
+ writeConfig(config, target);
316
+ onComplete(config);
317
+ };
318
+ if (setupStep === 'target') {
319
+ return (_jsx(Box, { marginTop: 1, children: _jsx(TargetSelection, { title: "First-time setup", subtitle: "Create a ProtoAgent runtime config:", onSelect: (value) => {
320
+ setTarget(value);
321
+ setSetupStep('provider');
322
+ } }) }));
323
+ }
249
324
  if (setupStep === 'provider') {
250
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "First-time setup" }), _jsx(Text, { dimColor: true, children: "Select a provider and model:" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: providerItems.map((item) => ({ value: item.value, label: item.label })), onChange: (value) => {
251
- const [providerId, modelId] = value.split(':::');
252
- setSelectedProviderId(providerId);
253
- setSelectedModelId(modelId);
254
- setSetupStep('api_key');
255
- } }) })] }));
325
+ return (_jsx(Box, { marginTop: 1, children: _jsx(ModelSelection, { setSelectedProviderId: setSelectedProviderId, setSelectedModelId: setSelectedModelId, onSelect: handleModelSelect, title: "First-time setup" }) }));
256
326
  }
257
- const provider = getProvider(selectedProviderId);
258
- const hasResolvedAuth = Boolean(resolveApiKey({ provider: selectedProviderId, apiKey: undefined }));
259
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "First-time setup" }), _jsxs(Text, { dimColor: true, children: ["Selected: ", provider?.name, " / ", selectedModelId] }), _jsx(Text, { children: hasResolvedAuth ? 'Optional API key:' : 'Enter your API key:' }), apiKeyError && _jsx(Text, { color: "red", children: apiKeyError }), _jsx(PasswordInput, { placeholder: hasResolvedAuth ? 'Press enter to keep resolved auth' : `Paste your ${provider?.apiKeyEnvVar || 'API'} key`, onSubmit: (value) => {
260
- if (value.trim().length === 0 && !hasResolvedAuth) {
261
- setApiKeyError('API key cannot be empty.');
262
- return;
263
- }
264
- const newConfig = {
265
- provider: selectedProviderId,
266
- model: selectedModelId,
267
- ...(value.trim().length > 0 ? { apiKey: value.trim() } : {}),
268
- };
269
- writeConfig(newConfig, 'project');
270
- onComplete(newConfig);
271
- } })] }));
327
+ return (_jsx(Box, { marginTop: 1, children: _jsx(ApiKeyInput, { selectedProviderId: selectedProviderId, selectedModelId: selectedModelId, target: target, title: "First-time setup", showProviderHeaders: false, onComplete: handleConfigComplete }) }));
272
328
  };
273
329
  // ─── Main App ───
274
- export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
330
+ export const App = ({ dangerouslySkipPermissions = false, logLevel, sessionId, }) => {
275
331
  const { exit } = useApp();
276
332
  const { stdout } = useStdout();
277
333
  const terminalRows = stdout?.rows ?? 24;
@@ -309,7 +365,6 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
309
365
  const [threadErrors, setThreadErrors] = useState([]);
310
366
  const [initialized, setInitialized] = useState(false);
311
367
  const [needsSetup, setNeedsSetup] = useState(false);
312
- const [logFilePath, setLogFilePath] = useState(null);
313
368
  // Input reset key — incremented on submit to force TextInput remount and clear
314
369
  const [inputResetKey, setInputResetKey] = useState(0);
315
370
  // Approval state
@@ -331,7 +386,6 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
331
386
  const abortControllerRef = useRef(null);
332
387
  const didPrintIntroRef = useRef(false);
333
388
  const printedThreadErrorIdsRef = useRef(new Set());
334
- const printedLogPathRef = useRef(null);
335
389
  // ─── Post-config initialization (reused after inline setup) ───
336
390
  const initializeWithConfig = useCallback(async (loadedConfig) => {
337
391
  setConfig(loadedConfig);
@@ -350,7 +404,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
350
404
  setCompletionMessages(loadedSession.completionMessages);
351
405
  if (!didPrintIntroRef.current) {
352
406
  printBanner(addStatic);
353
- printRuntimeHeader(addStatic, loadedConfig, loadedSession, logFilePath, dangerouslyAcceptAll);
407
+ printRuntimeHeader(addStatic, loadedConfig, loadedSession, dangerouslySkipPermissions);
354
408
  replayMessagesToScrollback(addStatic, loadedSession.completionMessages);
355
409
  didPrintIntroRef.current = true;
356
410
  }
@@ -369,13 +423,13 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
369
423
  setSession(newSession);
370
424
  if (!didPrintIntroRef.current) {
371
425
  printBanner(addStatic);
372
- printRuntimeHeader(addStatic, loadedConfig, newSession, logFilePath, dangerouslyAcceptAll);
426
+ printRuntimeHeader(addStatic, loadedConfig, newSession, dangerouslySkipPermissions);
373
427
  didPrintIntroRef.current = true;
374
428
  }
375
429
  }
376
430
  setNeedsSetup(false);
377
431
  setInitialized(true);
378
- }, [dangerouslyAcceptAll, logFilePath, sessionId, addStatic]);
432
+ }, [dangerouslySkipPermissions, sessionId, addStatic]);
379
433
  // ─── Initialization ───
380
434
  useEffect(() => {
381
435
  if (!loading) {
@@ -392,13 +446,6 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
392
446
  addStatic(`\x1b[31mError: ${error}\x1b[0m\n\n`);
393
447
  }
394
448
  }, [error, addStatic]);
395
- useEffect(() => {
396
- if (!didPrintIntroRef.current || !logFilePath || printedLogPathRef.current === logFilePath) {
397
- return;
398
- }
399
- printedLogPathRef.current = logFilePath;
400
- addStatic(`Debug logs: ${logFilePath}\n\n`);
401
- }, [logFilePath, addStatic]);
402
449
  useEffect(() => {
403
450
  for (const threadError of threadErrors) {
404
451
  if (threadError.transient || printedThreadErrorIdsRef.current.has(threadError.id)) {
@@ -415,15 +462,14 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
415
462
  const level = LogLevel[logLevel.toUpperCase()];
416
463
  if (level !== undefined) {
417
464
  setLogLevel(level);
418
- const logPath = initLogFile();
419
- setLogFilePath(logPath);
465
+ initLogFile();
420
466
  logger.info(`ProtoAgent started with log level: ${logLevel}`);
421
- logger.info(`Log file: ${logPath}`);
467
+ logger.info(`Log file: ${logger.getLogFilePath()}`);
422
468
  }
423
469
  }
424
470
  // Set global approval mode
425
- if (dangerouslyAcceptAll) {
426
- setDangerouslyAcceptAll(true);
471
+ if (dangerouslySkipPermissions) {
472
+ setDangerouslySkipPermissions(true);
427
473
  }
428
474
  // Register interactive approval handler
429
475
  setApprovalHandler(async (req) => {
@@ -477,23 +523,6 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
477
523
  setError(`Failed to save session before exit: ${err.message}`);
478
524
  }
479
525
  return true;
480
- case '/clear':
481
- // Re-initialize messages with just the system prompt
482
- initializeMessages().then((msgs) => {
483
- setCompletionMessages(msgs);
484
- setHelpMessage(null);
485
- setLastUsage(null);
486
- setTotalCost(0);
487
- setThreadErrors([]);
488
- if (session) {
489
- const newSession = createSession(config.model, config.provider);
490
- clearTodos(session.id);
491
- clearTodos(newSession.id);
492
- newSession.completionMessages = msgs;
493
- setSession(newSession);
494
- }
495
- });
496
- return true;
497
526
  case '/expand':
498
527
  case '/collapse':
499
528
  // expand/collapse removed — transcript lives in scrollback
@@ -566,14 +595,18 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
566
595
  }
567
596
  case 'sub_agent_iteration':
568
597
  if (event.subAgentTool) {
569
- const { tool, status } = event.subAgentTool;
598
+ const { tool, status, args } = event.subAgentTool;
570
599
  if (status === 'running') {
571
- setActiveTool(`sub_agent → ${tool}`);
600
+ setActiveTool(formatSubAgentActivity(tool, args));
572
601
  }
573
602
  else {
574
603
  setActiveTool(null);
575
604
  }
576
605
  }
606
+ // Handle sub-agent usage update
607
+ if (event.subAgentUsage) {
608
+ setTotalCost((prev) => prev + event.subAgentUsage.cost);
609
+ }
577
610
  break;
578
611
  case 'tool_call':
579
612
  if (event.toolCall) {
@@ -770,7 +803,7 @@ export const App = ({ dangerouslyAcceptAll = false, logLevel, sessionId, }) => {
770
803
  initializeWithConfig(newConfig).catch((err) => {
771
804
  setError(`Initialization failed: ${err.message}`);
772
805
  });
773
- } })), isStreaming && (_jsxs(Text, { wrap: "wrap", children: [clipToRows(streamingText, terminalRows), _jsx(Text, { dimColor: true, children: "\u258D" })] })), threadErrors.filter((threadError) => threadError.transient).map((threadError) => (_jsx(LeftBar, { color: "red", marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", threadError.message] }) }, `thread-error-${threadError.id}`))), pendingApproval && (_jsx(ApprovalPrompt, { request: pendingApproval.request, onRespond: (response) => {
806
+ } })), isStreaming && (_jsxs(Text, { wrap: "wrap", children: [clipToRows(streamingText, terminalRows), _jsx(Text, { dimColor: true, children: "\u258D" })] })), threadErrors.filter((threadError) => threadError.transient).map((threadError) => (_jsx(LeftBar, { color: "gray", marginBottom: 1, children: _jsx(Text, { color: "gray", children: threadError.message }) }, `thread-error-${threadError.id}`))), pendingApproval && (_jsx(ApprovalPrompt, { request: pendingApproval.request, onRespond: (response) => {
774
807
  pendingApproval.resolve(response);
775
808
  setPendingApproval(null);
776
809
  } })), initialized && !pendingApproval && loading && !isStreaming && (_jsx(Box, { children: _jsxs(Text, { color: "green", bold: true, children: [SPINNER_FRAMES[spinnerFrame], ' ', activeTool ? `Running ${activeTool}...` : 'Working...'] }) })), initialized && !pendingApproval && inputText.startsWith('/') && (_jsx(CommandFilter, { inputText: inputText })), _jsx(UsageDisplay, { usage: lastUsage ?? null, totalCost: totalCost }), initialized && !pendingApproval && (_jsx(Box, { children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: "green", bold: true, children: '>' }) }), _jsx(Box, { flexGrow: 1, minWidth: 10, children: _jsx(TextInput, { defaultValue: inputText, onChange: setInputText, placeholder: "Type your message... (/help for commands)", onSubmit: handleSubmit }, inputResetKey) })] }) })), quittingSession && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Session saved. Resume with:" }), _jsxs(Text, { color: "green", children: ["protoagent --session ", quittingSession.id] })] }))] }));