nox-qwen-agent-cli 1.1.4 → 1.1.6

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
@@ -22,10 +22,10 @@ PowerShell can also run `.\install.ps1` directly.
22
22
 
23
23
  ## Configure
24
24
 
25
- Interactive mode asks for a masked API key on every launch. The key remains in
26
- memory for that process only and is never stored in source code, settings, or
27
- chat history. Non-interactive `-p` runs read `NOX_API_KEY`,
28
- `OPENAI_COMPAT_API_KEY`, or `OPENAI_API_KEY`.
25
+ Runs read `NOX_API_KEY`, `OPENAI_COMPAT_API_KEY`, or `OPENAI_API_KEY` when one
26
+ is configured. If no key is configured and a terminal is available, the CLI asks
27
+ for a masked API key. The key remains in memory for that process only and is
28
+ never stored in source code, settings, or chat history.
29
29
 
30
30
  Linux, macOS, and Termux:
31
31
 
@@ -45,6 +45,7 @@ $env:NOX_API_KEY = "your-key"
45
45
  qwen
46
46
  qwen -m qwen3.7-plus
47
47
  qwen -m gpt-5.5 -p "Inspect and test this project."
48
+ qwen --clarify -p "Add authentication."
48
49
  qwen --yes --no-stream
49
50
  ```
50
51
 
@@ -56,8 +57,13 @@ The interactive composer is multiline:
56
57
  - Arrow keys, Home, End, Backspace, and Delete edit the message.
57
58
  - Multiline clipboard paste is preserved.
58
59
 
59
- Commands: `/plan TASK`, `/image PATH PROMPT`, `/file PATH PROMPT`, `/new`, `/sessions`,
60
- `/resume SESSION`, `/approval ask|auto`, `/model`, and `/exit`.
60
+ Commands: `/plan TASK`, `/clarify TASK`, `/image PATH PROMPT`, `/file PATH PROMPT`,
61
+ `/new`, `/sessions`, `/resume SESSION`, `/approval ask|auto`, `/model`, and `/exit`.
62
+
63
+ Use `/clarify TASK` or `--clarify -p "TASK"` when you want the agent to ask
64
+ precise requirements questions first. In clarification mode the agent should
65
+ not edit files or run side-effecting commands; it returns focused questions and
66
+ safe assumptions before implementation.
61
67
 
62
68
  Images and files are sent as structured OpenAI attachment parts. Their bytes
63
69
  are uploaded through the selected provider's file endpoint and are never
@@ -67,8 +73,9 @@ Streaming is enabled by default. Reasoning is shown in a separate Thinking
67
73
  section when the provider returns it, and is never mixed into the final answer.
68
74
 
69
75
  The agent includes bounded file reads, batch reads and writes, tree and text
70
- search, path metadata, file operations, Git status/diff, and cross-platform
71
- shell execution. Mutating actions require approval unless `--yes` is used.
76
+ search, path metadata, file operations, Git status/diff, cross-platform shell
77
+ execution, and background terminals for dev servers and long-running commands.
78
+ Mutating actions require approval unless `--yes` is used.
72
79
  For substantial tasks the model can call `update_plan`; plan steps and their
73
80
  live status are rendered separately from chat and tool output.
74
81
 
@@ -77,12 +84,18 @@ live status are rendered separately from chat and tool output.
77
84
  The Agent prompt and file tools are maintained centrally. Available workspace
78
85
  tools include `file_info`, `list_files`, `search_files`, `find_in_file`,
79
86
  `read_file`, `read_lines`, `update_file`, `apply_patch`, `create_file`,
80
- `delete_file`, `move_file`, `copy_file`, and `make_directory`.
81
-
82
- Every Agent run sends fresh runtime context in the system prompt: OS/version,
83
- processor, Node.js version, workspace root, top-level workspace entries,
84
- available tools, and approval mode. The prompt includes an explicit Agent
85
- reminder at startup.
87
+ `delete_file`, `move_file`, `copy_file`, `make_directory`,
88
+ `start_background_terminal`, `read_background_terminal`, and
89
+ `stop_background_terminal`.
90
+
91
+ Every Agent run sends fresh runtime context in the first user message:
92
+ OS/version, processor, Node.js version, workspace root, top-level workspace
93
+ entries, available tools, and approval mode. The CLI and VS Code extension
94
+ intentionally transport the full Agent instructions by prefixing the first user
95
+ message with `<agent_instructions>`. Every fourth user turn is prefixed with
96
+ `<agent_reminder>` so the model keeps the active workspace and agent role in
97
+ context. This avoids gateways that drop or weaken `system` messages while still
98
+ sending a SHA-256 prompt proof in request metadata.
86
99
 
87
100
  The file tools enforce workspace-only paths, reject traversal and symlink
88
101
  escapes, deny protected secret-looking files by default, detect binary files,
@@ -28,9 +28,13 @@ Tool-selection rules:
28
28
  - Use create_file only when a new file is genuinely required.
29
29
  - Use delete_file only when deletion is explicitly required and safe.
30
30
  - Use run_shell_command only for commands relevant to the user's task.
31
+ - Use start_background_terminal for long-running commands such as dev servers, watchers, and commands that need later polling.
32
+ - Use read_background_terminal to inspect background output before claiming a server, watcher, or long task is ready.
33
+ - Use stop_background_terminal when a background process is no longer needed or is producing failures.
31
34
  - Run focused tests first, followed by the broader suite when appropriate.
32
35
 
33
36
  File-access rules:
37
+ - A file may be available through an uploaded file id, an attachment in the current request, a client-provided excerpt, workspace tools, a line-range read, or a search result followed by a targeted read.
34
38
  - Do not ask the user to upload a file when it is already available in the workspace.
35
39
  - Do not read an entire large file when a targeted range is sufficient.
36
40
  - Do not invent missing file contents.
@@ -47,8 +51,16 @@ Editing rules:
47
51
  Planning rules:
48
52
  Use a plan when the task has multiple steps, affects several files, requires investigation, or requires tests.
49
53
 
54
+ Clarification rules:
55
+ - If a request is broad, ambiguous, risky, destructive, or missing acceptance criteria, ask concise clarifying questions before changing files or running commands with side effects.
56
+ - Ask only for details that materially affect the implementation, validation, or safety of the task.
57
+ - Keep clarification questions numbered, specific, and limited to five unless the user explicitly asks for a deeper requirements interview.
58
+ - When a reasonable default is safe, state the assumption and continue instead of blocking.
59
+ - If the user explicitly asks for clarification mode, do not execute the task yet; return the questions and the proposed assumptions only.
60
+
50
61
  Progress rules:
51
62
  For longer operations, provide brief progress updates after meaningful milestones. Do not narrate every low-level command.
63
+ When a background terminal is running, summarize only meaningful state changes such as started, ready, failed, or stopped.
52
64
 
53
65
  Safety rules:
54
66
  - Stay inside the configured workspace root.
@@ -57,6 +69,7 @@ Safety rules:
57
69
  - Do not read protected secret files unless explicitly authorised.
58
70
  - Do not execute destructive commands without explicit permission.
59
71
  - Do not install untrusted packages or extensions silently.
72
+ - Do not leave background terminals running after they are no longer needed unless the user asked to keep them running.
60
73
  - Do not bypass tests merely to produce a successful-looking result.
61
74
 
62
75
  Completion rules:
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nox-qwen-agent-cli",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Cross-platform OpenAI-compatible Qwen coding agent CLI",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/qwen.js CHANGED
@@ -19,6 +19,16 @@ const MAX_FILE_BYTES = 20 * 1024 * 1024;
19
19
  const MAX_COMMAND_OUTPUT = 128 * 1024;
20
20
  const MAX_API_RETRIES = 2;
21
21
  const MAX_SHELL_TIMEOUT_MS = 10 * 60 * 1000;
22
+ const MAX_BACKGROUND_OUTPUT = 256 * 1024;
23
+ const MAX_BACKGROUND_TERMINALS = 8;
24
+ const CLARIFY_REQUEST_PROMPT = `Clarify this request before execution.
25
+ Do not modify files, run commands with side effects, or implement the task yet.
26
+ Ask the minimum set of precise questions needed to remove ambiguity, identify acceptance criteria, and confirm risky choices.
27
+ Use no more than five numbered questions unless the user explicitly asks for a deeper requirements interview.
28
+ If the request is already clear enough, say so and list the assumptions you would use before execution.
29
+
30
+ Request:
31
+ `;
22
32
  const SYSTEM_PROMPT_RULES = `The tools attached to the request are authoritative. Use their exact names and JSON schemas.
23
33
  Use tools for every local operation. Never claim a command ran or a file changed without a tool result.
24
34
  Perform one coherent tool action at a time, inspect its result, and continue until the task is complete.
@@ -32,6 +42,7 @@ When a tool is required, output exactly one block and no other text:
32
42
  <NOX_TOOL_CALL>{"id":"short-stable-id","tool":"exact_tool_name","args":{}}</NOX_TOOL_CALL>
33
43
  Arguments must match the selected tool schema. Never emit more than one tool call per response.
34
44
  After receiving a <tool_result> block, continue the same objective with the next tool call or the final answer.`;
45
+ const AGENT_REMINDER_PROMPT = `Agent reminder: continue following the initial Agent instructions, use the available tools when needed, keep workspace operations inside the active workspace, and finish only after the requested task is handled.`;
35
46
 
36
47
  const TOOL_DEFINITIONS = [
37
48
  tool("update_plan", "Create or update the visible execution plan.", {
@@ -144,6 +155,19 @@ const TOOL_DEFINITIONS = [
144
155
  cwd: stringProperty("Relative working directory; defaults to ."),
145
156
  timeout_ms: integerProperty("Timeout from 1 to 600000 ms"),
146
157
  }, ["command"]),
158
+ tool("start_background_terminal", "Start a long-running shell command in the background and return a terminal id for later polling.", {
159
+ command: stringProperty("Shell command"),
160
+ cwd: stringProperty("Relative working directory; defaults to ."),
161
+ }, ["command"]),
162
+ tool("read_background_terminal", "Read buffered output and status from a background terminal.", {
163
+ terminal_id: stringProperty("Background terminal id returned by start_background_terminal"),
164
+ max_bytes: integerProperty("Maximum stdout/stderr bytes to return; defaults to 12000"),
165
+ wait_ms: integerProperty("Optional polling wait from 0 to 10000 ms for new output or process exit"),
166
+ }, ["terminal_id"]),
167
+ tool("stop_background_terminal", "Stop a running background terminal.", {
168
+ terminal_id: stringProperty("Background terminal id returned by start_background_terminal"),
169
+ signal: stringProperty("Signal name; defaults to SIGTERM"),
170
+ }, ["terminal_id"]),
147
171
  ];
148
172
 
149
173
  const EFFECTIVE_TOOL_DEFINITIONS = [
@@ -191,6 +215,57 @@ function refreshSystemPrompt(state) {
191
215
  else state.messages.unshift({ role: "system", content: prompt });
192
216
  }
193
217
 
218
+ function prefixTextContent(content, prompt, kind) {
219
+ const firstTurn = kind === "instructions";
220
+ return [
221
+ firstTurn ? "<agent_instructions>" : "<agent_reminder>",
222
+ prompt,
223
+ firstTurn ? "</agent_instructions>" : "</agent_reminder>",
224
+ "<user_message>",
225
+ String(content ?? ""),
226
+ "</user_message>",
227
+ ].join("\n");
228
+ }
229
+
230
+ function promptForUserTurn(userTurn, prompt) {
231
+ if (userTurn === 1) return { kind: "instructions", prompt };
232
+ if (userTurn > 1 && userTurn % 4 === 0) return { kind: "reminder", prompt: AGENT_REMINDER_PROMPT };
233
+ return null;
234
+ }
235
+
236
+ function prefixUserContent(content, transport) {
237
+ if (!transport) return content;
238
+ if (Array.isArray(content)) {
239
+ let inserted = false;
240
+ const parts = content.map((part) => {
241
+ if (!inserted && part?.type === "text") {
242
+ inserted = true;
243
+ return { ...part, text: prefixTextContent(part.text, transport.prompt, transport.kind) };
244
+ }
245
+ return part;
246
+ });
247
+ if (inserted) return parts;
248
+ return [{ type: "text", text: prefixTextContent("", transport.prompt, transport.kind) }, ...parts];
249
+ }
250
+ return prefixTextContent(content, transport.prompt, transport.kind);
251
+ }
252
+
253
+ function outboundMessages(state) {
254
+ const prompt = systemPrompt(state.workspace, state.options);
255
+ let userTurn = 1;
256
+ return state.messages
257
+ .filter((message) => message?.role !== "system" && message?.role !== "developer")
258
+ .map((message) => {
259
+ if (message?.role !== "user") return message;
260
+ const transport = promptForUserTurn(userTurn, prompt);
261
+ userTurn += 1;
262
+ return {
263
+ ...message,
264
+ content: prefixUserContent(message.content, transport),
265
+ };
266
+ });
267
+ }
268
+
194
269
  function tool(name, description, properties, required = []) {
195
270
  return {
196
271
  type: "function",
@@ -226,6 +301,7 @@ function parseArgs(argv) {
226
301
  prompt: "",
227
302
  stream: true,
228
303
  yes: false,
304
+ clarify: false,
229
305
  approvalMode: "ask",
230
306
  history: true,
231
307
  resume: "",
@@ -238,6 +314,7 @@ function parseArgs(argv) {
238
314
  else if (value === "-p" || value === "--prompt") options.prompt = argv[++index] || "";
239
315
  else if (value === "--base-url") options.baseUrl = argv[++index] || options.baseUrl;
240
316
  else if (value === "-C" || value === "--workspace") options.workspace = argv[++index] || options.workspace;
317
+ else if (value === "--clarify") options.clarify = true;
241
318
  else if (value === "--no-stream") options.stream = false;
242
319
  else if (value === "--stream") options.stream = true;
243
320
  else if (value === "--request-timeout") options.requestTimeoutMs = Number.parseInt(argv[++index] || "", 10) * 1000;
@@ -262,11 +339,13 @@ Usage:
262
339
  qwen
263
340
  qwen install ext
264
341
  qwen -m gpt-5.5 -p "inspect this project"
342
+ qwen --clarify -p "add auth"
265
343
  qwen --yes --no-stream
266
344
 
267
345
  Options:
268
346
  -m, --model MODEL Select model without the menu
269
347
  -p, --prompt TEXT Run one task and exit
348
+ --clarify Ask precise clarification questions for --prompt before execution
270
349
  -C, --workspace PATH Active workspace
271
350
  --base-url URL OpenAI-compatible base URL
272
351
  --stream / --no-stream Streaming mode; streaming is default
@@ -287,10 +366,15 @@ Interactive commands:
287
366
  /resume SESSION Resume a saved chat
288
367
  /approval ask|auto Change approval mode for this session
289
368
  /plan TASK Require a visible plan before execution
369
+ /clarify TASK Ask precise questions and assumptions before execution
290
370
  /clear Alias for /new
291
371
  /exit Exit`;
292
372
  }
293
373
 
374
+ function clarifyPrompt(request) {
375
+ return `${CLARIFY_REQUEST_PROMPT}${String(request || "").trim()}`;
376
+ }
377
+
294
378
  function configuredApiKey() {
295
379
  return String(
296
380
  process.env.NOX_API_KEY
@@ -668,6 +752,9 @@ async function executeToolCall(call, context) {
668
752
  }
669
753
  return await runShell(String(args.command), cwd, args.timeout_ms);
670
754
  }
755
+ if (name === "start_background_terminal") return await startBackgroundTerminal(context, args);
756
+ if (name === "read_background_terminal") return await readBackgroundTerminal(context, args);
757
+ if (name === "stop_background_terminal") return await stopBackgroundTerminal(context, args);
671
758
  return { ok: false, error: `Unknown tool: ${name}` };
672
759
  } catch (error) {
673
760
  return { ok: false, error: String(error?.message || error) };
@@ -764,6 +851,44 @@ function shellQuote(value) {
764
851
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
765
852
  }
766
853
 
854
+ function parseTextToolCall(content) {
855
+ const text = typeof content === "string" ? content.trim() : "";
856
+ const match = text.match(/^<NOX_TOOL_CALL>([\s\S]+)<\/NOX_TOOL_CALL>$/);
857
+ if (!match) return null;
858
+ let payload;
859
+ try {
860
+ payload = JSON.parse(match[1]);
861
+ } catch {
862
+ return null;
863
+ }
864
+ const name = String(payload?.tool || "");
865
+ if (!name) return null;
866
+ const id = String(payload?.id || `text_tool_${crypto.randomUUID().slice(0, 8)}`);
867
+ const args = payload?.args && typeof payload.args === "object" && !Array.isArray(payload.args)
868
+ ? payload.args
869
+ : {};
870
+ return {
871
+ id,
872
+ type: "function",
873
+ function: {
874
+ name,
875
+ arguments: JSON.stringify(args),
876
+ },
877
+ };
878
+ }
879
+
880
+ function normalizeAssistantToolTransport(assistant) {
881
+ const nativeCalls = Array.isArray(assistant.tool_calls) ? assistant.tool_calls : [];
882
+ if (nativeCalls.length) return assistant;
883
+ const textCall = parseTextToolCall(assistant.content);
884
+ if (!textCall) return assistant;
885
+ return {
886
+ ...assistant,
887
+ content: null,
888
+ tool_calls: [textCall],
889
+ };
890
+ }
891
+
767
892
  async function runShell(command, cwd, timeoutValue) {
768
893
  const timeoutMs = Math.max(1, Math.min(MAX_SHELL_TIMEOUT_MS, Number(timeoutValue || 120000)));
769
894
  return await new Promise((resolvePromise) => {
@@ -798,6 +923,125 @@ async function runShell(command, cwd, timeoutValue) {
798
923
  });
799
924
  }
800
925
 
926
+ function ensureBackgroundTerminals(context) {
927
+ if (!context.backgroundTerminals) context.backgroundTerminals = new Map();
928
+ return context.backgroundTerminals;
929
+ }
930
+
931
+ function backgroundSnapshot(entry, maxBytes = 12000) {
932
+ const limit = Math.max(1, Math.min(MAX_BACKGROUND_OUTPUT, Number(maxBytes || 12000)));
933
+ const stdout = entry.stdout.slice(-limit);
934
+ const stderr = entry.stderr.slice(-limit);
935
+ return {
936
+ ok: true,
937
+ terminal_id: entry.id,
938
+ command: entry.command,
939
+ cwd: entry.relativeCwd,
940
+ running: entry.exit === null,
941
+ pid: entry.child.pid,
942
+ started_at: entry.startedAt,
943
+ exited_at: entry.exitedAt,
944
+ exit_code: entry.exit?.code ?? null,
945
+ signal: entry.exit?.signal ?? null,
946
+ stdout,
947
+ stderr,
948
+ truncated: entry.stdout.length > stdout.length || entry.stderr.length > stderr.length || entry.truncated,
949
+ };
950
+ }
951
+
952
+ function appendBackgroundOutput(entry, target, chunk) {
953
+ const text = chunk.toString("utf8");
954
+ entry[target] += text;
955
+ if (entry.stdout.length + entry.stderr.length <= MAX_BACKGROUND_OUTPUT) return;
956
+ const overflow = entry.stdout.length + entry.stderr.length - MAX_BACKGROUND_OUTPUT;
957
+ if (entry[target].length >= overflow) entry[target] = entry[target].slice(overflow);
958
+ else {
959
+ const remaining = overflow - entry[target].length;
960
+ entry[target] = "";
961
+ const other = target === "stdout" ? "stderr" : "stdout";
962
+ entry[other] = entry[other].slice(Math.min(remaining, entry[other].length));
963
+ }
964
+ entry.truncated = true;
965
+ }
966
+
967
+ async function startBackgroundTerminal(context, args) {
968
+ const terminals = ensureBackgroundTerminals(context);
969
+ for (const [id, entry] of terminals) {
970
+ if (entry.exit !== null) terminals.delete(id);
971
+ }
972
+ if (terminals.size >= MAX_BACKGROUND_TERMINALS) {
973
+ throw new Error(`At most ${MAX_BACKGROUND_TERMINALS} background terminals can run at once.`);
974
+ }
975
+ const cwd = safeWorkspacePath(context.workspace, args.cwd || ".");
976
+ const command = String(args.command || "");
977
+ if (!command.trim()) throw new Error("command must not be empty.");
978
+ if (!await confirm(context.terminal, context.options, "start background terminal", `$ ${command}\n cwd: ${path.relative(context.workspace, cwd) || "."}`)) {
979
+ return { ok: false, error: "User denied background terminal." };
980
+ }
981
+ const windows = process.platform === "win32";
982
+ const executable = windows ? (process.env.ComSpec || "cmd.exe") : "/bin/sh";
983
+ const shellArgs = windows ? ["/d", "/s", "/c", command] : ["-lc", command];
984
+ const child = spawn(executable, shellArgs, { cwd, env: process.env, windowsHide: true });
985
+ const id = `bg_${crypto.randomUUID().slice(0, 8)}`;
986
+ const entry = {
987
+ id,
988
+ command,
989
+ relativeCwd: path.relative(context.workspace, cwd) || ".",
990
+ child,
991
+ stdout: "",
992
+ stderr: "",
993
+ truncated: false,
994
+ startedAt: new Date().toISOString(),
995
+ exitedAt: null,
996
+ exit: null,
997
+ };
998
+ child.stdout.on("data", (chunk) => appendBackgroundOutput(entry, "stdout", chunk));
999
+ child.stderr.on("data", (chunk) => appendBackgroundOutput(entry, "stderr", chunk));
1000
+ child.on("error", (error) => {
1001
+ entry.stderr += error.message;
1002
+ entry.exit = { code: null, signal: "ERROR" };
1003
+ entry.exitedAt = new Date().toISOString();
1004
+ });
1005
+ child.on("close", (code, signal) => {
1006
+ entry.exit = { code, signal };
1007
+ entry.exitedAt = new Date().toISOString();
1008
+ });
1009
+ terminals.set(id, entry);
1010
+ return backgroundSnapshot(entry, args.max_bytes);
1011
+ }
1012
+
1013
+ async function readBackgroundTerminal(context, args) {
1014
+ const terminals = ensureBackgroundTerminals(context);
1015
+ const id = String(args.terminal_id || "");
1016
+ const entry = terminals.get(id);
1017
+ if (!entry) return { ok: false, error: `Unknown background terminal: ${id}` };
1018
+ const waitMs = Math.max(0, Math.min(10000, Number(args.wait_ms || 0)));
1019
+ if (waitMs > 0 && entry.exit === null) {
1020
+ const stdoutLength = entry.stdout.length;
1021
+ const stderrLength = entry.stderr.length;
1022
+ const deadline = Date.now() + waitMs;
1023
+ while (Date.now() < deadline && entry.exit === null && entry.stdout.length === stdoutLength && entry.stderr.length === stderrLength) {
1024
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
1025
+ }
1026
+ }
1027
+ const snapshot = backgroundSnapshot(entry, args.max_bytes);
1028
+ if (!snapshot.running) terminals.delete(id);
1029
+ return snapshot;
1030
+ }
1031
+
1032
+ async function stopBackgroundTerminal(context, args) {
1033
+ const terminals = ensureBackgroundTerminals(context);
1034
+ const id = String(args.terminal_id || "");
1035
+ const entry = terminals.get(id);
1036
+ if (!entry) return { ok: false, error: `Unknown background terminal: ${id}` };
1037
+ const signal = String(args.signal || "SIGTERM");
1038
+ if (entry.exit === null) entry.child.kill(signal);
1039
+ await new Promise((resolve) => setTimeout(resolve, 25));
1040
+ const snapshot = backgroundSnapshot(entry, args.max_bytes);
1041
+ if (entry.exit !== null) terminals.delete(id);
1042
+ return snapshot;
1043
+ }
1044
+
801
1045
  async function requestCompletion(state) {
802
1046
  for (let attempt = 0; attempt <= MAX_API_RETRIES; attempt += 1) {
803
1047
  try {
@@ -817,9 +1061,10 @@ async function requestCompletion(state) {
817
1061
 
818
1062
  async function requestCompletionOnce(state) {
819
1063
  const providerSession = state.providerSessions?.[state.model] || null;
1064
+ const prompt = systemPrompt(state.workspace, state.options);
820
1065
  const body = {
821
1066
  model: state.model,
822
- messages: state.messages,
1067
+ messages: outboundMessages(state),
823
1068
  tools: EFFECTIVE_TOOL_DEFINITIONS,
824
1069
  tool_choice: "auto",
825
1070
  parallel_tool_calls: false,
@@ -831,6 +1076,8 @@ async function requestCompletionOnce(state) {
831
1076
  nox_new_provider_chat: !providerSession,
832
1077
  ...(providerSession ? { nox_provider_session: providerSession } : {}),
833
1078
  qwen_cli_session_id: state.sessionId,
1079
+ nox_prompt_transport: "user-prefix",
1080
+ nox_system_prompt_sha256: crypto.createHash("sha256").update(prompt).digest("hex"),
834
1081
  nox_thinking_level: "medium",
835
1082
  },
836
1083
  };
@@ -874,9 +1121,26 @@ async function collectChatStream(body, { onText, onThinking }) {
874
1121
  let buffer = "";
875
1122
  let content = "";
876
1123
  let reasoning = "";
1124
+ let pendingText = "";
1125
+ let streamingText = false;
877
1126
  const toolCalls = [];
878
1127
  let providerSession = null;
879
1128
  let completed = false;
1129
+ const maybeTextToolPrefix = (value) => {
1130
+ const trimmed = String(value || "").trimStart();
1131
+ return "<NOX_TOOL_CALL>".startsWith(trimmed) || trimmed.startsWith("<NOX_TOOL_CALL>");
1132
+ };
1133
+ const handleText = (chunk) => {
1134
+ if (streamingText) {
1135
+ onText(chunk);
1136
+ return;
1137
+ }
1138
+ pendingText += chunk;
1139
+ if (maybeTextToolPrefix(pendingText)) return;
1140
+ streamingText = true;
1141
+ onText(pendingText);
1142
+ pendingText = "";
1143
+ };
880
1144
  while (true) {
881
1145
  const { value, done } = await reader.read();
882
1146
  if (done) break;
@@ -911,7 +1175,7 @@ async function collectChatStream(body, { onText, onThinking }) {
911
1175
  }
912
1176
  if (typeof delta.content === "string" && delta.content) {
913
1177
  content += delta.content;
914
- onText(delta.content);
1178
+ handleText(delta.content);
915
1179
  }
916
1180
  for (const part of delta.tool_calls || []) {
917
1181
  const index = Number(part.index || 0);
@@ -933,6 +1197,17 @@ async function collectChatStream(body, { onText, onThinking }) {
933
1197
  error.partialResponse = Boolean(content || toolCalls.length);
934
1198
  throw error;
935
1199
  }
1200
+ const textToolCall = parseTextToolCall(content);
1201
+ if (textToolCall && !toolCalls.length) {
1202
+ return {
1203
+ role: "assistant",
1204
+ content: null,
1205
+ ...(reasoning ? { reasoning_content: reasoning } : {}),
1206
+ ...(providerSession ? { provider_session: providerSession } : {}),
1207
+ tool_calls: [textToolCall],
1208
+ };
1209
+ }
1210
+ if (!streamingText && pendingText) onText(pendingText);
936
1211
  if (content && !onText) process.stdout.write("\n");
937
1212
  return {
938
1213
  role: "assistant",
@@ -949,7 +1224,7 @@ async function runAgentTurn(state, userMessage) {
949
1224
  await persistSession(state);
950
1225
  for (let step = 0; step < MAX_TOOL_STEPS; step += 1) {
951
1226
  state.ui?.working(step + 1);
952
- const assistant = await requestCompletion(state);
1227
+ const assistant = normalizeAssistantToolTransport(await requestCompletion(state));
953
1228
  if (assistant.provider_session && typeof assistant.provider_session === "object") {
954
1229
  state.providerSessions[state.model] = assistant.provider_session;
955
1230
  }
@@ -1043,7 +1318,7 @@ async function main() {
1043
1318
  }
1044
1319
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1045
1320
  const ui = interactive ? new TerminalUI(process.stdin, process.stdout) : null;
1046
- const apiKey = ui ? await ui.readApiKey() : configuredApiKey();
1321
+ const apiKey = configuredApiKey() || (ui ? await ui.readApiKey() : "");
1047
1322
  if (!apiKey) {
1048
1323
  throw new Error("Set OPENAI_COMPAT_API_KEY, OPENAI_API_KEY, or NOX_API_KEY before running qwen.");
1049
1324
  }
@@ -1068,6 +1343,7 @@ async function main() {
1068
1343
  workspace,
1069
1344
  sessionId: "",
1070
1345
  providerSessions: {},
1346
+ backgroundTerminals: new Map(),
1071
1347
  messages: [],
1072
1348
  ui,
1073
1349
  historyErrorShown: false,
@@ -1098,7 +1374,7 @@ async function main() {
1098
1374
 
1099
1375
  try {
1100
1376
  if (options.prompt) {
1101
- await runAgentTurn(state, { role: "user", content: options.prompt });
1377
+ await runAgentTurn(state, { role: "user", content: options.clarify ? clarifyPrompt(options.prompt) : options.prompt });
1102
1378
  return;
1103
1379
  }
1104
1380
  if (!ui) throw new Error("Interactive mode requires a terminal or --prompt.");
@@ -1148,13 +1424,16 @@ async function main() {
1148
1424
  const image = parseImageCommand(input);
1149
1425
  const file = parseFileCommand(input);
1150
1426
  const planTask = input.startsWith("/plan ") ? input.slice(6).trim() : "";
1427
+ const clarifyTask = input.startsWith("/clarify ") ? input.slice(9).trim() : "";
1151
1428
  try {
1152
1429
  await runAgentTurn(state,
1153
1430
  image ? await imageMessage(image.filePath, image.prompt)
1154
1431
  : file ? await fileMessage(file.filePath, file.prompt)
1155
1432
  : {
1156
1433
  role: "user",
1157
- content: planTask
1434
+ content: clarifyTask
1435
+ ? clarifyPrompt(clarifyTask)
1436
+ : planTask
1158
1437
  ? `Create and show a concrete plan with update_plan before executing this task, then keep it updated:\n\n${planTask}`
1159
1438
  : input,
1160
1439
  });
package/terminal-ui.js CHANGED
@@ -218,7 +218,7 @@ export class TerminalUI {
218
218
  if (approvalMode === "auto") {
219
219
  this.write(`${this.yellow(" Auto approval is active: model-requested changes and commands run without prompts.")}\n`);
220
220
  }
221
- this.write(`${this.dim(" /plan /image /file /model /sessions /approval /exit")}\n`);
221
+ this.write(`${this.dim(" /plan /clarify /image /file /model /sessions /approval /exit")}\n`);
222
222
  }
223
223
 
224
224
  async readMessage() {