nolo-cli 0.1.49 → 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 +191 -10
  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,
@@ -5131,6 +5162,81 @@ function readTrimmedString(value) {
5131
5162
  function readFiniteNumber(value) {
5132
5163
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5133
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
+ }
5134
5240
  function extractActivityRefs(rawRefs) {
5135
5241
  if (!Array.isArray(rawRefs)) return void 0;
5136
5242
  const refs = rawRefs.flatMap((entry) => {
@@ -6802,6 +6908,18 @@ async function previewLifecycleTool(args2) {
6802
6908
  async function execShellTool(args2) {
6803
6909
  const parsed = parseWorkspaceToolArguments(args2.call.arguments);
6804
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
+ }
6805
6923
  const result = await runWorkspaceCommand({
6806
6924
  workspaceRoot: args2.workspaceRoot,
6807
6925
  command: buildWorkspaceShellCommand({
@@ -6809,7 +6927,7 @@ async function execShellTool(args2) {
6809
6927
  command: command2,
6810
6928
  shell: parsed.shell
6811
6929
  }),
6812
- timeoutMs: args2.commandTimeoutMs,
6930
+ timeoutMs: resolveExecShellTimeoutMs(args2.commandTimeoutMs),
6813
6931
  outputLimit: args2.commandOutputLimit,
6814
6932
  commandPrefix: args2.commandPrefix
6815
6933
  });
@@ -6887,11 +7005,12 @@ function createLocalWorkspaceToolExecutors(args2) {
6887
7005
  })
6888
7006
  };
6889
7007
  }
6890
- 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;
6891
7009
  var init_localWorkspaceTools = __esm({
6892
7010
  "packages/agent-runtime/localWorkspaceTools.ts"() {
6893
7011
  "use strict";
6894
7012
  init_runtimeCompat();
7013
+ EXEC_SHELL_TIMEOUT_ENV = "NOLO_EXEC_SHELL_TIMEOUT_MS";
6895
7014
  WORKSPACE_TOOL_NAMES = [
6896
7015
  "listFiles",
6897
7016
  "readFile",
@@ -7698,6 +7817,7 @@ function spawnProcess(options) {
7698
7817
  } catch {
7699
7818
  return {
7700
7819
  exited: Promise.resolve(127),
7820
+ stdin: null,
7701
7821
  stdout: null,
7702
7822
  stderr: null
7703
7823
  };
@@ -7708,6 +7828,7 @@ function spawnProcess(options) {
7708
7828
  });
7709
7829
  return {
7710
7830
  exited,
7831
+ stdin: child.stdin,
7711
7832
  stdout: child.stdout,
7712
7833
  stderr: child.stderr
7713
7834
  };
@@ -68427,8 +68548,18 @@ function clip4(value, max = 72) {
68427
68548
  const compact2 = value.replace(/\s+/g, " ").trim();
68428
68549
  return compact2.length > max ? `${compact2.slice(0, max - 1)}\u2026` : compact2;
68429
68550
  }
68430
- 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;
68431
68560
  if (!summary) return "";
68561
+ const exitMatch = summary.match(/exit=(\d+)/);
68562
+ if (exitMatch && exitMatch[1] !== "0") return `exit ${exitMatch[1]}`;
68432
68563
  const linesMatch = summary.match(/(\d+)\s+lines?/);
68433
68564
  if (linesMatch) {
68434
68565
  if (toolName === "readFile" || toolName === "listFiles" || toolName === "globFiles") {
@@ -68438,10 +68569,13 @@ function compactResultHint(summary, toolName) {
68438
68569
  return `${linesMatch[1]} lines`;
68439
68570
  }
68440
68571
  }
68441
- const exitMatch = summary.match(/exit=(\d+)/);
68442
- if (exitMatch && exitMatch[1] !== "0") return `exit ${exitMatch[1]}`;
68443
68572
  return "";
68444
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
+ }
68445
68579
  function formatToolTraceLine(text, colorEnabled, accent = "none") {
68446
68580
  if (!colorEnabled) return `${text}
68447
68581
  `;
@@ -68483,10 +68617,12 @@ function formatCompactToolLine(event, pending, colorEnabled) {
68483
68617
  const timing2 = ms ? ` \xB7 ${ms}` : "";
68484
68618
  return formatToolTraceLine(` \u25B8 ${label} \u2717 ${message}${timing2}`, colorEnabled, "error");
68485
68619
  }
68486
- const hint = compactResultHint(event.summary, toolName);
68620
+ const hint = compactResultHint(event, toolName);
68487
68621
  const timing = ms ? ` ${ms}` : "";
68488
68622
  const suffix = hint ? ` \xB7 ${hint}` : "";
68489
- 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);
68490
68626
  }
68491
68627
  function createToolEventFormatter(mode, colorEnabled = resolveCliColorEnabled()) {
68492
68628
  const pending = /* @__PURE__ */ new Map();
@@ -69028,6 +69164,7 @@ async function runLocalAgentTurnForCli(options, settings) {
69028
69164
  }
69029
69165
  } : {},
69030
69166
  ...typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {},
69167
+ ...options.userActionHandler ? { onUserAction: options.userActionHandler } : {},
69031
69168
  ...traceLocalTools ? {
69032
69169
  onToolEvent: (event) => {
69033
69170
  spinner.stop();
@@ -75050,7 +75187,7 @@ ${renderTuiHelp()}`
75050
75187
  }
75051
75188
 
75052
75189
  // packages/cli/tui/readlineWorkspace.ts
75053
- async function runAgentChat(scriptDir, state, message, env, output2, agentRunner = runAgentTurn) {
75190
+ async function runAgentChat(scriptDir, state, message, env, output2, agentRunner = runAgentTurn, userActionHandler) {
75054
75191
  const result = await agentRunner({
75055
75192
  agentName: state.agentName,
75056
75193
  agentKey: state.agentKey,
@@ -75066,10 +75203,52 @@ async function runAgentChat(scriptDir, state, message, env, output2, agentRunner
75066
75203
  NOLO_CLI_TOOLS: state.toolDisplay,
75067
75204
  NOLO_CLI_RENDER: state.renderDisplay
75068
75205
  },
75069
- output: output2
75206
+ output: output2,
75207
+ ...userActionHandler ? { userActionHandler } : {}
75070
75208
  });
75071
75209
  return result;
75072
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
+ }
75073
75252
  async function pipeReadableToOutput(stream, output2) {
75074
75253
  const text = await readPipeText(stream);
75075
75254
  if (text) output2.write(text);
@@ -75110,6 +75289,7 @@ async function startTuiWorkspace(options) {
75110
75289
  const output2 = options.output ?? defaultOutput;
75111
75290
  const cliEntrypointPath = options.cliEntrypointPath ?? resolveDefaultCliEntrypoint(options.scriptDir);
75112
75291
  const cliCommandRunner = options.cliCommandRunner ?? runCliCommandInChildProcess;
75292
+ const spawnRunner = options.spawnRunner ?? spawnProcess;
75113
75293
  const selfUpdater = options.selfUpdater ?? ((target) => runSelfUpdate({ output: target }));
75114
75294
  const rl = createInterface2({ input: input2, output: output2 });
75115
75295
  output2.write(renderWelcome(state));
@@ -75252,7 +75432,8 @@ async function startTuiWorkspace(options) {
75252
75432
  result.action.message,
75253
75433
  options.env ?? process.env,
75254
75434
  output2,
75255
- options.agentRunner
75435
+ options.agentRunner,
75436
+ (action) => waitForManualUserAction(rl, input2, output2, action, spawnRunner)
75256
75437
  );
75257
75438
  if (runResult.dialogId || runResult.turnTokens) {
75258
75439
  state = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nolo-cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "description": "Agent-first terminal workspace for Nolo",
6
6
  "bin": {