@lazyingart/agintiflow 0.14.0 → 0.15.0
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 +8 -5
- package/docs/agent-runtime-pipe.md +2 -2
- package/package.json +4 -2
- package/public/app.js +9 -4
- package/public/index.html +3 -1
- package/scripts/smoke-auth.js +86 -0
- package/scripts/smoke-capabilities.js +1 -0
- package/scripts/smoke-cli-chat.js +11 -2
- package/scripts/smoke-web-api.js +9 -0
- package/src/auth-onboarding.js +274 -5
- package/src/capabilities.js +5 -1
- package/src/cli.js +37 -18
- package/src/interactive-cli.js +45 -38
- package/src/model-routing.js +9 -0
- package/src/project.js +12 -3
- package/web.js +3 -1
package/README.md
CHANGED
|
@@ -46,13 +46,15 @@ aginti --list-profiles
|
|
|
46
46
|
aginti --sandbox-status
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
On first interactive use, if no
|
|
49
|
+
On first interactive use, if no main model key is detected, `aginti` opens an auth wizard. Use Up/Down to choose DeepSeek, OpenAI, or Qwen, paste the key, and press Enter to save it to the project-local ignored file `.aginti/.env` with `0600` permissions. The wizard then offers the optional auxiliary image key; press Esc to skip. You can rerun it even when keys already exist:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
aginti
|
|
53
|
-
|
|
52
|
+
aginti auth
|
|
53
|
+
aginti auth openai
|
|
54
|
+
# inside chat, use /login or /auth for the same wizard
|
|
54
55
|
# or non-interactively:
|
|
55
56
|
printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
|
|
57
|
+
printf '%s' "$QWEN_API_KEY" | aginti keys set qwen --stdin
|
|
56
58
|
|
|
57
59
|
# optional image-generation auxiliary skill:
|
|
58
60
|
aginti login grsai
|
|
@@ -67,7 +69,7 @@ aginti
|
|
|
67
69
|
aginti chat
|
|
68
70
|
```
|
|
69
71
|
|
|
70
|
-
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/instructions` to inspect `AGINTI.md`, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. During an active run, Enter sends the draft as an ASAP pipe message (`→`) and Tab queues it for after the run (`↳`); ASAP messages are consumed
|
|
72
|
+
Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/instructions` to inspect `AGINTI.md`, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. During an active run, Enter sends the draft as an ASAP pipe message (`→`) and Tab queues it for after the run (`↳`); ASAP messages are consumed before after-finish queued prompts, Alt+Up edits the last piped message, and Shift+Left edits the last after-finish queued message. Idle Esc is ignored so it does not disturb the input panel; during a run, Esc waits when `→` messages are pending and otherwise stops the run cleanly. Ctrl+C always stops and prints the resume command. The input panel always shows the current `cwd` footer and a single live status row, so long goals and tool updates are compacted instead of flooding the transcript. Assistant responses start on a fresh line after the `aginti>` header with a colored response gutter and render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, tables, and red/green patch diff lines. Resuming a session prints the full saved chat history with wrapped messages before the prompt.
|
|
71
73
|
|
|
72
74
|
`aginti init` creates `AGINTI.md` at the project root. This is the editable project-instruction file for both CLI and web runs, similar to `AGENTS.md` or project memory in other agents. Keep durable preferences, commands, and constraints there, but never secrets. You can edit it manually or ask in chat, for example: `update AGINTI.md to remember that this project uses pytest and npm run check`.
|
|
73
75
|
|
|
@@ -269,7 +271,7 @@ Defaults:
|
|
|
269
271
|
| `smart` | DeepSeek | Fast for normal tasks, pro for complex tasks | `AGENT_ROUTING_MODE=smart` |
|
|
270
272
|
| `fast` | DeepSeek | `deepseek-v4-flash` | `DEEPSEEK_FAST_MODEL` |
|
|
271
273
|
| `complex` | DeepSeek | `deepseek-v4-pro` | `DEEPSEEK_PRO_MODEL` |
|
|
272
|
-
| `manual` | DeepSeek/OpenAI | user supplied | `AGENT_PROVIDER`, `LLM_MODEL` |
|
|
274
|
+
| `manual` | DeepSeek/OpenAI/Qwen | user supplied | `AGENT_PROVIDER`, `LLM_MODEL` |
|
|
273
275
|
|
|
274
276
|
Provider credentials:
|
|
275
277
|
|
|
@@ -277,6 +279,7 @@ Provider credentials:
|
|
|
277
279
|
| --- | --- | --- |
|
|
278
280
|
| OpenAI | `OPENAI_API_KEY` | `https://api.openai.com/v1` |
|
|
279
281
|
| DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` |
|
|
282
|
+
| Qwen | `QWEN_API_KEY` | `QWEN_BASE_URL` or DashScope compatible mode |
|
|
280
283
|
|
|
281
284
|
Project-local credentials can be stored without committing secrets:
|
|
282
285
|
|
|
@@ -9,11 +9,11 @@ AgInTiFlow keeps CLI and web runs equivalent by using the project folder as the
|
|
|
9
9
|
|
|
10
10
|
When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The web API exposes `GET /api/sessions/:id/inbox`, `POST /api/sessions/:id/inbox`, `PATCH /api/sessions/:id/inbox/:itemId`, and `DELETE /api/sessions/:id/inbox/:itemId` so browser users can inspect, edit, or remove pending pipe messages before the runner consumes them. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
|
|
11
11
|
|
|
12
|
-
The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. The current command cwd is rendered below the input panel in both idle and running states.
|
|
12
|
+
The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items and before after-finish queued prompts. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. Idle Esc is ignored so it does not redraw the prompt into the transcript. During a run, Esc waits when `→` pipe messages are still pending and stops the run only when no ASAP pipe message is pending; Ctrl+C always stops. The current command cwd is rendered below the input panel in both idle and running states.
|
|
13
13
|
|
|
14
14
|
The web UI uses a related but browser-appropriate pattern. Enter sends and Shift+Enter adds a newline. `Pipe to run` writes an ASAP inbox item shared with CLI. `Queue after finish` keeps a browser-local next prompt and starts it after the current web-owned run finishes. Both lanes render in a pending panel with Edit and Remove buttons instead of terminal-only keybindings.
|
|
15
15
|
|
|
16
|
-
Runs can be stopped without corrupting session state. The CLI listens for
|
|
16
|
+
Runs can be stopped without corrupting session state. The CLI listens for Ctrl+C during an active run, and Esc stops only when no ASAP pipe message is waiting to be applied. The web UI exposes a Stop button plus Esc. Stop paths send an abort signal, persist `session.stopped`, and leave the session resumable through `aginti resume <session-id>`.
|
|
17
17
|
|
|
18
18
|
Default execution is Docker workspace mode with package installs approved inside the sandbox. The project is mounted at `/workspace`; persistent agent toolchain folders are mounted at `/aginti-home`, `/aginti-cache`, and `/aginti-env` from `~/.agintiflow/docker/`. Python, conda, and other language-level environments should be installed under `/aginti-env` so they survive across runs. Apt/apk package changes are ephemeral unless the Docker image is rebuilt.
|
|
19
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"scripts/setup-agent-toolchain-docker.sh",
|
|
43
43
|
"scripts/real-deepseek-capabilities.js",
|
|
44
44
|
"scripts/smoke-auxiliary-tools.js",
|
|
45
|
+
"scripts/smoke-auth.js",
|
|
45
46
|
"scripts/smoke-cli-chat.js",
|
|
46
47
|
"scripts/smoke-coding-tools.js",
|
|
47
48
|
"scripts/smoke-capabilities.js",
|
|
@@ -65,13 +66,14 @@
|
|
|
65
66
|
"setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
|
|
66
67
|
"smoke:coding-tools": "node scripts/smoke-coding-tools.js",
|
|
67
68
|
"smoke:auxiliary-tools": "node scripts/smoke-auxiliary-tools.js",
|
|
69
|
+
"smoke:auth": "node scripts/smoke-auth.js",
|
|
68
70
|
"smoke:cli-chat": "node scripts/smoke-cli-chat.js",
|
|
69
71
|
"smoke:skills": "node scripts/smoke-skills.js",
|
|
70
72
|
"smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
|
|
71
73
|
"smoke:inbox": "node scripts/smoke-inbox.js",
|
|
72
74
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
73
75
|
"real:deepseek": "node scripts/real-deepseek-capabilities.js",
|
|
74
|
-
"test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:capabilities && npm run smoke:skills && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
76
|
+
"test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:capabilities && npm run smoke:skills && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
75
77
|
"pack:dry-run": "npm pack --dry-run",
|
|
76
78
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
77
79
|
},
|
package/public/app.js
CHANGED
|
@@ -22,9 +22,9 @@ const translations = {
|
|
|
22
22
|
projectStatusTitle: "Project folder",
|
|
23
23
|
setupTitle: "Provider setup",
|
|
24
24
|
setupHelp:
|
|
25
|
-
"DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local
|
|
25
|
+
"DeepSeek/OpenAI/Qwen keys are missing. Use mock mode, export an env var, or save a project-local model key.",
|
|
26
26
|
setupEnvHelp:
|
|
27
|
-
"Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, LLM_API_KEY, and optional GRSAI for image generation. Mock mode remains available for local tests.",
|
|
27
|
+
"Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, LLM_API_KEY, and optional GRSAI for image generation. Mock mode remains available for local tests.",
|
|
28
28
|
setupProviderLabel: "Provider",
|
|
29
29
|
setupKeyLabel: "API key",
|
|
30
30
|
saveKeyButton: "Save local key",
|
|
@@ -714,6 +714,7 @@ const ariaLabelNodes = [...document.querySelectorAll("[data-i18n-aria-label]")];
|
|
|
714
714
|
const defaults = {
|
|
715
715
|
openai: "gpt-5.4-mini",
|
|
716
716
|
deepseek: "deepseek-v4-flash",
|
|
717
|
+
qwen: "qwen-plus",
|
|
717
718
|
mock: "mock-agent",
|
|
718
719
|
};
|
|
719
720
|
|
|
@@ -783,12 +784,14 @@ function renderKeyStatus(status = lastKeyStatus) {
|
|
|
783
784
|
if (!status) return;
|
|
784
785
|
keyStatusEl.textContent = `${t("keysLabel")}: OpenAI ${
|
|
785
786
|
status.openai ? t("availableLabel") : t("missingLabel")
|
|
786
|
-
} · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} ·
|
|
787
|
+
} · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · Qwen ${
|
|
788
|
+
status.qwen ? t("availableLabel") : t("missingLabel")
|
|
789
|
+
} · GRS AI ${
|
|
787
790
|
status.grsai ? t("availableLabel") : t("missingLabel")
|
|
788
791
|
} · ${t("mockLabel")} ${
|
|
789
792
|
status.mock ? t("availableLabel") : t("missingLabel")
|
|
790
793
|
}`;
|
|
791
|
-
if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek);
|
|
794
|
+
if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek || status.qwen);
|
|
792
795
|
}
|
|
793
796
|
|
|
794
797
|
function renderProjectStatus(info = projectInfo) {
|
|
@@ -2278,6 +2281,7 @@ providerField.addEventListener("change", () => {
|
|
|
2278
2281
|
!modelField.value.trim() ||
|
|
2279
2282
|
modelField.value === defaults.openai ||
|
|
2280
2283
|
modelField.value === defaults.deepseek ||
|
|
2284
|
+
modelField.value === defaults.qwen ||
|
|
2281
2285
|
modelField.value === defaults.mock
|
|
2282
2286
|
) {
|
|
2283
2287
|
modelField.value = defaults[providerField.value] || "";
|
|
@@ -2513,6 +2517,7 @@ async function loadConfig() {
|
|
|
2513
2517
|
taskProfiles = data.taskProfiles || [];
|
|
2514
2518
|
projectInfo = data.project || null;
|
|
2515
2519
|
defaults.openai = data.defaults?.openai?.model || defaults.openai;
|
|
2520
|
+
defaults.qwen = data.defaults?.qwen?.model || defaults.qwen;
|
|
2516
2521
|
defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
|
|
2517
2522
|
defaults.mock = data.defaults?.mock?.model || defaults.mock;
|
|
2518
2523
|
|
package/public/index.html
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.
|
|
49
49
|
</p>
|
|
50
50
|
<p class="subtle" data-i18n="setupEnvHelp">
|
|
51
|
-
Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
|
|
51
|
+
Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
|
|
52
52
|
</p>
|
|
53
53
|
</div>
|
|
54
54
|
<div class="grid">
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
<select id="setup-provider">
|
|
58
58
|
<option value="deepseek">DeepSeek</option>
|
|
59
59
|
<option value="openai">OpenAI</option>
|
|
60
|
+
<option value="qwen">Qwen</option>
|
|
60
61
|
<option value="grsai">GRS AI image</option>
|
|
61
62
|
</select>
|
|
62
63
|
</label>
|
|
@@ -88,6 +89,7 @@
|
|
|
88
89
|
<select id="provider" name="provider">
|
|
89
90
|
<option value="deepseek">DeepSeek</option>
|
|
90
91
|
<option value="openai">OpenAI</option>
|
|
92
|
+
<option value="qwen">Qwen</option>
|
|
91
93
|
<option value="mock" data-i18n="mockProviderOption">Mock local</option>
|
|
92
94
|
</select>
|
|
93
95
|
</label>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { normalizeAuthProvider } from "../src/auth-onboarding.js";
|
|
8
|
+
import { getProviderDefaults } from "../src/model-routing.js";
|
|
9
|
+
import { providerKeyStatus, setProviderKey } from "../src/project.js";
|
|
10
|
+
|
|
11
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auth-"));
|
|
13
|
+
|
|
14
|
+
function assert(condition, message) {
|
|
15
|
+
if (!condition) throw new Error(message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function runCli(args, stdin = "") {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), ...args], {
|
|
21
|
+
cwd: tempRoot,
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
AGINTIFLOW_RUNTIME_DIR: "",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
let stdout = "";
|
|
29
|
+
let stderr = "";
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
child.kill("SIGTERM");
|
|
32
|
+
reject(new Error("auth smoke command timed out"));
|
|
33
|
+
}, 12000);
|
|
34
|
+
child.stdout.on("data", (chunk) => {
|
|
35
|
+
stdout += String(chunk);
|
|
36
|
+
});
|
|
37
|
+
child.stderr.on("data", (chunk) => {
|
|
38
|
+
stderr += String(chunk);
|
|
39
|
+
});
|
|
40
|
+
child.on("error", (error) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
reject(error);
|
|
43
|
+
});
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
if (code === 0) resolve(stdout);
|
|
47
|
+
else reject(new Error(`auth smoke command failed ${code}\n${stdout}\n${stderr}`));
|
|
48
|
+
});
|
|
49
|
+
child.stdin.end(stdin);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
assert(normalizeAuthProvider("auxilliary") === "grsai", "auxilliary alias did not normalize to grsai");
|
|
55
|
+
assert(normalizeAuthProvider("qwen") === "qwen", "qwen provider did not normalize");
|
|
56
|
+
const qwenDefaults = getProviderDefaults("qwen");
|
|
57
|
+
assert(qwenDefaults.provider === "qwen" && qwenDefaults.model, "qwen provider defaults are not available");
|
|
58
|
+
|
|
59
|
+
await setProviderKey(tempRoot, "qwen", "test-qwen-key");
|
|
60
|
+
let status = providerKeyStatus(tempRoot);
|
|
61
|
+
assert(status.qwen, "qwen key status was not detected");
|
|
62
|
+
assert(status.envVars.qwen.includes("QWEN_API_KEY"), "qwen env var name was not reported");
|
|
63
|
+
|
|
64
|
+
await runCli(["keys", "set", "openai", "--stdin"], "test-openai-key");
|
|
65
|
+
await runCli(["keys", "set", "grsai", "--stdin"], "test-grsai-key");
|
|
66
|
+
status = providerKeyStatus(tempRoot);
|
|
67
|
+
assert(status.openai && status.grsai && status.qwen, "stored auth keys were not detected");
|
|
68
|
+
|
|
69
|
+
const keysOutput = await runCli(["keys", "status"]);
|
|
70
|
+
assert(keysOutput.includes("qwen=available"), "keys status did not include qwen");
|
|
71
|
+
assert(!keysOutput.includes("test-openai-key") && !keysOutput.includes("test-qwen-key"), "keys status leaked a raw key");
|
|
72
|
+
|
|
73
|
+
console.log(
|
|
74
|
+
JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
ok: true,
|
|
77
|
+
projectRoot: tempRoot,
|
|
78
|
+
checks: ["normalize-auth-provider", "qwen-defaults", "qwen-key-status", "cli-key-status-redacted"],
|
|
79
|
+
},
|
|
80
|
+
null,
|
|
81
|
+
2
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
} finally {
|
|
85
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
86
|
+
}
|
|
@@ -37,6 +37,7 @@ try {
|
|
|
37
37
|
assert(capabilities.project.instructionsPresent, "capabilities did not report AGINTI.md");
|
|
38
38
|
assert(capabilities.project.sharedSessionFolder, "capabilities did not report shared session folder");
|
|
39
39
|
assert(capabilities.keys?.mock === true, "capabilities did not report mock availability");
|
|
40
|
+
assert(typeof capabilities.keys?.qwen === "boolean", "capabilities did not report qwen key status");
|
|
40
41
|
assert(
|
|
41
42
|
capabilities.checks.some((check) => check.name === "npm-prefix-test-policy" && check.ok),
|
|
42
43
|
"npm --prefix test policy is not allowed"
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { buildPromptLayout, stripMarkdown } from "../src/interactive-cli.js";
|
|
7
|
+
import { buildPromptLayout, classifyEscapeAction, stripMarkdown } from "../src/interactive-cli.js";
|
|
8
8
|
|
|
9
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
10
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-cli-chat-"));
|
|
@@ -112,6 +112,15 @@ try {
|
|
|
112
112
|
if (!queuedText.includes("run running · tool: apply_patch") || !queuedText.includes("→ apply this") || !queuedText.includes("↳ run this") || !queuedText.includes("cwd /tmp/aginti-project")) {
|
|
113
113
|
throw new Error("terminal prompt layout did not render live input queue and cwd footer");
|
|
114
114
|
}
|
|
115
|
+
if (classifyEscapeAction({ active: false }) !== "noop") {
|
|
116
|
+
throw new Error("idle Esc should not redraw or clear the prompt");
|
|
117
|
+
}
|
|
118
|
+
if (classifyEscapeAction({ active: true, pendingAsap: [{ content: "apply now" }] }) !== "wait-for-asap") {
|
|
119
|
+
throw new Error("active Esc should wait when ASAP pipe messages are pending");
|
|
120
|
+
}
|
|
121
|
+
if (classifyEscapeAction({ active: true, pendingAsap: [] }) !== "abort") {
|
|
122
|
+
throw new Error("active Esc should abort when no ASAP pipe messages are pending");
|
|
123
|
+
}
|
|
115
124
|
|
|
116
125
|
await runCli(["init"], "");
|
|
117
126
|
const instructions = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
|
|
@@ -163,7 +172,7 @@ try {
|
|
|
163
172
|
{
|
|
164
173
|
ok: true,
|
|
165
174
|
projectRoot: tempRoot,
|
|
166
|
-
checks: ["markdown-render", "markdown-table-no-duplicate", "patch-diff-render", "prompt-layout", "live-input-status-layout", "agent-response-gutter", "aginti-md", "instructions-command", "skills-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest", "resume-history-full"],
|
|
175
|
+
checks: ["markdown-render", "markdown-table-no-duplicate", "patch-diff-render", "prompt-layout", "escape-policy", "live-input-status-layout", "agent-response-gutter", "aginti-md", "instructions-command", "skills-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest", "resume-history-full"],
|
|
167
176
|
},
|
|
168
177
|
null,
|
|
169
178
|
2
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -87,6 +87,7 @@ try {
|
|
|
87
87
|
|
|
88
88
|
const keyStatus = await fetchJson("/api/keys/status");
|
|
89
89
|
if (typeof keyStatus.keyStatus?.deepseek !== "boolean") throw new Error("key status endpoint is invalid");
|
|
90
|
+
if (typeof keyStatus.keyStatus?.qwen !== "boolean") throw new Error("qwen key status is missing");
|
|
90
91
|
if ("localEnvPath" in keyStatus.keyStatus) throw new Error("key status leaked a local env path");
|
|
91
92
|
const capabilities = await fetchJson("/api/capabilities");
|
|
92
93
|
if (capabilities.project?.root !== runtimeDir || !Array.isArray(capabilities.checks)) {
|
|
@@ -106,6 +107,14 @@ try {
|
|
|
106
107
|
if (!savedKey.ok || !savedKey.keyStatus?.deepseek || "apiKey" in savedKey || "key" in savedKey) {
|
|
107
108
|
throw new Error("local key save endpoint returned invalid or sensitive data");
|
|
108
109
|
}
|
|
110
|
+
const savedQwenKey = await fetchJson("/api/keys/qwen", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ apiKey: "test-qwen-key-not-real" }),
|
|
114
|
+
});
|
|
115
|
+
if (!savedQwenKey.ok || !savedQwenKey.keyStatus?.qwen || "apiKey" in savedQwenKey || "key" in savedQwenKey) {
|
|
116
|
+
throw new Error("qwen local key save endpoint returned invalid or sensitive data");
|
|
117
|
+
}
|
|
109
118
|
|
|
110
119
|
const status = await fetchJson("/api/sandbox/status");
|
|
111
120
|
if (!status.status?.workspaceReadable) throw new Error("sandbox status did not report a readable workspace");
|
package/src/auth-onboarding.js
CHANGED
|
@@ -1,8 +1,60 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
|
+
import { emitKeypressEvents } from "node:readline";
|
|
2
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
4
|
import { Writable } from "node:stream";
|
|
4
5
|
import { providerKeyStatus, setProviderKey } from "./project.js";
|
|
5
6
|
|
|
7
|
+
export const MAIN_AUTH_PROVIDERS = [
|
|
8
|
+
{
|
|
9
|
+
id: "deepseek",
|
|
10
|
+
label: "DeepSeek",
|
|
11
|
+
keyName: "DEEPSEEK_API_KEY",
|
|
12
|
+
description: "default fast/pro route",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "openai",
|
|
16
|
+
label: "OpenAI",
|
|
17
|
+
keyName: "OPENAI_API_KEY",
|
|
18
|
+
description: "OpenAI-compatible fallback",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "qwen",
|
|
22
|
+
label: "Qwen",
|
|
23
|
+
keyName: "QWEN_API_KEY",
|
|
24
|
+
description: "Qwen OpenAI-compatible route",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const AUXILIARY_AUTH_PROVIDER = {
|
|
29
|
+
id: "grsai",
|
|
30
|
+
label: "GRS AI / Nano Banana",
|
|
31
|
+
keyName: "GRSAI",
|
|
32
|
+
description: "optional image generation",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const AUTH_ALIASES = {
|
|
36
|
+
auxiliary: "grsai",
|
|
37
|
+
auxilliary: "grsai",
|
|
38
|
+
image: "grsai",
|
|
39
|
+
imagegen: "grsai",
|
|
40
|
+
grs: "grsai",
|
|
41
|
+
grsai: "grsai",
|
|
42
|
+
deepseek: "deepseek",
|
|
43
|
+
ds: "deepseek",
|
|
44
|
+
openai: "openai",
|
|
45
|
+
qwen: "qwen",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function normalizeAuthProvider(provider = "", fallback = "deepseek") {
|
|
49
|
+
const normalized = AUTH_ALIASES[String(provider || "").trim().toLowerCase()] || String(provider || "").trim().toLowerCase();
|
|
50
|
+
return ["deepseek", "openai", "qwen", "grsai"].includes(normalized) ? normalized : fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function providerLabel(provider = "") {
|
|
54
|
+
const match = [...MAIN_AUTH_PROVIDERS, AUXILIARY_AUTH_PROVIDER].find((item) => item.id === provider);
|
|
55
|
+
return match?.label || provider;
|
|
56
|
+
}
|
|
57
|
+
|
|
6
58
|
class MutedWritable extends Writable {
|
|
7
59
|
constructor(target) {
|
|
8
60
|
super();
|
|
@@ -16,8 +68,55 @@ class MutedWritable extends Writable {
|
|
|
16
68
|
}
|
|
17
69
|
}
|
|
18
70
|
|
|
19
|
-
export async function
|
|
20
|
-
if (!input.isTTY || !output.isTTY) return "";
|
|
71
|
+
export async function promptSecret(promptText, { allowEscape = true } = {}) {
|
|
72
|
+
if (!input.isTTY || !output.isTTY) return { value: "", skipped: true };
|
|
73
|
+
|
|
74
|
+
if (typeof input.setRawMode === "function") {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
emitKeypressEvents(input);
|
|
77
|
+
const wasRaw = Boolean(input.isRaw);
|
|
78
|
+
let value = "";
|
|
79
|
+
|
|
80
|
+
const cleanup = () => {
|
|
81
|
+
input.off("keypress", handler);
|
|
82
|
+
if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
|
|
83
|
+
input.pause();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const finish = (result) => {
|
|
87
|
+
cleanup();
|
|
88
|
+
output.write("\n");
|
|
89
|
+
resolve(result);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handler = (str = "", key = {}) => {
|
|
93
|
+
if (key.ctrl && key.name === "c") {
|
|
94
|
+
cleanup();
|
|
95
|
+
reject(Object.assign(new Error("Interrupted by ctrl-c."), { name: "AbortError", code: "ABORT_ERR" }));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (allowEscape && key.name === "escape") {
|
|
99
|
+
finish({ value: "", skipped: true });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (key.name === "return" || key.name === "enter" || key.sequence === "\r" || str === "\r") {
|
|
103
|
+
finish({ value: value.trim(), skipped: !value.trim() });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.name === "backspace" || key.name === "delete") {
|
|
107
|
+
value = value.slice(0, -1);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (key.ctrl || key.meta || key.sequence?.startsWith("\x1b")) return;
|
|
111
|
+
if (str) value += str.replace(/\r|\n/g, "");
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
output.write(promptText);
|
|
115
|
+
input.resume();
|
|
116
|
+
input.setRawMode(true);
|
|
117
|
+
input.on("keypress", handler);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
21
120
|
|
|
22
121
|
const mutedOutput = new MutedWritable(output);
|
|
23
122
|
const rl = readline.createInterface({
|
|
@@ -31,19 +130,132 @@ export async function promptHidden(promptText) {
|
|
|
31
130
|
mutedOutput.muted = true;
|
|
32
131
|
const value = await rl.question("");
|
|
33
132
|
output.write("\n");
|
|
34
|
-
|
|
133
|
+
const text = String(value || "").trim();
|
|
134
|
+
return { value: text, skipped: !text };
|
|
35
135
|
} finally {
|
|
36
136
|
mutedOutput.muted = false;
|
|
37
137
|
rl.close();
|
|
38
138
|
}
|
|
39
139
|
}
|
|
40
140
|
|
|
141
|
+
export async function promptHidden(promptText) {
|
|
142
|
+
const result = await promptSecret(promptText, { allowEscape: true });
|
|
143
|
+
return typeof result === "string" ? result : result.value || "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderProviderPicker({ providers, selected, title, status }) {
|
|
147
|
+
const lines = [
|
|
148
|
+
`\n${title}`,
|
|
149
|
+
"Use Up/Down to choose, Enter to confirm, Esc to go back/skip.",
|
|
150
|
+
`Current key status: ${status}`,
|
|
151
|
+
"",
|
|
152
|
+
...providers.map((provider, index) => {
|
|
153
|
+
const cursor = index === selected ? ">" : " ";
|
|
154
|
+
return `${cursor} ${provider.label.padEnd(10)} ${provider.keyName.padEnd(16)} ${provider.description}`;
|
|
155
|
+
}),
|
|
156
|
+
];
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function chooseAuthProvider({
|
|
161
|
+
providers = MAIN_AUTH_PROVIDERS,
|
|
162
|
+
title = "Choose main model API key",
|
|
163
|
+
initialProvider = "deepseek",
|
|
164
|
+
projectRoot = process.cwd(),
|
|
165
|
+
} = {}) {
|
|
166
|
+
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
|
|
167
|
+
return normalizeAuthProvider(initialProvider, providers[0]?.id || "deepseek");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
emitKeypressEvents(input);
|
|
172
|
+
const wasRaw = Boolean(input.isRaw);
|
|
173
|
+
const status = providerKeyStatus(projectRoot);
|
|
174
|
+
let selected = Math.max(
|
|
175
|
+
providers.findIndex((provider) => provider.id === normalizeAuthProvider(initialProvider, providers[0]?.id)),
|
|
176
|
+
0
|
|
177
|
+
);
|
|
178
|
+
let renderedLines = 0;
|
|
179
|
+
|
|
180
|
+
const cleanup = () => {
|
|
181
|
+
input.off("keypress", handler);
|
|
182
|
+
if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
|
|
183
|
+
input.pause();
|
|
184
|
+
output.write("\x1b[?25h");
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const clear = () => {
|
|
188
|
+
if (renderedLines <= 0) return;
|
|
189
|
+
output.write(`\x1b[${renderedLines - 1}A`);
|
|
190
|
+
for (let index = 0; index < renderedLines; index += 1) {
|
|
191
|
+
output.write("\r\x1b[2K");
|
|
192
|
+
if (index < renderedLines - 1) output.write("\x1b[1B");
|
|
193
|
+
}
|
|
194
|
+
output.write(`\x1b[${renderedLines - 1}A\r`);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const render = () => {
|
|
198
|
+
clear();
|
|
199
|
+
const providerStatus = providers
|
|
200
|
+
.map((provider) => `${provider.label}=${status[provider.id] ? "available" : "missing"}`)
|
|
201
|
+
.join(" · ");
|
|
202
|
+
const text = renderProviderPicker({
|
|
203
|
+
providers,
|
|
204
|
+
selected,
|
|
205
|
+
title,
|
|
206
|
+
status: providerStatus,
|
|
207
|
+
});
|
|
208
|
+
const lines = text.split("\n");
|
|
209
|
+
renderedLines = lines.length;
|
|
210
|
+
output.write(`\x1b[?25l${text}`);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const finish = (value) => {
|
|
214
|
+
clear();
|
|
215
|
+
cleanup();
|
|
216
|
+
resolve(value);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const handler = (_str = "", key = {}) => {
|
|
220
|
+
if (key.ctrl && key.name === "c") {
|
|
221
|
+
clear();
|
|
222
|
+
cleanup();
|
|
223
|
+
reject(Object.assign(new Error("Interrupted by ctrl-c."), { name: "AbortError", code: "ABORT_ERR" }));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (key.name === "escape") {
|
|
227
|
+
finish("");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (key.name === "up") {
|
|
231
|
+
selected = (selected - 1 + providers.length) % providers.length;
|
|
232
|
+
render();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (key.name === "down") {
|
|
236
|
+
selected = (selected + 1) % providers.length;
|
|
237
|
+
render();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (key.name === "return" || key.name === "enter" || key.sequence === "\r") {
|
|
241
|
+
finish(providers[selected].id);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
input.resume();
|
|
246
|
+
input.setRawMode(true);
|
|
247
|
+
input.on("keypress", handler);
|
|
248
|
+
render();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
41
252
|
export function shouldPromptForDeepSeek(args = {}, projectRoot = process.cwd()) {
|
|
42
253
|
const provider = String(args.provider || "").toLowerCase();
|
|
43
|
-
if (provider === "mock" || provider === "openai") return false;
|
|
254
|
+
if (provider === "mock" || provider === "openai" || provider === "qwen") return false;
|
|
44
255
|
if (process.env.AGINTIFLOW_NO_AUTH_PROMPT === "1") return false;
|
|
45
256
|
if (!input.isTTY || !output.isTTY) return false;
|
|
46
|
-
|
|
257
|
+
const status = providerKeyStatus(projectRoot);
|
|
258
|
+
return !status.deepseek && !status.openai && !status.qwen;
|
|
47
259
|
}
|
|
48
260
|
|
|
49
261
|
export async function promptAndSaveDeepSeekKey(projectRoot = process.cwd(), options = {}) {
|
|
@@ -60,3 +272,60 @@ export async function promptAndSaveDeepSeekKey(projectRoot = process.cwd(), opti
|
|
|
60
272
|
path: result.path,
|
|
61
273
|
};
|
|
62
274
|
}
|
|
275
|
+
|
|
276
|
+
export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
277
|
+
const status = providerKeyStatus(projectRoot);
|
|
278
|
+
const initialProvider = normalizeAuthProvider(options.provider || options.initialProvider || "deepseek", "deepseek");
|
|
279
|
+
const directProvider =
|
|
280
|
+
options.provider && ["deepseek", "openai", "qwen", "grsai"].includes(normalizeAuthProvider(options.provider, ""))
|
|
281
|
+
? normalizeAuthProvider(options.provider)
|
|
282
|
+
: "";
|
|
283
|
+
const mainProvider =
|
|
284
|
+
directProvider === "grsai"
|
|
285
|
+
? ""
|
|
286
|
+
: directProvider ||
|
|
287
|
+
(await chooseAuthProvider({
|
|
288
|
+
projectRoot,
|
|
289
|
+
initialProvider,
|
|
290
|
+
title: "Choose the main model API key to save",
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
const saved = [];
|
|
294
|
+
const skipped = [];
|
|
295
|
+
|
|
296
|
+
if (mainProvider) {
|
|
297
|
+
const current = status[mainProvider] ? "currently available; paste a new key to replace, or Esc to keep existing" : "missing";
|
|
298
|
+
const prompt = `${providerLabel(mainProvider)} main API key (${current}) [hidden]: `;
|
|
299
|
+
const secret = await promptSecret(prompt, { allowEscape: true });
|
|
300
|
+
if (secret.value) {
|
|
301
|
+
const result = await setProviderKey(projectRoot, mainProvider, secret.value);
|
|
302
|
+
saved.push(result);
|
|
303
|
+
} else {
|
|
304
|
+
skipped.push({ provider: mainProvider, reason: secret.skipped ? "skipped" : "empty" });
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
skipped.push({ provider: "main", reason: "skipped" });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (options.includeAuxiliary !== false && directProvider !== "grsai") {
|
|
311
|
+
const auxStatus = providerKeyStatus(projectRoot);
|
|
312
|
+
const current = auxStatus.grsai ? "currently available; paste a new key to replace, or Esc to skip" : "optional; paste key or Esc to skip";
|
|
313
|
+
const secret = await promptSecret(`${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key (${current}) [hidden]: `, {
|
|
314
|
+
allowEscape: true,
|
|
315
|
+
});
|
|
316
|
+
if (secret.value) {
|
|
317
|
+
const result = await setProviderKey(projectRoot, "grsai", secret.value);
|
|
318
|
+
saved.push(result);
|
|
319
|
+
} else {
|
|
320
|
+
skipped.push({ provider: "grsai", reason: "skipped" });
|
|
321
|
+
}
|
|
322
|
+
} else if (directProvider === "grsai") {
|
|
323
|
+
const secret = await promptSecret(`${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key [hidden]: `, {
|
|
324
|
+
allowEscape: true,
|
|
325
|
+
});
|
|
326
|
+
if (secret.value) saved.push(await setProviderKey(projectRoot, "grsai", secret.value));
|
|
327
|
+
else skipped.push({ provider: "grsai", reason: "skipped" });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { saved, skipped };
|
|
331
|
+
}
|
package/src/capabilities.js
CHANGED
|
@@ -147,6 +147,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
147
147
|
capability("docker", Boolean(dockerStatus?.dockerAvailable), dockerStatus || {}),
|
|
148
148
|
capability("deepseek-key", keyStatus.deepseek, { envVars: keyStatus.envVars.deepseek }),
|
|
149
149
|
capability("openai-key", keyStatus.openai, { envVars: keyStatus.envVars.openai }),
|
|
150
|
+
capability("qwen-key", keyStatus.qwen, { envVars: keyStatus.envVars.qwen }),
|
|
150
151
|
capability("grsai-key", keyStatus.grsai, {
|
|
151
152
|
envVars: keyStatus.envVars.grsai,
|
|
152
153
|
setup: "Optional for image generation. Run `aginti login grsai` or use `/auxilliary grsai` in chat.",
|
|
@@ -202,6 +203,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
202
203
|
keys: {
|
|
203
204
|
deepseek: keyStatus.deepseek,
|
|
204
205
|
openai: keyStatus.openai,
|
|
206
|
+
qwen: keyStatus.qwen,
|
|
205
207
|
grsai: keyStatus.grsai,
|
|
206
208
|
mock: true,
|
|
207
209
|
localEnv: keyStatus.localEnv,
|
|
@@ -266,7 +268,9 @@ export function printCapabilityReport(report) {
|
|
|
266
268
|
console.log(
|
|
267
269
|
`keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
|
|
268
270
|
report.keys.openai ? "available" : "missing"
|
|
269
|
-
}
|
|
271
|
+
} qwen=${report.keys.qwen ? "available" : "missing"} grsai=${
|
|
272
|
+
report.keys.grsai ? "available" : "missing"
|
|
273
|
+
} mock=available localEnv=${report.keys.localEnv}`
|
|
270
274
|
);
|
|
271
275
|
for (const check of report.checks) {
|
|
272
276
|
const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
|
package/src/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "./project.js";
|
|
17
17
|
import { listTaskProfiles } from "./task-profiles.js";
|
|
18
18
|
import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
|
|
19
|
-
import {
|
|
19
|
+
import { normalizeAuthProvider, promptHidden, runAuthWizard, shouldPromptForDeepSeek } from "./auth-onboarding.js";
|
|
20
20
|
import { listSkills, selectSkillsForGoal } from "./skill-library.js";
|
|
21
21
|
import fs from "node:fs/promises";
|
|
22
22
|
import path from "node:path";
|
|
@@ -267,13 +267,14 @@ export function parseArgs(argv) {
|
|
|
267
267
|
|
|
268
268
|
function printUsage() {
|
|
269
269
|
console.log(
|
|
270
|
-
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti skills [query] OR aginti login deepseek|openai|grsai OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--list-skills] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
270
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti skills [query] OR aginti auth [deepseek|openai|qwen|grsai] OR aginti login [deepseek|openai|qwen|grsai] OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--list-skills] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
271
271
|
);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
function providerLabel(provider) {
|
|
275
275
|
const normalized = String(provider || "").toLowerCase();
|
|
276
276
|
if (normalized === "openai") return "OpenAI";
|
|
277
|
+
if (normalized === "qwen") return "Qwen";
|
|
277
278
|
if (normalized === "grsai" || normalized === "auxiliary" || normalized === "auxilliary") return "GRSAI";
|
|
278
279
|
return "DeepSeek";
|
|
279
280
|
}
|
|
@@ -355,7 +356,9 @@ function printDoctorReport(report) {
|
|
|
355
356
|
console.log(
|
|
356
357
|
`keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
|
|
357
358
|
report.keys.openai ? "available" : "missing"
|
|
358
|
-
}
|
|
359
|
+
} qwen=${report.keys.qwen ? "available" : "missing"} grsai=${
|
|
360
|
+
report.keys.grsai ? "available" : "missing"
|
|
361
|
+
} mock=available localEnv=${report.project.localEnvPresent}`
|
|
359
362
|
);
|
|
360
363
|
console.log(
|
|
361
364
|
`sandbox=${report.sandbox?.sandboxMode || "unknown"} docker=${
|
|
@@ -377,16 +380,14 @@ async function readStdin() {
|
|
|
377
380
|
|
|
378
381
|
async function ensureDeepSeekKeyForOneShot(args) {
|
|
379
382
|
if (!shouldPromptForDeepSeek(args, process.cwd())) return true;
|
|
380
|
-
console.log("
|
|
381
|
-
console.log("
|
|
382
|
-
const result = await
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (result.saved) {
|
|
386
|
-
console.log(`saved ${result.keyName} to project-local ignored env`);
|
|
383
|
+
console.log("No main model API key is configured for this project.");
|
|
384
|
+
console.log("Choose DeepSeek, OpenAI, or Qwen, then paste a key to save in `.aginti/.env` with 0600 permissions.");
|
|
385
|
+
const result = await runAuthWizard(process.cwd(), { provider: args.provider || "" });
|
|
386
|
+
printAuthWizardResult(result);
|
|
387
|
+
if (result.saved.some((item) => item.provider !== "grsai")) {
|
|
387
388
|
return true;
|
|
388
389
|
}
|
|
389
|
-
console.error("No
|
|
390
|
+
console.error("No main key saved. Run `aginti auth` later, or use `--provider mock` for local tests.");
|
|
390
391
|
return false;
|
|
391
392
|
}
|
|
392
393
|
|
|
@@ -397,9 +398,9 @@ async function handleKeyCommand(argv) {
|
|
|
397
398
|
console.log(
|
|
398
399
|
`keys: deepseek=${status.deepseek ? "available" : "missing"} openai=${
|
|
399
400
|
status.openai ? "available" : "missing"
|
|
400
|
-
} grsai=${status.grsai ? "available" : "missing"} mock=available localEnv=${status.localEnv}`
|
|
401
|
+
} qwen=${status.qwen ? "available" : "missing"} grsai=${status.grsai ? "available" : "missing"} mock=available localEnv=${status.localEnv}`
|
|
401
402
|
);
|
|
402
|
-
console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; image=GRSAI or GRSAI_API_KEY");
|
|
403
|
+
console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; Qwen=QWEN_API_KEY; image=GRSAI or GRSAI_API_KEY");
|
|
403
404
|
return;
|
|
404
405
|
}
|
|
405
406
|
|
|
@@ -415,10 +416,22 @@ async function handleKeyCommand(argv) {
|
|
|
415
416
|
return;
|
|
416
417
|
}
|
|
417
418
|
|
|
418
|
-
console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|grsai [--stdin]");
|
|
419
|
+
console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|qwen|grsai [--stdin]");
|
|
419
420
|
process.exit(1);
|
|
420
421
|
}
|
|
421
422
|
|
|
423
|
+
function printAuthWizardResult(result) {
|
|
424
|
+
if (result.saved.length > 0) {
|
|
425
|
+
for (const item of result.saved) {
|
|
426
|
+
console.log(`saved ${item.provider} key to project-local ignored env (${item.keyName})`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (result.saved.length === 0) console.log("No key saved.");
|
|
430
|
+
if (result.skipped.length > 0) {
|
|
431
|
+
console.log(`skipped: ${result.skipped.map((item) => item.provider).join(", ")}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
422
435
|
async function handleSessionsCommand(argv) {
|
|
423
436
|
const [verb = "list", sessionId = ""] = argv;
|
|
424
437
|
if (verb === "list") {
|
|
@@ -532,16 +545,22 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
532
545
|
return;
|
|
533
546
|
}
|
|
534
547
|
|
|
535
|
-
if (argv[0] === "login") {
|
|
536
|
-
const provider = argv[1] || "
|
|
548
|
+
if (argv[0] === "auth" || argv[0] === "login") {
|
|
549
|
+
const provider = normalizeAuthProvider(argv[1] || "", "");
|
|
550
|
+
if (argv[0] === "auth" || (!provider && process.stdin.isTTY)) {
|
|
551
|
+
const result = await runAuthWizard(process.cwd(), { provider });
|
|
552
|
+
printAuthWizardResult(result);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const target = provider || "deepseek";
|
|
537
556
|
const key = argv.includes("--stdin") || !process.stdin.isTTY
|
|
538
557
|
? await readStdin()
|
|
539
|
-
: await promptHidden(`${providerLabel(
|
|
558
|
+
: await promptHidden(`${providerLabel(target)} API key/token: `);
|
|
540
559
|
if (!key) {
|
|
541
560
|
console.error("No key saved.");
|
|
542
561
|
process.exit(1);
|
|
543
562
|
}
|
|
544
|
-
const result = await setProviderKey(process.cwd(),
|
|
563
|
+
const result = await setProviderKey(process.cwd(), target, key);
|
|
545
564
|
console.log(`saved ${result.provider} key to project-local ignored env (${result.keyName})`);
|
|
546
565
|
return;
|
|
547
566
|
}
|
package/src/interactive-cli.js
CHANGED
|
@@ -3,11 +3,11 @@ import { emitKeypressEvents } from "node:readline";
|
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
4
|
import { runAgent } from "./agent-runner.js";
|
|
5
5
|
import { loadConfig } from "./config.js";
|
|
6
|
-
import { initProject, listProjectSessions, projectPaths, providerKeyStatus, readProjectInstructions
|
|
6
|
+
import { initProject, listProjectSessions, projectPaths, providerKeyStatus, readProjectInstructions } from "./project.js";
|
|
7
7
|
import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
|
|
8
8
|
import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
|
|
9
9
|
import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
|
|
10
|
-
import {
|
|
10
|
+
import { normalizeAuthProvider, runAuthWizard, shouldPromptForDeepSeek } from "./auth-onboarding.js";
|
|
11
11
|
import { SessionStore } from "./session-store.js";
|
|
12
12
|
import { listSkills, selectSkillsForGoal } from "./skill-library.js";
|
|
13
13
|
|
|
@@ -474,8 +474,8 @@ function printHelp() {
|
|
|
474
474
|
"Commands:",
|
|
475
475
|
" /help Show this help.",
|
|
476
476
|
" /status Show active route, workspace, sandbox, and session.",
|
|
477
|
-
" /login [deepseek|openai|grsai]
|
|
478
|
-
" /auth [deepseek|openai|grsai] Alias for /login.",
|
|
477
|
+
" /login [deepseek|openai|qwen|grsai] Pick, paste, and save project-local API keys.",
|
|
478
|
+
" /auth [deepseek|openai|qwen|grsai] Alias for /login.",
|
|
479
479
|
" /instructions Show AGINTI.md project instructions status.",
|
|
480
480
|
" /memory Alias for /instructions.",
|
|
481
481
|
" /auxilliary [status|grsai|on|off|image]",
|
|
@@ -488,7 +488,7 @@ function printHelp() {
|
|
|
488
488
|
" /web-search on|off Enable or disable the web_search tool.",
|
|
489
489
|
" /scouts on|off|<1-10> Enable parallel DeepSeek scouts and set scout count.",
|
|
490
490
|
" /routing <mode> Set routing: smart, fast, complex, manual.",
|
|
491
|
-
" /provider <name> Set provider: deepseek, openai, mock.",
|
|
491
|
+
" /provider <name> Set provider: deepseek, openai, qwen, mock.",
|
|
492
492
|
" /model <name> Set an explicit model, or /model auto.",
|
|
493
493
|
" /docker on Use docker-workspace with approved package installs.",
|
|
494
494
|
" /docker off Use host shell policy.",
|
|
@@ -501,7 +501,7 @@ function printHelp() {
|
|
|
501
501
|
"Type / then Tab to autocomplete commands.",
|
|
502
502
|
"While a run is active, Enter pipes a message into the current run (→), Tab queues it after finish (↳).",
|
|
503
503
|
"Alt+Up edits the last piped message; Shift+Left edits the last queued message.",
|
|
504
|
-
"Esc or
|
|
504
|
+
"Esc is ignored while idle. During a run, Esc waits for pending → pipe messages or stops if none; Ctrl+C always stops.",
|
|
505
505
|
].join("\n")
|
|
506
506
|
);
|
|
507
507
|
}
|
|
@@ -793,6 +793,11 @@ function createAbortError(message = "Aborted with Ctrl+C") {
|
|
|
793
793
|
return error;
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
export function classifyEscapeAction({ active = false, pendingAsap = [] } = {}) {
|
|
797
|
+
if (!active) return "noop";
|
|
798
|
+
return Array.isArray(pendingAsap) && pendingAsap.length > 0 ? "wait-for-asap" : "abort";
|
|
799
|
+
}
|
|
800
|
+
|
|
796
801
|
function readTtyPrompt(options = {}) {
|
|
797
802
|
return new Promise((resolve, reject) => {
|
|
798
803
|
emitKeypressEvents(input);
|
|
@@ -966,10 +971,7 @@ function readTtyPrompt(options = {}) {
|
|
|
966
971
|
return;
|
|
967
972
|
}
|
|
968
973
|
if (key.name === "escape") {
|
|
969
|
-
|
|
970
|
-
cursor = 0;
|
|
971
|
-
preferredColumn = null;
|
|
972
|
-
redraw();
|
|
974
|
+
if (classifyEscapeAction({ active: false }) === "noop") return;
|
|
973
975
|
return;
|
|
974
976
|
}
|
|
975
977
|
if (key.ctrl || key.meta) return;
|
|
@@ -1192,6 +1194,13 @@ class LiveRunInput {
|
|
|
1192
1194
|
return;
|
|
1193
1195
|
}
|
|
1194
1196
|
if (key.name === "escape") {
|
|
1197
|
+
const action = classifyEscapeAction({ active: true, pendingAsap: this.pendingAsap });
|
|
1198
|
+
if (action === "wait-for-asap") {
|
|
1199
|
+
const count = this.pendingAsap.length;
|
|
1200
|
+
this.setStatus(`running · waiting to apply ${count} asap pipe message${count === 1 ? "" : "s"}`);
|
|
1201
|
+
this.redraw();
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1195
1204
|
this.controller.abort(new Error("Interrupted by escape."));
|
|
1196
1205
|
return;
|
|
1197
1206
|
}
|
|
@@ -1431,48 +1440,46 @@ async function maybeOnboardDeepSeekKey(state) {
|
|
|
1431
1440
|
|
|
1432
1441
|
printAgentMessage(
|
|
1433
1442
|
[
|
|
1434
|
-
"
|
|
1435
|
-
"
|
|
1443
|
+
"No main model API key is configured for this project.",
|
|
1444
|
+
"Choose DeepSeek, OpenAI, or Qwen, then paste a key to save in `.aginti/.env` with 0600 permissions.",
|
|
1445
|
+
"After that, you can optionally paste the auxiliary image key. Press Esc to skip.",
|
|
1436
1446
|
].join("\n")
|
|
1437
1447
|
);
|
|
1438
|
-
const result = await
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
if (result.saved) {
|
|
1442
|
-
printAgentMessage(`Saved ${result.keyName} to project-local ignored env.`);
|
|
1448
|
+
const result = await runAuthWizard(process.cwd(), { provider: state.provider || "", includeAuxiliary: true });
|
|
1449
|
+
applyAuthWizardResult(result, state);
|
|
1450
|
+
if (result.saved.some((item) => item.provider !== "grsai")) {
|
|
1443
1451
|
return;
|
|
1444
1452
|
}
|
|
1445
1453
|
|
|
1446
1454
|
state.provider = "mock";
|
|
1447
1455
|
state.routingMode = "manual";
|
|
1448
1456
|
state.model = "mock-agent";
|
|
1449
|
-
printAgentMessage("No key saved. Continuing in local mock mode. Use `/
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
async function promptAndSaveProviderKey(provider = "deepseek", state = null) {
|
|
1453
|
-
const aliases = { auxiliary: "grsai", auxilliary: "grsai", image: "grsai", imagegen: "grsai" };
|
|
1454
|
-
const candidate = aliases[String(provider || "").toLowerCase()] || String(provider || "").toLowerCase();
|
|
1455
|
-
const normalized = ["openai", "deepseek", "grsai"].includes(candidate)
|
|
1456
|
-
? String(provider || "").toLowerCase()
|
|
1457
|
-
: "deepseek";
|
|
1458
|
-
const canonical = aliases[normalized] || normalized;
|
|
1459
|
-
const labelText = canonical === "openai" ? "OpenAI" : canonical === "grsai" ? "GRSAI" : "DeepSeek";
|
|
1460
|
-
const key = await promptHidden(`${labelText} API key/token (paste, Enter to save): `);
|
|
1461
|
-
if (!key) {
|
|
1462
|
-
printAgentMessage("No key saved.");
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1457
|
+
printAgentMessage("No main key saved. Continuing in local mock mode. Use `/auth` later to save DeepSeek, OpenAI, or Qwen.");
|
|
1458
|
+
}
|
|
1465
1459
|
|
|
1466
|
-
|
|
1460
|
+
function applyAuthWizardResult(result, state = null) {
|
|
1467
1461
|
if (state) {
|
|
1468
|
-
|
|
1462
|
+
const main = result.saved.find((item) => item.provider !== "grsai");
|
|
1463
|
+
if (main) state.provider = main.provider;
|
|
1469
1464
|
if (state.routingMode === "manual" && state.model === "mock-agent") {
|
|
1470
1465
|
state.routingMode = "smart";
|
|
1471
1466
|
state.model = "";
|
|
1472
1467
|
}
|
|
1473
|
-
if (
|
|
1468
|
+
if (result.saved.some((item) => item.provider === "grsai")) state.allowAuxiliaryTools = true;
|
|
1474
1469
|
}
|
|
1475
|
-
|
|
1470
|
+
if (result.saved.length > 0) {
|
|
1471
|
+
printAgentMessage(
|
|
1472
|
+
result.saved.map((item) => `Saved ${item.keyName} to project-local ignored env. Raw key was not printed.`).join("\n")
|
|
1473
|
+
);
|
|
1474
|
+
} else {
|
|
1475
|
+
printAgentMessage("No key saved.");
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
async function promptAndSaveProviderKey(provider = "", state = null) {
|
|
1480
|
+
const canonical = normalizeAuthProvider(provider || "", "");
|
|
1481
|
+
const result = await runAuthWizard(process.cwd(), { provider: canonical, includeAuxiliary: canonical !== "grsai" });
|
|
1482
|
+
applyAuthWizardResult(result, state);
|
|
1476
1483
|
}
|
|
1477
1484
|
|
|
1478
1485
|
async function handleCommand(line, state, packageDir) {
|
|
@@ -1490,7 +1497,7 @@ async function handleCommand(line, state, packageDir) {
|
|
|
1490
1497
|
printSystemLine(
|
|
1491
1498
|
`keys deepseek=${keys.deepseek ? "available" : "missing"} openai=${keys.openai ? "available" : "missing"} grsai=${
|
|
1492
1499
|
keys.grsai ? "available" : "missing"
|
|
1493
|
-
}`
|
|
1500
|
+
} qwen=${keys.qwen ? "available" : "missing"}`
|
|
1494
1501
|
);
|
|
1495
1502
|
return true;
|
|
1496
1503
|
}
|
package/src/model-routing.js
CHANGED
|
@@ -88,6 +88,15 @@ export function getProviderDefaults(provider = "deepseek") {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
if (provider === "qwen") {
|
|
92
|
+
return {
|
|
93
|
+
provider: "qwen",
|
|
94
|
+
apiKey: process.env.QWEN_API_KEY || "",
|
|
95
|
+
baseURL: process.env.QWEN_BASE_URL || process.env.LLM_BASE_URL || "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
96
|
+
model: process.env.QWEN_DEFAULT_MODEL || process.env.LLM_MODEL || "qwen-plus",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
return {
|
|
92
101
|
provider: "deepseek",
|
|
93
102
|
apiKey: process.env.LLM_API_KEY || process.env.DEEPSEEK_API_KEY || "",
|
package/src/project.js
CHANGED
|
@@ -252,6 +252,7 @@ export function providerKeyStatus(projectRoot = process.cwd()) {
|
|
|
252
252
|
return {
|
|
253
253
|
openai: Boolean(process.env.OPENAI_API_KEY || process.env.LLM_API_KEY),
|
|
254
254
|
deepseek: Boolean(process.env.DEEPSEEK_API_KEY || process.env.LLM_API_KEY),
|
|
255
|
+
qwen: Boolean(process.env.QWEN_API_KEY),
|
|
255
256
|
grsai: Boolean(process.env.GRSAI || process.env.GRSAI_API_KEY),
|
|
256
257
|
mock: true,
|
|
257
258
|
localEnv: env.loaded,
|
|
@@ -259,6 +260,7 @@ export function providerKeyStatus(projectRoot = process.cwd()) {
|
|
|
259
260
|
envVars: {
|
|
260
261
|
openai: ["OPENAI_API_KEY", "LLM_API_KEY"],
|
|
261
262
|
deepseek: ["DEEPSEEK_API_KEY", "LLM_API_KEY"],
|
|
263
|
+
qwen: ["QWEN_API_KEY"],
|
|
262
264
|
grsai: ["GRSAI", "GRSAI_API_KEY"],
|
|
263
265
|
},
|
|
264
266
|
};
|
|
@@ -274,9 +276,15 @@ export async function setProviderKey(projectRoot, provider, value) {
|
|
|
274
276
|
};
|
|
275
277
|
const canonicalProvider = aliases[normalizedProvider] || normalizedProvider;
|
|
276
278
|
const keyName =
|
|
277
|
-
canonicalProvider === "openai"
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
canonicalProvider === "openai"
|
|
280
|
+
? "OPENAI_API_KEY"
|
|
281
|
+
: canonicalProvider === "qwen"
|
|
282
|
+
? "QWEN_API_KEY"
|
|
283
|
+
: canonicalProvider === "grsai"
|
|
284
|
+
? "GRSAI"
|
|
285
|
+
: "DEEPSEEK_API_KEY";
|
|
286
|
+
if (!["deepseek", "openai", "qwen", "grsai"].includes(canonicalProvider)) {
|
|
287
|
+
throw new Error("Provider must be deepseek, openai, qwen, or grsai.");
|
|
280
288
|
}
|
|
281
289
|
|
|
282
290
|
const keyValue = String(value || "").trim();
|
|
@@ -412,6 +420,7 @@ export async function doctorReport(projectRoot, packageVersion, config) {
|
|
|
412
420
|
keys: {
|
|
413
421
|
openai: keyStatus.openai,
|
|
414
422
|
deepseek: keyStatus.deepseek,
|
|
423
|
+
qwen: keyStatus.qwen,
|
|
415
424
|
grsai: keyStatus.grsai,
|
|
416
425
|
mock: true,
|
|
417
426
|
},
|
package/web.js
CHANGED
|
@@ -113,7 +113,7 @@ function serializeRun(run) {
|
|
|
113
113
|
function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
|
|
114
114
|
const modelPresets = getModelPresets();
|
|
115
115
|
const providerCandidate = body.provider || current.provider || "deepseek";
|
|
116
|
-
const provider = ["openai", "deepseek", "mock"].includes(providerCandidate) ? providerCandidate : "deepseek";
|
|
116
|
+
const provider = ["openai", "deepseek", "qwen", "mock"].includes(providerCandidate) ? providerCandidate : "deepseek";
|
|
117
117
|
const routingMode =
|
|
118
118
|
provider === "mock" ? "manual" : normalizeRoutingMode(body.routingMode || current.routingMode || "smart");
|
|
119
119
|
const providerDefaults = getProviderDefaults(provider);
|
|
@@ -195,6 +195,7 @@ function publicKeyStatus(projectRoot = baseDir) {
|
|
|
195
195
|
return {
|
|
196
196
|
openai: status.openai,
|
|
197
197
|
deepseek: status.deepseek,
|
|
198
|
+
qwen: status.qwen,
|
|
198
199
|
grsai: status.grsai,
|
|
199
200
|
mock: true,
|
|
200
201
|
localEnv: status.localEnv,
|
|
@@ -570,6 +571,7 @@ app.get("/api/config", async (_req, res) => {
|
|
|
570
571
|
defaults: {
|
|
571
572
|
openai: publicProviderDefault("openai"),
|
|
572
573
|
deepseek: publicProviderDefault("deepseek"),
|
|
574
|
+
qwen: publicProviderDefault("qwen"),
|
|
573
575
|
mock: publicProviderDefault("mock"),
|
|
574
576
|
headless: true,
|
|
575
577
|
maxSteps: 24,
|