nolo-cli 0.1.48 → 0.1.50

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 (2) hide show
  1. package/index.js +311 -50
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1576,6 +1576,26 @@ function formatToolMessageContent(args2) {
1576
1576
  [tool metadata]
1577
1577
  ${JSON.stringify(args2.metadata)}`;
1578
1578
  }
1579
+ function buildUserAction(args2) {
1580
+ const rawAction = args2.metadata?.requiresUserAction;
1581
+ if (!rawAction || typeof rawAction !== "object" || Array.isArray(rawAction)) return null;
1582
+ const action = rawAction;
1583
+ if (action.type !== "terminal_command") return null;
1584
+ if (!Array.isArray(action.argv)) return null;
1585
+ const argv = action.argv.flatMap(
1586
+ (item) => typeof item === "string" && item.trim() ? [item.trim()] : []
1587
+ );
1588
+ if (argv.length === 0) return null;
1589
+ return {
1590
+ type: "terminal_command",
1591
+ toolName: args2.toolName,
1592
+ toolCallId: args2.toolCallId,
1593
+ argv,
1594
+ ...typeof action.displayCommand === "string" ? { displayCommand: action.displayCommand } : {},
1595
+ ...typeof action.reason === "string" ? { reason: action.reason, message: action.reason } : {},
1596
+ ...typeof action.resumeHint === "string" ? { resumeHint: action.resumeHint } : {}
1597
+ };
1598
+ }
1579
1599
  function summarizeHistoricalToolContent(content) {
1580
1600
  if (typeof content !== "string") return content;
1581
1601
  if (content.length <= MAX_HISTORICAL_TOOL_CONTENT_CHARS) return content;
@@ -1677,6 +1697,17 @@ async function runLocalAgentTurn(input2) {
1677
1697
  arguments: toolCall.function.arguments,
1678
1698
  ...userInputText ? { userInput: userInputText } : {}
1679
1699
  });
1700
+ const userAction = buildUserAction({
1701
+ toolName,
1702
+ toolCallId: toolCall.id,
1703
+ metadata: toolResult.metadata
1704
+ });
1705
+ if (userAction && input2.onUserAction) {
1706
+ const replacement = await input2.onUserAction(userAction);
1707
+ if (replacement) {
1708
+ toolResult = replacement;
1709
+ }
1710
+ }
1680
1711
  emitToolEvent(input2, {
1681
1712
  type: "tool-result",
1682
1713
  round,
@@ -3436,6 +3467,35 @@ function accumulateToolCallDelta(accumulated, deltas) {
3436
3467
  function finalizeAccumulatedToolCalls(accumulated) {
3437
3468
  return Object.keys(accumulated).map((key2) => accumulated[Number(key2)]).filter((call) => call?.function?.name);
3438
3469
  }
3470
+ function processOpenAiCompatibleSseEvent(event, state) {
3471
+ for (const line of event.split("\n")) {
3472
+ const trimmed = line.trim();
3473
+ if (!trimmed.startsWith("data:")) continue;
3474
+ const payload = trimmed.slice(5).trim();
3475
+ if (!payload || payload === "[DONE]") continue;
3476
+ let parsed;
3477
+ try {
3478
+ parsed = JSON.parse(payload);
3479
+ } catch {
3480
+ continue;
3481
+ }
3482
+ if (parsed?.usage && typeof parsed.usage === "object") {
3483
+ state.usage = parsed.usage;
3484
+ }
3485
+ const delta = parsed?.choices?.[0]?.delta;
3486
+ if (!delta || typeof delta !== "object") continue;
3487
+ const reasoningChunk = typeof delta.reasoning_content === "string" ? delta.reasoning_content : typeof delta.reasoning === "string" ? delta.reasoning : "";
3488
+ if (reasoningChunk) state.reasoning += reasoningChunk;
3489
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
3490
+ accumulateToolCallDelta(state.accumulatedToolCalls, delta.tool_calls);
3491
+ }
3492
+ const textChunk = typeof delta.content === "string" ? delta.content : "";
3493
+ if (textChunk) {
3494
+ state.content += textChunk;
3495
+ state.onTextDelta?.(textChunk);
3496
+ }
3497
+ }
3498
+ }
3439
3499
  async function readOpenAiCompatibleSseCompletion(args2) {
3440
3500
  const reader = args2.response.body?.getReader();
3441
3501
  if (!reader) {
@@ -3443,10 +3503,13 @@ async function readOpenAiCompatibleSseCompletion(args2) {
3443
3503
  }
3444
3504
  const decoder = new TextDecoder();
3445
3505
  let buffer = "";
3446
- let content = "";
3447
- let reasoning = "";
3448
- let usage2;
3449
- const accumulatedToolCalls = {};
3506
+ const state = {
3507
+ content: "",
3508
+ reasoning: "",
3509
+ usage: void 0,
3510
+ accumulatedToolCalls: {},
3511
+ onTextDelta: args2.onTextDelta
3512
+ };
3450
3513
  while (true) {
3451
3514
  const { done, value } = await reader.read();
3452
3515
  if (done) break;
@@ -3456,41 +3519,19 @@ async function readOpenAiCompatibleSseCompletion(args2) {
3456
3519
  if (boundary === -1) break;
3457
3520
  const event = buffer.slice(0, boundary);
3458
3521
  buffer = buffer.slice(boundary + 2);
3459
- for (const line of event.split("\n")) {
3460
- const trimmed = line.trim();
3461
- if (!trimmed.startsWith("data:")) continue;
3462
- const payload = trimmed.slice(5).trim();
3463
- if (!payload || payload === "[DONE]") continue;
3464
- let parsed;
3465
- try {
3466
- parsed = JSON.parse(payload);
3467
- } catch {
3468
- continue;
3469
- }
3470
- if (parsed?.usage && typeof parsed.usage === "object") {
3471
- usage2 = parsed.usage;
3472
- }
3473
- const delta = parsed?.choices?.[0]?.delta;
3474
- if (!delta || typeof delta !== "object") continue;
3475
- const reasoningChunk = typeof delta.reasoning_content === "string" ? delta.reasoning_content : typeof delta.reasoning === "string" ? delta.reasoning : "";
3476
- if (reasoningChunk) reasoning += reasoningChunk;
3477
- if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
3478
- accumulateToolCallDelta(accumulatedToolCalls, delta.tool_calls);
3479
- }
3480
- const textChunk = typeof delta.content === "string" ? delta.content : "";
3481
- if (textChunk) {
3482
- content += textChunk;
3483
- args2.onTextDelta?.(textChunk);
3484
- }
3485
- }
3522
+ processOpenAiCompatibleSseEvent(event, state);
3486
3523
  }
3487
3524
  }
3488
- const tool_calls = finalizeAccumulatedToolCalls(accumulatedToolCalls);
3525
+ buffer += decoder.decode();
3526
+ if (buffer.trim()) {
3527
+ processOpenAiCompatibleSseEvent(buffer, state);
3528
+ }
3529
+ const tool_calls = finalizeAccumulatedToolCalls(state.accumulatedToolCalls);
3489
3530
  return {
3490
- content,
3491
- ...reasoning ? { reasoning_content: reasoning } : {},
3531
+ content: state.content,
3532
+ ...state.reasoning ? { reasoning_content: state.reasoning } : {},
3492
3533
  ...tool_calls.length > 0 ? { tool_calls } : {},
3493
- ...usage2 ? { usage: usage2 } : {}
3534
+ ...state.usage ? { usage: state.usage } : {}
3494
3535
  };
3495
3536
  }
3496
3537
  async function executeOpenAiCompatibleChatCompletion(args2) {
@@ -5121,6 +5162,81 @@ function readTrimmedString(value) {
5121
5162
  function readFiniteNumber(value) {
5122
5163
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5123
5164
  }
5165
+ function resolveExecShellTimeoutMs(override) {
5166
+ if (typeof override === "number" && Number.isFinite(override) && override > 0) {
5167
+ return override;
5168
+ }
5169
+ const raw = process.env[EXEC_SHELL_TIMEOUT_ENV];
5170
+ if (raw !== void 0) {
5171
+ const parsed = Number(raw);
5172
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
5173
+ }
5174
+ return void 0;
5175
+ }
5176
+ function tokenizeShellPrefix(command2) {
5177
+ const tokens = [];
5178
+ const pattern = /"([^"]*)"|'([^']*)'|([^\s"'|;&<>]+)/g;
5179
+ let match;
5180
+ while (match = pattern.exec(command2)) {
5181
+ tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
5182
+ }
5183
+ return tokens;
5184
+ }
5185
+ function extractInteractiveGhAuthCommand(command2) {
5186
+ const tokens = tokenizeShellPrefix(command2);
5187
+ if (tokens[0] !== "gh" || tokens[1] !== "auth") return null;
5188
+ const subcommand = tokens[2];
5189
+ if (subcommand !== "login" && subcommand !== "refresh") return null;
5190
+ if (tokens.includes("--help")) return null;
5191
+ const result = ["gh", "auth", subcommand];
5192
+ for (let index = 3; index < tokens.length; index += 1) {
5193
+ const token = tokens[index];
5194
+ if (!token) continue;
5195
+ if (token === "-h" || token === "--hostname" || token === "-s" || token === "--scopes" || token === "--remove-scopes" || token === "-r") {
5196
+ const value = tokens[index + 1];
5197
+ if (value) {
5198
+ result.push(token, value);
5199
+ index += 1;
5200
+ }
5201
+ continue;
5202
+ }
5203
+ if (token === "--clipboard" || token === "-c" || token === "--insecure-storage" || token === "--reset-scopes") {
5204
+ result.push(token);
5205
+ continue;
5206
+ }
5207
+ if (token.startsWith("--hostname=") || token.startsWith("--scopes=") || token.startsWith("--remove-scopes=")) {
5208
+ result.push(token);
5209
+ continue;
5210
+ }
5211
+ }
5212
+ return result.join(" ");
5213
+ }
5214
+ function splitShellWords(command2) {
5215
+ return tokenizeShellPrefix(command2);
5216
+ }
5217
+ function buildInteractiveCommandBlockedResult(command2) {
5218
+ const argv = splitShellWords(command2);
5219
+ return {
5220
+ content: [
5221
+ "requires_user_action: terminal_command",
5222
+ `command: ${command2}`,
5223
+ "Run this in the current TUI terminal, then resume the agent turn.",
5224
+ "exitCode: 130"
5225
+ ].join("\n"),
5226
+ metadata: {
5227
+ exitCode: 130,
5228
+ timedOut: false,
5229
+ requiresUserAction: {
5230
+ type: "terminal_command",
5231
+ argv,
5232
+ displayCommand: command2,
5233
+ reason: "This command requires an interactive terminal.",
5234
+ resumeHint: "Continue after the terminal command exits."
5235
+ },
5236
+ reason: "interactive-command-requires-terminal"
5237
+ }
5238
+ };
5239
+ }
5124
5240
  function extractActivityRefs(rawRefs) {
5125
5241
  if (!Array.isArray(rawRefs)) return void 0;
5126
5242
  const refs = rawRefs.flatMap((entry) => {
@@ -6792,6 +6908,18 @@ async function previewLifecycleTool(args2) {
6792
6908
  async function execShellTool(args2) {
6793
6909
  const parsed = parseWorkspaceToolArguments(args2.call.arguments);
6794
6910
  const command2 = requireShellCommand(parsed, args2.call.name);
6911
+ const interactiveAuthCommand = extractInteractiveGhAuthCommand(command2);
6912
+ if (interactiveAuthCommand) {
6913
+ const activity2 = extractActivity(parsed);
6914
+ const blocked = buildInteractiveCommandBlockedResult(interactiveAuthCommand);
6915
+ return {
6916
+ ...blocked,
6917
+ metadata: {
6918
+ ...blocked.metadata,
6919
+ ...activity2 ? { activity: activity2 } : {}
6920
+ }
6921
+ };
6922
+ }
6795
6923
  const result = await runWorkspaceCommand({
6796
6924
  workspaceRoot: args2.workspaceRoot,
6797
6925
  command: buildWorkspaceShellCommand({
@@ -6799,7 +6927,7 @@ async function execShellTool(args2) {
6799
6927
  command: command2,
6800
6928
  shell: parsed.shell
6801
6929
  }),
6802
- timeoutMs: args2.commandTimeoutMs,
6930
+ timeoutMs: resolveExecShellTimeoutMs(args2.commandTimeoutMs),
6803
6931
  outputLimit: args2.commandOutputLimit,
6804
6932
  commandPrefix: args2.commandPrefix
6805
6933
  });
@@ -6877,11 +7005,12 @@ function createLocalWorkspaceToolExecutors(args2) {
6877
7005
  })
6878
7006
  };
6879
7007
  }
6880
- var WORKSPACE_TOOL_NAMES, DEFAULT_LOCAL_CODING_TOOL_NAMES, SHELL_TOOL_NAMES, WORKSPACE_TOOL_NAME_SET, REMOVED_WORKSPACE_TOOL_NAMES;
7008
+ var EXEC_SHELL_TIMEOUT_ENV, WORKSPACE_TOOL_NAMES, DEFAULT_LOCAL_CODING_TOOL_NAMES, SHELL_TOOL_NAMES, WORKSPACE_TOOL_NAME_SET, REMOVED_WORKSPACE_TOOL_NAMES;
6881
7009
  var init_localWorkspaceTools = __esm({
6882
7010
  "packages/agent-runtime/localWorkspaceTools.ts"() {
6883
7011
  "use strict";
6884
7012
  init_runtimeCompat();
7013
+ EXEC_SHELL_TIMEOUT_ENV = "NOLO_EXEC_SHELL_TIMEOUT_MS";
6885
7014
  WORKSPACE_TOOL_NAMES = [
6886
7015
  "listFiles",
6887
7016
  "readFile",
@@ -7688,6 +7817,7 @@ function spawnProcess(options) {
7688
7817
  } catch {
7689
7818
  return {
7690
7819
  exited: Promise.resolve(127),
7820
+ stdin: null,
7691
7821
  stdout: null,
7692
7822
  stderr: null
7693
7823
  };
@@ -7698,6 +7828,7 @@ function spawnProcess(options) {
7698
7828
  });
7699
7829
  return {
7700
7830
  exited,
7831
+ stdin: child.stdin,
7701
7832
  stdout: child.stdout,
7702
7833
  stderr: child.stderr
7703
7834
  };
@@ -64754,7 +64885,9 @@ function createCliLocalRuntimeAdapter(deps) {
64754
64885
  const data2 = parsePlatformChatCompletionData(raw2);
64755
64886
  throw new Error(`platform provider failed: HTTP ${res.status} ${JSON.stringify(data2)}`);
64756
64887
  }
64757
- if (stream && options?.onTextDelta) {
64888
+ const contentType = res.headers.get("content-type") ?? "";
64889
+ const shouldStream = Boolean(stream && options?.onTextDelta) && contentType.includes("text/event-stream");
64890
+ if (shouldStream && options?.onTextDelta) {
64758
64891
  const streamed = await readOpenAiCompatibleSseCompletion({
64759
64892
  response: res,
64760
64893
  onTextDelta: options.onTextDelta
@@ -68114,6 +68247,64 @@ function formatAssistantDisplay(text, mode = "rich") {
68114
68247
  }
68115
68248
  return polished.split("\n").map((line) => styleRichMarkdownLine(line)).join("\n");
68116
68249
  }
68250
+ function emitFormattedAssistantBlock(write2, text, renderMode, trailingNewline = false) {
68251
+ if (!text) return;
68252
+ write2(formatAssistantDisplay(text, renderMode));
68253
+ if (trailingNewline) write2("\n");
68254
+ }
68255
+ function createRenderAwareStreamWriter(args2) {
68256
+ let buffer = "";
68257
+ const flushCompleteBlocks = () => {
68258
+ if (args2.renderMode === "plain") {
68259
+ if (!buffer) return;
68260
+ args2.write(buffer);
68261
+ buffer = "";
68262
+ return;
68263
+ }
68264
+ while (buffer.includes("\n")) {
68265
+ const lines = buffer.split("\n");
68266
+ if (lines.length < 2) break;
68267
+ if (isTableRow(lines[0] ?? "") && isTableSeparator(lines[1] ?? "")) {
68268
+ let end = 2;
68269
+ while (end < lines.length && isTableRow(lines[end] ?? "") && !isTableSeparator(lines[end] ?? "")) {
68270
+ end += 1;
68271
+ }
68272
+ const tableComplete = end < lines.length || buffer.endsWith("\n");
68273
+ if (!tableComplete) break;
68274
+ emitFormattedAssistantBlock(
68275
+ args2.write,
68276
+ lines.slice(0, end).join("\n"),
68277
+ args2.renderMode,
68278
+ true
68279
+ );
68280
+ buffer = lines.slice(end).join("\n");
68281
+ continue;
68282
+ }
68283
+ emitFormattedAssistantBlock(args2.write, lines[0] ?? "", args2.renderMode, true);
68284
+ buffer = lines.slice(1).join("\n");
68285
+ }
68286
+ };
68287
+ return {
68288
+ push(chunk) {
68289
+ if (!chunk) return;
68290
+ if (args2.renderMode === "plain") {
68291
+ args2.write(chunk);
68292
+ return;
68293
+ }
68294
+ buffer += chunk;
68295
+ flushCompleteBlocks();
68296
+ },
68297
+ flush() {
68298
+ if (!buffer) return;
68299
+ if (args2.renderMode === "plain") {
68300
+ args2.write(buffer);
68301
+ } else {
68302
+ emitFormattedAssistantBlock(args2.write, buffer, args2.renderMode);
68303
+ }
68304
+ buffer = "";
68305
+ }
68306
+ };
68307
+ }
68117
68308
 
68118
68309
  // packages/cli/client/thinkingOutput.ts
68119
68310
  var THINK_OPEN = /<think>/i;
@@ -68357,8 +68548,18 @@ function clip4(value, max = 72) {
68357
68548
  const compact2 = value.replace(/\s+/g, " ").trim();
68358
68549
  return compact2.length > max ? `${compact2.slice(0, max - 1)}\u2026` : compact2;
68359
68550
  }
68360
- function compactResultHint(summary, toolName) {
68551
+ function compactResultHint(event, toolName) {
68552
+ const rawAction = event.metadata?.requiresUserAction;
68553
+ if (rawAction && typeof rawAction === "object" && !Array.isArray(rawAction)) {
68554
+ const action = rawAction;
68555
+ const command2 = typeof action.displayCommand === "string" ? action.displayCommand : Array.isArray(action.argv) ? action.argv.filter((item) => typeof item === "string").join(" ") : "";
68556
+ return command2.trim() ? `needs terminal: ${clip4(command2, 120)}` : "needs terminal";
68557
+ }
68558
+ if (event.metadata?.timedOut) return "timed out";
68559
+ const summary = event.summary;
68361
68560
  if (!summary) return "";
68561
+ const exitMatch = summary.match(/exit=(\d+)/);
68562
+ if (exitMatch && exitMatch[1] !== "0") return `exit ${exitMatch[1]}`;
68362
68563
  const linesMatch = summary.match(/(\d+)\s+lines?/);
68363
68564
  if (linesMatch) {
68364
68565
  if (toolName === "readFile" || toolName === "listFiles" || toolName === "globFiles") {
@@ -68368,10 +68569,13 @@ function compactResultHint(summary, toolName) {
68368
68569
  return `${linesMatch[1]} lines`;
68369
68570
  }
68370
68571
  }
68371
- const exitMatch = summary.match(/exit=(\d+)/);
68372
- if (exitMatch && exitMatch[1] !== "0") return `exit ${exitMatch[1]}`;
68373
68572
  return "";
68374
68573
  }
68574
+ function isFailedToolResult(event) {
68575
+ const exitCode2 = event.metadata?.exitCode;
68576
+ if (typeof exitCode2 === "number" && exitCode2 !== 0) return true;
68577
+ return Boolean(event.metadata?.timedOut || event.metadata?.requiresUserAction);
68578
+ }
68375
68579
  function formatToolTraceLine(text, colorEnabled, accent = "none") {
68376
68580
  if (!colorEnabled) return `${text}
68377
68581
  `;
@@ -68413,10 +68617,12 @@ function formatCompactToolLine(event, pending, colorEnabled) {
68413
68617
  const timing2 = ms ? ` \xB7 ${ms}` : "";
68414
68618
  return formatToolTraceLine(` \u25B8 ${label} \u2717 ${message}${timing2}`, colorEnabled, "error");
68415
68619
  }
68416
- const hint = compactResultHint(event.summary, toolName);
68620
+ const hint = compactResultHint(event, toolName);
68417
68621
  const timing = ms ? ` ${ms}` : "";
68418
68622
  const suffix = hint ? ` \xB7 ${hint}` : "";
68419
- return formatToolTraceLine(` \u25B8 ${label} \u2713${timing}${suffix}`, colorEnabled);
68623
+ const marker = isFailedToolResult(event) ? "\u2717" : "\u2713";
68624
+ const accent = isFailedToolResult(event) ? "error" : "none";
68625
+ return formatToolTraceLine(` \u25B8 ${label} ${marker}${timing}${suffix}`, colorEnabled, accent);
68420
68626
  }
68421
68627
  function createToolEventFormatter(mode, colorEnabled = resolveCliColorEnabled()) {
68422
68628
  const pending = /* @__PURE__ */ new Map();
@@ -68920,12 +69126,16 @@ async function runLocalAgentTurnForCli(options, settings) {
68920
69126
  const traceLocalTools = shouldEmitToolEvents(toolDisplayMode);
68921
69127
  const formatToolEvent = createToolEventFormatter(toolDisplayMode);
68922
69128
  const eventMode = resolveAgentEventMode(options);
68923
- let wroteToolTrace = false;
68924
69129
  let streamedAssistantText = false;
68925
69130
  let printedAssistantLabel = false;
68926
69131
  const thinkingMode = resolveThinkingDisplayMode(options.env);
69132
+ const renderMode = resolveRenderDisplayMode(options.env);
69133
+ const renderWriter = createRenderAwareStreamWriter({
69134
+ write: (chunk) => options.output.write(chunk),
69135
+ renderMode
69136
+ });
68927
69137
  const thinkingFilter = createThinkingAwareStreamFilter(
68928
- (chunk) => options.output.write(chunk),
69138
+ (chunk) => renderWriter.push(chunk),
68929
69139
  thinkingMode
68930
69140
  );
68931
69141
  const subjectRefs = buildSubjectRefs(options);
@@ -68954,12 +69164,12 @@ async function runLocalAgentTurnForCli(options, settings) {
68954
69164
  }
68955
69165
  } : {},
68956
69166
  ...typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {},
69167
+ ...options.userActionHandler ? { onUserAction: options.userActionHandler } : {},
68957
69168
  ...traceLocalTools ? {
68958
69169
  onToolEvent: (event) => {
68959
69170
  spinner.stop();
68960
69171
  const chunk = eventMode === "jsonl" ? formatToolJsonEvent(event) : formatToolEvent(event);
68961
69172
  if (chunk) {
68962
- wroteToolTrace = true;
68963
69173
  options.output.write(chunk);
68964
69174
  }
68965
69175
  }
@@ -68980,6 +69190,7 @@ ${options.agentName} > `);
68980
69190
  spinner.stop();
68981
69191
  if (streamedAssistantText) {
68982
69192
  thinkingFilter.flush();
69193
+ renderWriter.flush();
68983
69194
  options.output.write("\n");
68984
69195
  } else {
68985
69196
  const content = formatAssistantResponseForCli(result.content.trim(), options);
@@ -69017,8 +69228,13 @@ async function readStreamingAgentRun(options, res) {
69017
69228
  }
69018
69229
  const decoder = new TextDecoder();
69019
69230
  const thinkingMode = resolveThinkingDisplayMode(options.env);
69231
+ const renderMode = resolveRenderDisplayMode(options.env);
69232
+ const renderWriter = createRenderAwareStreamWriter({
69233
+ write: (chunk) => options.output.write(chunk),
69234
+ renderMode
69235
+ });
69020
69236
  const writer = createStreamingTextWriter({
69021
- write: (chunk) => options.output.write(chunk)
69237
+ write: (chunk) => renderWriter.push(chunk)
69022
69238
  });
69023
69239
  const thinkingFilter = createThinkingAwareStreamFilter(
69024
69240
  (chunk) => writer.push(chunk),
@@ -69091,6 +69307,7 @@ ${options.agentName} > `);
69091
69307
  } finally {
69092
69308
  writer.flushAll();
69093
69309
  thinkingFilter.flush();
69310
+ renderWriter.flush();
69094
69311
  }
69095
69312
  if (!content) {
69096
69313
  options.output.write(`
@@ -74970,7 +75187,7 @@ ${renderTuiHelp()}`
74970
75187
  }
74971
75188
 
74972
75189
  // packages/cli/tui/readlineWorkspace.ts
74973
- async function runAgentChat(scriptDir, state, message, env, output2, agentRunner = runAgentTurn) {
75190
+ async function runAgentChat(scriptDir, state, message, env, output2, agentRunner = runAgentTurn, userActionHandler) {
74974
75191
  const result = await agentRunner({
74975
75192
  agentName: state.agentName,
74976
75193
  agentKey: state.agentKey,
@@ -74986,10 +75203,52 @@ async function runAgentChat(scriptDir, state, message, env, output2, agentRunner
74986
75203
  NOLO_CLI_TOOLS: state.toolDisplay,
74987
75204
  NOLO_CLI_RENDER: state.renderDisplay
74988
75205
  },
74989
- output: output2
75206
+ output: output2,
75207
+ ...userActionHandler ? { userActionHandler } : {}
74990
75208
  });
74991
75209
  return result;
74992
75210
  }
75211
+ function waitForManualUserAction(rl, input2, output2, action, spawnRunner) {
75212
+ const displayCommand = action.displayCommand ?? action.argv.join(" ");
75213
+ output2.write("\n[nolo] Action needed in your terminal\n");
75214
+ if (action.reason) output2.write(`[nolo] ${action.reason}
75215
+ `);
75216
+ output2.write(` ${displayCommand}
75217
+ `);
75218
+ output2.write("[nolo] Press Enter to run it now. Follow any prompts below, or Ctrl+C to cancel.\n");
75219
+ return new Promise((resolve6) => {
75220
+ rl.question("", async () => {
75221
+ const rawInput = input2;
75222
+ const restoreRawMode = Boolean(rawInput.isRaw);
75223
+ rl.pause();
75224
+ rawInput.setRawMode?.(false);
75225
+ let exitCode2 = 1;
75226
+ try {
75227
+ const proc = spawnRunner({
75228
+ cmd: action.argv,
75229
+ stdin: "inherit",
75230
+ stdout: "inherit",
75231
+ stderr: "inherit"
75232
+ });
75233
+ exitCode2 = await proc.exited;
75234
+ } finally {
75235
+ if (restoreRawMode) rawInput.setRawMode?.(true);
75236
+ rl.resume();
75237
+ }
75238
+ resolve6({
75239
+ content: exitCode2 === 0 ? `user action completed: ${displayCommand}` : `user action failed with exit code ${exitCode2}: ${displayCommand}`,
75240
+ metadata: {
75241
+ exitCode: exitCode2,
75242
+ userActionCompleted: exitCode2 === 0,
75243
+ userActionFailed: exitCode2 !== 0,
75244
+ requiresUserActionCompleted: true,
75245
+ argv: action.argv,
75246
+ displayCommand
75247
+ }
75248
+ });
75249
+ });
75250
+ });
75251
+ }
74993
75252
  async function pipeReadableToOutput(stream, output2) {
74994
75253
  const text = await readPipeText(stream);
74995
75254
  if (text) output2.write(text);
@@ -75030,6 +75289,7 @@ async function startTuiWorkspace(options) {
75030
75289
  const output2 = options.output ?? defaultOutput;
75031
75290
  const cliEntrypointPath = options.cliEntrypointPath ?? resolveDefaultCliEntrypoint(options.scriptDir);
75032
75291
  const cliCommandRunner = options.cliCommandRunner ?? runCliCommandInChildProcess;
75292
+ const spawnRunner = options.spawnRunner ?? spawnProcess;
75033
75293
  const selfUpdater = options.selfUpdater ?? ((target) => runSelfUpdate({ output: target }));
75034
75294
  const rl = createInterface2({ input: input2, output: output2 });
75035
75295
  output2.write(renderWelcome(state));
@@ -75172,7 +75432,8 @@ async function startTuiWorkspace(options) {
75172
75432
  result.action.message,
75173
75433
  options.env ?? process.env,
75174
75434
  output2,
75175
- options.agentRunner
75435
+ options.agentRunner,
75436
+ (action) => waitForManualUserAction(rl, input2, output2, action, spawnRunner)
75176
75437
  );
75177
75438
  if (runResult.dialogId || runResult.turnTokens) {
75178
75439
  state = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {