@nodetool-ai/cli 0.7.0-rc.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.
Files changed (77) hide show
  1. package/dist/ExecutionTree.d.ts +19 -0
  2. package/dist/ExecutionTree.d.ts.map +1 -0
  3. package/dist/ExecutionTree.js +100 -0
  4. package/dist/ExecutionTree.js.map +1 -0
  5. package/dist/app.d.ts +50 -0
  6. package/dist/app.d.ts.map +1 -0
  7. package/dist/app.js +767 -0
  8. package/dist/app.js.map +1 -0
  9. package/dist/commands/agent.d.ts +11 -0
  10. package/dist/commands/agent.d.ts.map +1 -0
  11. package/dist/commands/agent.js +483 -0
  12. package/dist/commands/agent.js.map +1 -0
  13. package/dist/commands/deploy-helpers.d.ts +46 -0
  14. package/dist/commands/deploy-helpers.d.ts.map +1 -0
  15. package/dist/commands/deploy-helpers.js +448 -0
  16. package/dist/commands/deploy-helpers.js.map +1 -0
  17. package/dist/commands/deploy.d.ts +8 -0
  18. package/dist/commands/deploy.d.ts.map +1 -0
  19. package/dist/commands/deploy.js +645 -0
  20. package/dist/commands/deploy.js.map +1 -0
  21. package/dist/commands/models-hf.d.ts +9 -0
  22. package/dist/commands/models-hf.d.ts.map +1 -0
  23. package/dist/commands/models-hf.js +235 -0
  24. package/dist/commands/models-hf.js.map +1 -0
  25. package/dist/commands/models-recommended.d.ts +10 -0
  26. package/dist/commands/models-recommended.d.ts.map +1 -0
  27. package/dist/commands/models-recommended.js +128 -0
  28. package/dist/commands/models-recommended.js.map +1 -0
  29. package/dist/commands/package-helpers.d.ts +14 -0
  30. package/dist/commands/package-helpers.d.ts.map +1 -0
  31. package/dist/commands/package-helpers.js +76 -0
  32. package/dist/commands/package-helpers.js.map +1 -0
  33. package/dist/commands/package.d.ts +3 -0
  34. package/dist/commands/package.d.ts.map +1 -0
  35. package/dist/commands/package.js +254 -0
  36. package/dist/commands/package.js.map +1 -0
  37. package/dist/index.d.ts +13 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +233 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/markdown.d.ts +9 -0
  42. package/dist/markdown.d.ts.map +1 -0
  43. package/dist/markdown.js +48 -0
  44. package/dist/markdown.js.map +1 -0
  45. package/dist/nodetool.d.ts +16 -0
  46. package/dist/nodetool.d.ts.map +1 -0
  47. package/dist/nodetool.js +1154 -0
  48. package/dist/nodetool.js.map +1 -0
  49. package/dist/providers.d.ts +47 -0
  50. package/dist/providers.d.ts.map +1 -0
  51. package/dist/providers.js +173 -0
  52. package/dist/providers.js.map +1 -0
  53. package/dist/readline-input.d.ts +29 -0
  54. package/dist/readline-input.d.ts.map +1 -0
  55. package/dist/readline-input.js +199 -0
  56. package/dist/readline-input.js.map +1 -0
  57. package/dist/run-dsl.d.ts +11 -0
  58. package/dist/run-dsl.d.ts.map +1 -0
  59. package/dist/run-dsl.js +31 -0
  60. package/dist/run-dsl.js.map +1 -0
  61. package/dist/settings.d.ts +18 -0
  62. package/dist/settings.d.ts.map +1 -0
  63. package/dist/settings.js +86 -0
  64. package/dist/settings.js.map +1 -0
  65. package/dist/stdin.d.ts +48 -0
  66. package/dist/stdin.d.ts.map +1 -0
  67. package/dist/stdin.js +352 -0
  68. package/dist/stdin.js.map +1 -0
  69. package/dist/useExecutionState.d.ts +44 -0
  70. package/dist/useExecutionState.d.ts.map +1 -0
  71. package/dist/useExecutionState.js +281 -0
  72. package/dist/useExecutionState.js.map +1 -0
  73. package/dist/websocket-client.d.ts +100 -0
  74. package/dist/websocket-client.d.ts.map +1 -0
  75. package/dist/websocket-client.js +351 -0
  76. package/dist/websocket-client.js.map +1 -0
  77. package/package.json +85 -0
package/dist/app.js ADDED
@@ -0,0 +1,767 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * NodeTool Chat CLI — Ink-based terminal UI.
4
+ *
5
+ * Layout:
6
+ * ╭─ nodetool ─────────────────── provider · model ─╮
7
+ * │ │
8
+ * │ You: message │
9
+ * │ │
10
+ * │ response with markdown │
11
+ * │ │
12
+ * │ ◆ tool_name result preview │
13
+ * │ │
14
+ * ├──────────────────────────────────────────────────┤
15
+ * │ › input │
16
+ * ╰──────────────────────────────────────────────────╯
17
+ */
18
+ import { useState, useEffect, useCallback, useRef } from "react";
19
+ import { Box, Text, Static, useApp, useInput } from "ink";
20
+ import ReadlineInput from "./readline-input.js";
21
+ import Spinner from "ink-spinner";
22
+ import { ExecutionTree } from "./ExecutionTree.js";
23
+ import { useExecutionState } from "./useExecutionState.js";
24
+ import { ProcessingContext } from "@nodetool-ai/runtime";
25
+ import { processChat } from "@nodetool-ai/chat";
26
+ import { MultiModeAgent } from "@nodetool-ai/agents";
27
+ import { ReadFileTool, WriteFileTool, ListDirectoryTool, EditFileTool, GlobTool, GrepTool, DownloadFileTool, HttpRequestTool, GoogleSearchTool, GoogleNewsTool, GoogleImagesTool, BrowserTool, ScreenshotTool, RunCodeTool, CalculatorTool, ExtractPDFTextTool, ConvertPDFToMarkdownTool, ConvertDocumentTool, StatisticsTool, GeometryTool, ConversionTool, OpenAIWebSearchTool, OpenAIImageGenerationTool, OpenAITextToSpeechTool, DataForSEOSearchTool, DataForSEONewsTool, SearchEmailTool, ArchiveEmailTool, getAllMcpTools, } from "@nodetool-ai/agents";
28
+ import { createProvider, DEFAULT_MODELS, KNOWN_PROVIDERS, WebSocketProvider } from "./providers.js";
29
+ import { WebSocketChatClient } from "./websocket-client.js";
30
+ import { renderMarkdown } from "./markdown.js";
31
+ import { saveSettings } from "./settings.js";
32
+ import { getSecret } from "@nodetool-ai/models";
33
+ // ---------------------------------------------------------------------------
34
+ // Command definitions
35
+ // ---------------------------------------------------------------------------
36
+ const COMMANDS = {
37
+ "/help": "Show available commands",
38
+ "/clear": "Clear conversation history",
39
+ "/model": "Set model: /model <model-id>",
40
+ "/provider": "Set provider: /provider <name>",
41
+ "/agent": "Toggle agent mode",
42
+ "/planner": "Set agent planner: /planner graph|multi",
43
+ "/tools": "List enabled tools",
44
+ "/exit": "Exit the chat",
45
+ "/quit": "Exit the chat",
46
+ };
47
+ // ---------------------------------------------------------------------------
48
+ // Individual message rendering
49
+ // ---------------------------------------------------------------------------
50
+ function UserMessage({ content }) {
51
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "magenta", dimColor: true, bold: true, children: "❯ " }), _jsx(Text, { bold: true, inverse: true, children: " " + content + " " })] }));
52
+ }
53
+ function AssistantMessage({ content, rendered }) {
54
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "● " }), _jsx(Text, { children: rendered ?? content })] }));
55
+ }
56
+ function ToolMessage({ toolName, toolArgs, content }) {
57
+ const argsStr = toolArgs
58
+ ? Object.entries(toolArgs)
59
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
60
+ .join(", ")
61
+ : "";
62
+ const preview = content.split("\n").slice(0, 3).join(" ").slice(0, 200);
63
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "● " }), _jsx(Text, { bold: true, children: toolName }), argsStr ? _jsxs(Text, { color: "gray", dimColor: true, children: ["(", argsStr, ")"] }) : null] }), preview ? (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: "gray", dimColor: true, children: "└ " }), _jsx(Text, { color: "gray", dimColor: true, children: preview })] })) : null] }));
64
+ }
65
+ function SystemMessage({ content }) {
66
+ return (_jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: " " + content }) }));
67
+ }
68
+ function ChatMessageItem({ msg }) {
69
+ switch (msg.role) {
70
+ case "user": return _jsx(UserMessage, { content: msg.content });
71
+ case "assistant": return _jsx(AssistantMessage, { content: msg.content, rendered: msg.rendered });
72
+ case "tool": return _jsx(ToolMessage, { toolName: msg.toolName ?? "tool", toolArgs: msg.toolArgs, content: msg.content });
73
+ case "system": return _jsx(SystemMessage, { content: msg.content });
74
+ }
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Autocomplete menu
78
+ // ---------------------------------------------------------------------------
79
+ const CMD_WIDTH = 14;
80
+ function AutocompleteMenu({ matches, selectedIndex, }) {
81
+ return (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: matches.map(({ cmd, desc }, i) => {
82
+ const selected = i === selectedIndex;
83
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: selected ? "cyan" : "gray", bold: selected, children: [selected ? "› " : " ", cmd.padEnd(CMD_WIDTH)] }), _jsx(Text, { color: "gray", dimColor: true, children: desc })] }, cmd));
84
+ }) }));
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Help panel
88
+ // ---------------------------------------------------------------------------
89
+ function HelpPanel() {
90
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "gray", dimColor: true, bold: true, children: "Commands" }), Object.entries(COMMANDS).map(([cmd, desc]) => (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: (" " + cmd).padEnd(16) }), _jsx(Text, { color: "gray", dimColor: true, children: desc })] }, cmd))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: " \u2191\u2193 history \u00B7 Tab complete \u00B7 Ctrl+C exit" }) })] }));
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Main App
94
+ // ---------------------------------------------------------------------------
95
+ export function App({ initialProvider, initialModel, initialAgentMode, initialAgentPlanner = "graph", enabledTools, workspaceDir, wsUrl, extraTools, registry, agentProviders, }) {
96
+ const { exit } = useApp();
97
+ // --- State ---
98
+ const [provider, setProvider] = useState(initialProvider);
99
+ const [model, setModel] = useState(initialModel);
100
+ const [agentMode, setAgentMode] = useState(initialAgentMode);
101
+ const [agentPlanner, setAgentPlanner] = useState(initialAgentPlanner);
102
+ const [messages, setMessages] = useState([]);
103
+ const [chatHistory, setChatHistory] = useState([]);
104
+ const [inputValue, setInputValue] = useState("");
105
+ const [, setInputHistory] = useState([]);
106
+ const [, setHistoryIndex] = useState(-1);
107
+ const [historyDraft, setHistoryDraft] = useState("");
108
+ const [streaming, setStreaming] = useState(false);
109
+ const [streamContent, setStreamContent] = useState("");
110
+ const [streamLabel, setStreamLabel] = useState("");
111
+ // Buffer messages during streaming to avoid Ink <Static> artifacts
112
+ const pendingMessagesRef = useRef([]);
113
+ const streamingRef = useRef(false);
114
+ const [error, setError] = useState(null);
115
+ const [showHelp, setShowHelp] = useState(false);
116
+ const execState = useExecutionState();
117
+ const [acIndex, setAcIndex] = useState(0);
118
+ const [modelList, setModelList] = useState([]);
119
+ // Fetch available models when provider changes
120
+ useEffect(() => {
121
+ let cancelled = false;
122
+ createProvider(provider).then(async (prov) => {
123
+ try {
124
+ const models = await prov.getAvailableLanguageModels();
125
+ if (!cancelled) {
126
+ setModelList(models.map((m) => ({ id: m.id, name: m.name })));
127
+ }
128
+ }
129
+ catch {
130
+ if (!cancelled)
131
+ setModelList([]);
132
+ }
133
+ }).catch(() => {
134
+ if (!cancelled)
135
+ setModelList([]);
136
+ });
137
+ return () => { cancelled = true; };
138
+ }, [provider]);
139
+ // WebSocket client state (when --url is passed)
140
+ const wsClientRef = useRef(null);
141
+ const [threadId, setThreadId] = useState(() => crypto.randomUUID());
142
+ // Refs to hold latest values without causing re-renders in async callbacks
143
+ const chatHistoryRef = useRef(chatHistory);
144
+ const providerRef = useRef(provider);
145
+ const modelRef = useRef(model);
146
+ const agentModeRef = useRef(agentMode);
147
+ const agentPlannerRef = useRef(agentPlanner);
148
+ const abortRef = useRef(false);
149
+ const abortControllerRef = useRef(null);
150
+ // Guard against double-submit: useInput autocomplete Enter + TextInput onSubmit fire for the same keypress
151
+ const submittingRef = useRef(false);
152
+ useEffect(() => { chatHistoryRef.current = chatHistory; }, [chatHistory]);
153
+ useEffect(() => { providerRef.current = provider; }, [provider]);
154
+ useEffect(() => { modelRef.current = model; }, [model]);
155
+ useEffect(() => { agentModeRef.current = agentMode; }, [agentMode]);
156
+ useEffect(() => { agentPlannerRef.current = agentPlanner; }, [agentPlanner]);
157
+ useEffect(() => { setAcIndex(0); }, [inputValue]);
158
+ // Connect WebSocket when --url is provided
159
+ useEffect(() => {
160
+ if (!wsUrl)
161
+ return;
162
+ const client = new WebSocketChatClient(wsUrl);
163
+ client.connect().then(() => {
164
+ wsClientRef.current = client;
165
+ }).catch((err) => {
166
+ const msg = err instanceof Error ? err.message : String(err);
167
+ addMessage("system", `WebSocket connection failed: ${msg}`);
168
+ });
169
+ return () => {
170
+ client.disconnect();
171
+ wsClientRef.current = null;
172
+ };
173
+ }, [wsUrl]);
174
+ // Unique ID generator
175
+ const nextId = useRef(0);
176
+ const genId = () => `msg-${++nextId.current}`;
177
+ // Add a message to the display history.
178
+ // During streaming, tool/system messages are buffered to avoid Ink <Static> artifacts.
179
+ // User and assistant messages (which mark start/end of a turn) are added immediately.
180
+ const addMessage = useCallback(async (role, content, opts) => {
181
+ let rendered;
182
+ if (role === "assistant") {
183
+ rendered = await renderMarkdown(content);
184
+ }
185
+ const msg = {
186
+ id: genId(),
187
+ role,
188
+ content,
189
+ rendered,
190
+ toolName: opts?.toolName,
191
+ toolArgs: opts?.toolArgs,
192
+ };
193
+ if (streamingRef.current && role !== "user" && role !== "assistant") {
194
+ pendingMessagesRef.current.push(msg);
195
+ }
196
+ else {
197
+ setMessages(prev => [...prev, msg]);
198
+ }
199
+ }, []);
200
+ // Flush buffered messages when streaming ends
201
+ const flushPendingMessages = useCallback(() => {
202
+ if (pendingMessagesRef.current.length > 0) {
203
+ const pending = pendingMessagesRef.current;
204
+ pendingMessagesRef.current = [];
205
+ setMessages(prev => [...prev, ...pending]);
206
+ }
207
+ }, []);
208
+ // Create tools from enabled list
209
+ function buildTools() {
210
+ const toolMap = {
211
+ // Filesystem
212
+ read_file: new ReadFileTool(),
213
+ write_file: new WriteFileTool(),
214
+ edit_file: new EditFileTool(),
215
+ list_directory: new ListDirectoryTool(),
216
+ glob: new GlobTool(),
217
+ grep: new GrepTool(),
218
+ // HTTP
219
+ download_file: new DownloadFileTool(),
220
+ http_request: new HttpRequestTool(),
221
+ // Search (SERPAPI)
222
+ google_search: new GoogleSearchTool(),
223
+ google_news: new GoogleNewsTool(),
224
+ google_images: new GoogleImagesTool(),
225
+ // Browser
226
+ browser: new BrowserTool(),
227
+ screenshot: new ScreenshotTool(),
228
+ // Code & math
229
+ run_code: new RunCodeTool(),
230
+ calculator: new CalculatorTool(),
231
+ statistics: new StatisticsTool(),
232
+ geometry: new GeometryTool(),
233
+ conversion: new ConversionTool(),
234
+ // PDF & documents
235
+ extract_pdf_text: new ExtractPDFTextTool(),
236
+ convert_pdf_to_markdown: new ConvertPDFToMarkdownTool(),
237
+ convert_document: new ConvertDocumentTool(),
238
+ // OpenAI tools
239
+ openai_web_search: new OpenAIWebSearchTool(),
240
+ openai_image_generation: new OpenAIImageGenerationTool(),
241
+ openai_text_to_speech: new OpenAITextToSpeechTool(),
242
+ // DataForSEO
243
+ dataseo_search: new DataForSEOSearchTool(),
244
+ dataseo_news: new DataForSEONewsTool(),
245
+ // Email (IMAP)
246
+ search_email: new SearchEmailTool(),
247
+ archive_email: new ArchiveEmailTool(),
248
+ };
249
+ // NodeTool MCP tools (workflows, nodes, jobs, assets, models).
250
+ // When a NodeRegistry is in process, this swaps the REST node-search
251
+ // tools for the local biased versions (and adds `find_model`) so any
252
+ // agent loop gets the same node-selection bias as the GraphPlanner.
253
+ for (const tool of getAllMcpTools({
254
+ registry,
255
+ providers: agentProviders,
256
+ })) {
257
+ toolMap[tool.name] = tool;
258
+ }
259
+ // When sandbox tools are present, exclude host tools that have sandbox
260
+ // equivalents so the agent is forced to execute inside the sandbox.
261
+ const sandboxMode = extraTools && extraTools.length > 0;
262
+ const hostToolsToExclude = sandboxMode
263
+ ? new Set([
264
+ "read_file",
265
+ "write_file",
266
+ "edit_file",
267
+ "list_directory",
268
+ "glob",
269
+ "grep",
270
+ "run_code",
271
+ "browser",
272
+ "screenshot",
273
+ "google_search",
274
+ "google_news",
275
+ "google_images",
276
+ "dataseo_search",
277
+ "dataseo_news"
278
+ ])
279
+ : null;
280
+ const enabled = enabledTools
281
+ .filter(name => name in toolMap && !(hostToolsToExclude?.has(name)))
282
+ .map(name => toolMap[name]);
283
+ return extraTools && extraTools.length > 0
284
+ ? [...enabled, ...extraTools]
285
+ : enabled;
286
+ }
287
+ // ---------------------------------------------------------------------------
288
+ // Command handler
289
+ // ---------------------------------------------------------------------------
290
+ const handleCommand = useCallback(async (raw) => {
291
+ const parts = raw.trim().split(/\s+/);
292
+ const cmd = parts[0].toLowerCase();
293
+ const args = parts.slice(1);
294
+ switch (cmd) {
295
+ case "/help":
296
+ setShowHelp(prev => !prev);
297
+ return true;
298
+ case "/clear":
299
+ setMessages([{ id: genId(), role: "system", content: "History cleared." }]);
300
+ setChatHistory([]);
301
+ setShowHelp(false);
302
+ if (wsClientRef.current) {
303
+ setThreadId(crypto.randomUUID()); // next message starts a fresh server thread
304
+ }
305
+ return true;
306
+ case "/exit":
307
+ case "/quit":
308
+ await saveSettings({ provider, model, agentMode, agentPlanner });
309
+ exit();
310
+ return true;
311
+ case "/agent":
312
+ setAgentMode(prev => {
313
+ const next = !prev;
314
+ addMessage("system", `Agent mode ${next ? "ON" : "OFF"}.`);
315
+ return next;
316
+ });
317
+ return true;
318
+ case "/planner": {
319
+ const arg = args[0]?.toLowerCase();
320
+ if (arg === "graph" || arg === "multi") {
321
+ setAgentPlanner(arg);
322
+ await saveSettings({ agentPlanner: arg });
323
+ addMessage("system", arg === "graph"
324
+ ? "Planner set to graph (workflow builder)."
325
+ : "Planner set to multi (parallel task plan).");
326
+ }
327
+ else {
328
+ addMessage("system", `Current planner: ${agentPlanner}. Usage: /planner graph|multi`);
329
+ }
330
+ return true;
331
+ }
332
+ case "/model":
333
+ if (args[0]) {
334
+ setModel(args[0]);
335
+ await saveSettings({ model: args[0] });
336
+ addMessage("system", `Model set to: ${args[0]}`);
337
+ }
338
+ else {
339
+ addMessage("system", `Current model: ${model}. Usage: /model <model-id>`);
340
+ }
341
+ return true;
342
+ case "/provider": {
343
+ if (args[0]) {
344
+ const newProvider = args[0].toLowerCase();
345
+ const newModel = DEFAULT_MODELS[newProvider] ?? model;
346
+ setProvider(newProvider);
347
+ setModel(newModel);
348
+ await saveSettings({ provider: newProvider, model: newModel });
349
+ addMessage("system", `Provider: ${newProvider} • Model: ${newModel}`);
350
+ }
351
+ else {
352
+ addMessage("system", `Current provider: ${provider}. Usage: /provider <name>`);
353
+ }
354
+ return true;
355
+ }
356
+ case "/tools":
357
+ addMessage("system", `Enabled tools: ${enabledTools.join(", ") || "(none)"}`);
358
+ return true;
359
+ default:
360
+ if (cmd.startsWith("/")) {
361
+ addMessage("system", `Unknown command: ${cmd}. Type /help for commands.`);
362
+ return true;
363
+ }
364
+ return false;
365
+ }
366
+ }, [provider, model, agentMode, enabledTools, addMessage, exit]);
367
+ // ---------------------------------------------------------------------------
368
+ // Chat submission
369
+ // ---------------------------------------------------------------------------
370
+ const handleSubmit = useCallback(async (value) => {
371
+ const trimmed = value.trim();
372
+ if (!trimmed)
373
+ return;
374
+ // Deduplicate: useInput autocomplete Enter + TextInput onSubmit both fire for the same keypress
375
+ if (submittingRef.current)
376
+ return;
377
+ submittingRef.current = true;
378
+ // Add to history (deduplicated)
379
+ setInputHistory(prev => {
380
+ const filtered = prev.filter(h => h !== trimmed);
381
+ return [trimmed, ...filtered].slice(0, 100);
382
+ });
383
+ setHistoryIndex(-1);
384
+ setHistoryDraft("");
385
+ setInputValue("");
386
+ setError(null);
387
+ setShowHelp(false);
388
+ // Handle commands
389
+ if (trimmed.startsWith("/")) {
390
+ await handleCommand(trimmed);
391
+ submittingRef.current = false;
392
+ return;
393
+ }
394
+ // Add user message to display
395
+ await addMessage("user", trimmed);
396
+ abortRef.current = false;
397
+ const abortController = new AbortController();
398
+ abortControllerRef.current = abortController;
399
+ streamingRef.current = true;
400
+ setStreaming(true);
401
+ setStreamContent("");
402
+ setStreamLabel("thinking");
403
+ try {
404
+ const ctx = new ProcessingContext({ jobId: crypto.randomUUID(), userId: "1", workspaceDir, secretResolver: getSecret });
405
+ const tools = buildTools();
406
+ if (agentModeRef.current) {
407
+ // --- Agent mode ---
408
+ setStreamLabel("planning");
409
+ execState.reset();
410
+ const prov = wsClientRef.current
411
+ ? new WebSocketProvider(wsClientRef.current, modelRef.current, providerRef.current)
412
+ : await createProvider(providerRef.current);
413
+ // Pick the planner. The "graph" planner needs a registry; without
414
+ // one we silently downgrade to the multi-task planner.
415
+ const useGraphPlanner = agentPlannerRef.current === "graph" && !!registry;
416
+ const markdownOutputSchema = {
417
+ type: "object",
418
+ properties: {
419
+ markdown: {
420
+ type: "string",
421
+ description: "The markdown content of the response",
422
+ },
423
+ },
424
+ required: ["markdown"],
425
+ };
426
+ const agent = new MultiModeAgent({
427
+ name: "chat-agent",
428
+ objective: trimmed,
429
+ provider: prov,
430
+ model: modelRef.current,
431
+ mode: "plan",
432
+ tools,
433
+ useGraphPlanner,
434
+ registry: useGraphPlanner ? registry : undefined,
435
+ providers: useGraphPlanner ? agentProviders : undefined,
436
+ maxStepIterations: 20,
437
+ outputSchema: markdownOutputSchema,
438
+ });
439
+ // Feed all messages into the execution tree state.
440
+ let synthesisContent = "";
441
+ for await (const msg of agent.execute(ctx)) {
442
+ if (abortRef.current)
443
+ break;
444
+ execState.processMessage(msg);
445
+ // Stream the agent's final synthesis so the user sees the answer
446
+ // forming in real time. Other chunks (step thinking, planner
447
+ // commentary) are noise in the chat pane and stay in the tree.
448
+ if (msg.type === "chunk") {
449
+ const ch = msg;
450
+ if (ch.node_id === "agent_synthesizer" && ch.content) {
451
+ synthesisContent += ch.content;
452
+ setStreamContent(synthesisContent);
453
+ setStreamLabel("finalizing");
454
+ }
455
+ }
456
+ }
457
+ execState.markDone();
458
+ setStreamContent("");
459
+ const finalResults = agent.getResults();
460
+ const finalText = typeof finalResults === "string"
461
+ ? finalResults
462
+ : finalResults &&
463
+ typeof finalResults === "object" &&
464
+ "markdown" in finalResults &&
465
+ typeof finalResults.markdown ===
466
+ "string"
467
+ ? finalResults.markdown
468
+ : finalResults != null
469
+ ? JSON.stringify(finalResults, null, 2)
470
+ : "";
471
+ if (finalText) {
472
+ await addMessage("assistant", finalText);
473
+ }
474
+ }
475
+ else if (wsClientRef.current && !extraTools?.length) {
476
+ // --- Regular chat via WebSocket (server handles everything) ---
477
+ const wsClient = wsClientRef.current;
478
+ let assistantContent = "";
479
+ const toolSchemas = tools.map(t => t.toProviderTool());
480
+ const pendingToolArgs = new Map();
481
+ for await (const event of wsClient.chat(trimmed, threadId, modelRef.current, providerRef.current, toolSchemas)) {
482
+ if (abortRef.current)
483
+ break;
484
+ if (event.type === "chunk") {
485
+ assistantContent += event.content;
486
+ setStreamContent(assistantContent);
487
+ setStreamLabel("streaming");
488
+ }
489
+ else if (event.type === "tool_call") {
490
+ pendingToolArgs.set(event.id, event.args);
491
+ setStreamLabel(`tool: ${event.name}`);
492
+ }
493
+ else if (event.type === "tool_result") {
494
+ const preview = event.content.length > 100 ? event.content.slice(0, 100) + "…" : event.content;
495
+ const args = pendingToolArgs.get(event.id);
496
+ pendingToolArgs.delete(event.id);
497
+ await addMessage("tool", preview, { toolName: event.name, toolArgs: args });
498
+ setStreamLabel("thinking");
499
+ }
500
+ else if (event.type === "error") {
501
+ throw new Error(event.message);
502
+ }
503
+ else if (event.type === "done") {
504
+ break;
505
+ }
506
+ }
507
+ if (assistantContent) {
508
+ await addMessage("assistant", assistantContent);
509
+ }
510
+ }
511
+ else if (wsClientRef.current && extraTools?.length) {
512
+ // --- Regular chat via WebSocket inference + local sandbox tool execution ---
513
+ const prov = new WebSocketProvider(wsClientRef.current, modelRef.current, providerRef.current);
514
+ let assistantContent = "";
515
+ const updatedHistory = [...chatHistoryRef.current];
516
+ await processChat({
517
+ userInput: trimmed,
518
+ messages: updatedHistory,
519
+ model: modelRef.current,
520
+ provider: prov,
521
+ context: ctx,
522
+ tools,
523
+ signal: abortController.signal,
524
+ callbacks: {
525
+ onChunk: (text) => {
526
+ if (abortRef.current)
527
+ throw new Error("aborted");
528
+ assistantContent += text;
529
+ setStreamContent(assistantContent);
530
+ setStreamLabel("streaming");
531
+ },
532
+ onToolCall: (tc) => {
533
+ setStreamLabel(`tool: ${tc.name}`);
534
+ },
535
+ onToolResult: (tc, result) => {
536
+ const preview = typeof result === "string"
537
+ ? result
538
+ : JSON.stringify(result).slice(0, 100);
539
+ addMessage("tool", preview, { toolName: tc.name, toolArgs: tc.args });
540
+ },
541
+ },
542
+ });
543
+ setChatHistory(updatedHistory);
544
+ if (assistantContent) {
545
+ await addMessage("assistant", assistantContent);
546
+ }
547
+ }
548
+ else {
549
+ // --- Regular chat mode (direct provider) ---
550
+ const prov = await createProvider(providerRef.current);
551
+ let assistantContent = "";
552
+ const updatedHistory = [...chatHistoryRef.current];
553
+ await processChat({
554
+ userInput: trimmed,
555
+ messages: updatedHistory,
556
+ model: modelRef.current,
557
+ provider: prov,
558
+ context: ctx,
559
+ tools,
560
+ signal: abortController.signal,
561
+ callbacks: {
562
+ onChunk: (text) => {
563
+ if (abortRef.current)
564
+ throw new Error("aborted");
565
+ assistantContent += text;
566
+ setStreamContent(assistantContent);
567
+ setStreamLabel("streaming");
568
+ },
569
+ onToolCall: (tc) => {
570
+ setStreamLabel(`tool: ${tc.name}`);
571
+ },
572
+ onToolResult: (tc, result) => {
573
+ const preview = typeof result === "string"
574
+ ? result
575
+ : JSON.stringify(result).slice(0, 100);
576
+ addMessage("tool", preview, { toolName: tc.name, toolArgs: tc.args });
577
+ },
578
+ },
579
+ });
580
+ setChatHistory(updatedHistory);
581
+ if (assistantContent) {
582
+ await addMessage("assistant", assistantContent);
583
+ }
584
+ }
585
+ }
586
+ catch (err) {
587
+ if (!abortRef.current) {
588
+ const msg = err instanceof Error ? err.message : String(err);
589
+ setError(msg);
590
+ await addMessage("system", `Error: ${msg}`);
591
+ }
592
+ }
593
+ finally {
594
+ streamingRef.current = false;
595
+ setStreaming(false);
596
+ setStreamContent("");
597
+ setStreamLabel("");
598
+ flushPendingMessages();
599
+ submittingRef.current = false;
600
+ abortControllerRef.current = null;
601
+ }
602
+ }, [handleCommand, addMessage, flushPendingMessages, workspaceDir, enabledTools]);
603
+ let acMatches = [];
604
+ if (!streaming && inputValue.startsWith("/")) {
605
+ const lower = inputValue.toLowerCase();
606
+ const spaceIdx = lower.indexOf(" ");
607
+ if (spaceIdx === -1) {
608
+ // Command completion: /pro → /provider
609
+ acMatches = Object.entries(COMMANDS)
610
+ .filter(([cmd]) => cmd.startsWith(lower))
611
+ .map(([cmd, desc]) => ({ cmd, desc }));
612
+ }
613
+ else {
614
+ // Argument completion for specific commands
615
+ const cmd = lower.slice(0, spaceIdx);
616
+ const arg = lower.slice(spaceIdx + 1);
617
+ if (cmd === "/provider") {
618
+ acMatches = KNOWN_PROVIDERS
619
+ .filter((p) => p.startsWith(arg))
620
+ .map((p) => ({
621
+ cmd: p,
622
+ desc: DEFAULT_MODELS[p] ?? "",
623
+ replaceAll: `/provider ${p}`,
624
+ }));
625
+ }
626
+ else if (cmd === "/model") {
627
+ acMatches = modelList
628
+ .filter((m) => m.id.toLowerCase().startsWith(arg))
629
+ .map((m) => ({
630
+ cmd: m.id,
631
+ desc: m.name !== m.id ? m.name : "",
632
+ replaceAll: `/model ${m.id}`,
633
+ }));
634
+ }
635
+ }
636
+ }
637
+ const acOpen = acMatches.length > 0;
638
+ useInput((input, key) => {
639
+ // Escape or Ctrl+C: cancel streaming, or exit when idle
640
+ if (key.escape || (key.ctrl && input === "c")) {
641
+ if (streaming) {
642
+ abortRef.current = true;
643
+ abortControllerRef.current?.abort();
644
+ abortControllerRef.current = null;
645
+ if (wsClientRef.current) {
646
+ wsClientRef.current.stop(agentModeRef.current ? undefined : threadId);
647
+ }
648
+ // Immediately reset UI — don't wait for async cleanup
649
+ setStreaming(false);
650
+ setStreamContent("");
651
+ setStreamLabel("");
652
+ submittingRef.current = false;
653
+ }
654
+ else {
655
+ saveSettings({ provider, model, agentMode }).then(() => exit());
656
+ }
657
+ return;
658
+ }
659
+ // During agent streaming, allow tree navigation
660
+ if (streaming && agentModeRef.current) {
661
+ if (key.upArrow) {
662
+ execState.navigate("up");
663
+ return;
664
+ }
665
+ if (key.downArrow) {
666
+ execState.navigate("down");
667
+ return;
668
+ }
669
+ if (key.return || key.rightArrow) {
670
+ execState.toggleExpand();
671
+ return;
672
+ }
673
+ if (key.leftArrow) {
674
+ execState.toggleExpand();
675
+ return;
676
+ }
677
+ return;
678
+ }
679
+ if (streaming)
680
+ return; // block other input while streaming
681
+ // Autocomplete navigation
682
+ if (acOpen) {
683
+ if (key.upArrow) {
684
+ setAcIndex(i => (i <= 0 ? acMatches.length - 1 : i - 1));
685
+ return;
686
+ }
687
+ if (key.downArrow) {
688
+ setAcIndex(i => (i >= acMatches.length - 1 ? 0 : i + 1));
689
+ return;
690
+ }
691
+ if (key.tab) {
692
+ const selected = acMatches[acIndex] ?? acMatches[0];
693
+ if (selected)
694
+ setInputValue(selected.replaceAll ?? selected.cmd + " ");
695
+ setAcIndex(0);
696
+ return;
697
+ }
698
+ if (key.return) {
699
+ const selected = acMatches[acIndex] ?? acMatches[0];
700
+ if (selected) {
701
+ const completed = selected.replaceAll ?? selected.cmd + " ";
702
+ // Submit complete commands directly; partial ones go into input for editing
703
+ if (completed.trimEnd() in COMMANDS) {
704
+ handleSubmit(completed);
705
+ }
706
+ else {
707
+ setInputValue(completed);
708
+ }
709
+ setAcIndex(0);
710
+ }
711
+ return;
712
+ }
713
+ }
714
+ // History navigation (only when autocomplete is closed)
715
+ if (!acOpen) {
716
+ if (key.upArrow) {
717
+ setInputHistory(hist => {
718
+ if (hist.length === 0)
719
+ return hist;
720
+ setHistoryIndex(prev => {
721
+ if (prev === -1)
722
+ setHistoryDraft(inputValue);
723
+ const next = Math.min(prev + 1, hist.length - 1);
724
+ setInputValue(hist[next] ?? "");
725
+ return next;
726
+ });
727
+ return hist;
728
+ });
729
+ return;
730
+ }
731
+ if (key.downArrow) {
732
+ setHistoryIndex(prev => {
733
+ if (prev <= 0) {
734
+ setInputValue(historyDraft);
735
+ return -1;
736
+ }
737
+ const next = prev - 1;
738
+ setInputHistory(hist => {
739
+ setInputValue(hist[next] ?? "");
740
+ return hist;
741
+ });
742
+ return next;
743
+ });
744
+ return;
745
+ }
746
+ }
747
+ });
748
+ // ---------------------------------------------------------------------------
749
+ // Render
750
+ // ---------------------------------------------------------------------------
751
+ const statusParts = [
752
+ provider,
753
+ model,
754
+ agentMode ? `agent:${agentPlanner}` : null,
755
+ wsUrl ? "ws" : null,
756
+ ].filter(Boolean).join(" ");
757
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: messages, children: (msg) => (_jsx(Box, { children: _jsx(ChatMessageItem, { msg: msg }) }, msg.id)) }), showHelp && _jsx(HelpPanel, {}), streaming && agentMode && (_jsx(ExecutionTree, { state: execState.state, treeActive: true })), streaming && !agentMode && (() => {
758
+ // Truncate streaming preview to avoid overflowing the terminal's dynamic area.
759
+ const maxPreviewLines = Math.max((process.stdout.rows ?? 24) - 4, 5);
760
+ const lines = streamContent ? streamContent.split("\n") : [];
761
+ const truncated = lines.length > maxPreviewLines
762
+ ? lines.slice(-maxPreviewLines).join("\n")
763
+ : streamContent;
764
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [truncated ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "● " }), _jsx(Text, { children: truncated })] })) : null, _jsx(Box, { children: _jsxs(Text, { color: "gray", dimColor: true, children: [" ", _jsx(Spinner, { type: "dots" }), " ", streamLabel || "thinking"] }) })] }));
765
+ })(), error && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "red", children: "● " }), _jsx(Text, { color: "red", children: error })] })), acOpen && (_jsx(AutocompleteMenu, { matches: acMatches, selectedIndex: Math.min(acIndex, acMatches.length - 1) })), !streaming && (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(process.stdout.columns ?? 80) }) }), _jsxs(Box, { children: [_jsx(Text, { color: "magenta", dimColor: true, bold: true, children: "❯ " }), _jsx(ReadlineInput, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit })] }), _jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: "─".repeat(process.stdout.columns ?? 80) }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", dimColor: true, children: [" ", statusParts] }) })] }))] }));
766
+ }
767
+ //# sourceMappingURL=app.js.map