@lazyingart/agintiflow 0.20.121 → 0.20.123
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 +5 -3
- package/package.json +6 -2
- package/references/agintiflow-aaps-slogan-subtext-v3.md +1 -1
- package/scripts/smoke-cli-chat.js +34 -0
- package/scripts/smoke-permission-modes.js +10 -0
- package/scripts/smoke-web-autostart.js +47 -0
- package/scripts/smoke-web-port-fallback.js +82 -0
- package/src/cli.js +36 -12
- package/src/interactive-cli.js +7 -7
- package/src/web-autostart.js +121 -0
- package/web.js +32 -5
package/README.md
CHANGED
|
@@ -103,11 +103,13 @@ Provider signup and key pages:
|
|
|
103
103
|
| Qwen / DashScope | [https://bailian.console.aliyun.com/](https://bailian.console.aliyun.com/) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` |
|
|
104
104
|
| GRS AI image tools | [https://grsai.ai/dashboard/api-keys](https://grsai.ai/dashboard/api-keys) | Configure with `/auxiliary grsai` or `aginti login grsai` |
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
The CLI auto-starts or reuses the local web UI from the same project. It tries `http://127.0.0.1:3210` first, then `3211`, `3212`, and so on if the port is already occupied. The active URL is shown in the CLI launch header.
|
|
107
|
+
|
|
108
|
+
Launch the web UI explicitly when you want a foreground web server:
|
|
107
109
|
|
|
108
110
|
```bash
|
|
109
111
|
aginti web --port 3210
|
|
110
|
-
#
|
|
112
|
+
# opens http://127.0.0.1:3210, or the next available port
|
|
111
113
|
```
|
|
112
114
|
|
|
113
115
|
Run without live model credentials for smoke tests:
|
|
@@ -129,7 +131,7 @@ aginti --language de
|
|
|
129
131
|
| Goal | Command |
|
|
130
132
|
| --- | --- |
|
|
131
133
|
| Start interactive chat | `aginti` or `aginti chat` |
|
|
132
|
-
| Start local web app | `aginti web --port 3210` |
|
|
134
|
+
| Start local web app | Auto-starts with `aginti`; foreground mode is `aginti web --port 3210` |
|
|
133
135
|
| Save provider keys | `aginti auth`, `/auth`, `/login` |
|
|
134
136
|
| Review current repo | `/review [focus]` |
|
|
135
137
|
| Toggle SCS quality gate | `/scs` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.123",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -68,7 +68,9 @@
|
|
|
68
68
|
"scripts/smoke-skillmesh.js",
|
|
69
69
|
"scripts/smoke-tmux-tools.js",
|
|
70
70
|
"scripts/smoke-toolchain-docker.js",
|
|
71
|
+
"scripts/smoke-web-autostart.js",
|
|
71
72
|
"scripts/smoke-web-api.js",
|
|
73
|
+
"scripts/smoke-web-port-fallback.js",
|
|
72
74
|
"src/",
|
|
73
75
|
"run.js",
|
|
74
76
|
"skills/",
|
|
@@ -100,11 +102,13 @@
|
|
|
100
102
|
"smoke:permission-modes": "node scripts/smoke-permission-modes.js",
|
|
101
103
|
"smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
|
|
102
104
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
105
|
+
"smoke:web-autostart": "node scripts/smoke-web-autostart.js",
|
|
106
|
+
"smoke:web-port-fallback": "node scripts/smoke-web-port-fallback.js",
|
|
103
107
|
"smoke:autoupdate": "node scripts/smoke-auto-update.js",
|
|
104
108
|
"real:deepseek": "node scripts/real-deepseek-capabilities.js",
|
|
105
109
|
"supervision:seed": "node scripts/seed-supervised-homework.js",
|
|
106
110
|
"storage:migrate": "node bin/aginti-cli.js storage migrate",
|
|
107
|
-
"test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
111
|
+
"test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:web-autostart && npm run smoke:web-port-fallback && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
|
|
108
112
|
"pack:dry-run": "npm pack --dry-run",
|
|
109
113
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
110
114
|
},
|
|
@@ -14,7 +14,7 @@ AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-a
|
|
|
14
14
|
|
|
15
15
|
Project-oriented programming for agentic workflows.
|
|
16
16
|
|
|
17
|
-
AAPS is a prompt-native programming language and visual studio for turning prompts into structured, verifiable pipelines. It connects wet and dry experiments, hardware and software, and human intent with executable agent work through tasks, typed inputs, declared outputs, validation gates, recovery steps, and durable artifacts.
|
|
17
|
+
AAPS is a project oriented and prompt-native programming language and visual studio for turning prompts into structured, verifiable pipelines. It connects wet and dry experiments, hardware and software, and human intent with executable agent work through tasks, typed inputs, declared outputs, validation gates, recovery steps, and durable artifacts.
|
|
18
18
|
|
|
19
19
|
## Shorter Version
|
|
20
20
|
|
|
@@ -317,6 +317,15 @@ try {
|
|
|
317
317
|
if (launchHeader.includes("launchTagline")) {
|
|
318
318
|
throw new Error("empty launch tagline should not render the i18n key name");
|
|
319
319
|
}
|
|
320
|
+
const launchHeaderWithWeb = buildLaunchHeaderLines({
|
|
321
|
+
width: 120,
|
|
322
|
+
packageVersion: "0.0.0",
|
|
323
|
+
animated: false,
|
|
324
|
+
webAppUrl: "http://127.0.0.1:3210",
|
|
325
|
+
}).join("\n");
|
|
326
|
+
if (!launchHeaderWithWeb.includes("webapp: http://127.0.0.1:3210")) {
|
|
327
|
+
throw new Error("launch header did not render the active webapp URL in the tagline row");
|
|
328
|
+
}
|
|
320
329
|
if (
|
|
321
330
|
formatElapsedDuration(0) !== "00:00" ||
|
|
322
331
|
formatElapsedDuration(65_000) !== "01:05" ||
|
|
@@ -746,6 +755,30 @@ try {
|
|
|
746
755
|
throw new Error("resume history should render full saved messages instead of compact previews");
|
|
747
756
|
}
|
|
748
757
|
|
|
758
|
+
const explicitCwd = path.join(tempRoot, "explicit-cwd");
|
|
759
|
+
await fs.mkdir(explicitCwd, { recursive: true });
|
|
760
|
+
await runCli([
|
|
761
|
+
"--no-auto-update",
|
|
762
|
+
"--provider",
|
|
763
|
+
"mock",
|
|
764
|
+
"--routing",
|
|
765
|
+
"manual",
|
|
766
|
+
"--cwd",
|
|
767
|
+
explicitCwd,
|
|
768
|
+
"--sandbox-mode",
|
|
769
|
+
"host",
|
|
770
|
+
"--allow-file-tools",
|
|
771
|
+
"Create reports/cwd-smoke.md with a one-line cwd smoke note.",
|
|
772
|
+
], "");
|
|
773
|
+
const explicitCwdOutput = path.join(explicitCwd, "reports", "cwd-smoke.md");
|
|
774
|
+
const wrongCwdOutput = path.join(tempRoot, "reports", "cwd-smoke.md");
|
|
775
|
+
if (!(await fs.stat(explicitCwdOutput).then((stat) => stat.isFile()).catch(() => false))) {
|
|
776
|
+
throw new Error("one-shot --cwd did not write into the explicit command cwd");
|
|
777
|
+
}
|
|
778
|
+
if (await fs.stat(wrongCwdOutput).then((stat) => stat.isFile()).catch(() => false)) {
|
|
779
|
+
throw new Error("one-shot --cwd wrote into the process cwd instead of the explicit command cwd");
|
|
780
|
+
}
|
|
781
|
+
|
|
749
782
|
await runTmuxInterruptSmoke({ key: "Escape", expected: /Active run stopped|Session saved/ });
|
|
750
783
|
await runTmuxInterruptSmoke({ key: "C-c", expected: /EXIT:130|Session stopped by ctrl-c|Interrupted\. Session saved/ });
|
|
751
784
|
|
|
@@ -787,6 +820,7 @@ try {
|
|
|
787
820
|
"interactive-chat",
|
|
788
821
|
"mock-file-write",
|
|
789
822
|
"run-status",
|
|
823
|
+
"one-shot-cwd",
|
|
790
824
|
"tmux-escape-active-run-stop",
|
|
791
825
|
"tmux-ctrl-c-session-stop",
|
|
792
826
|
"resume-latest",
|
|
@@ -65,6 +65,16 @@ async function main() {
|
|
|
65
65
|
assert(parsedDanger.sandboxMode === "host", "danger should set host sandbox");
|
|
66
66
|
assert(parsedDanger.allowDestructive === true, "danger should allow destructive shell");
|
|
67
67
|
assert(parsedDanger.allowOutsideWorkspaceFileTools === true, "danger should allow outside workspace file tools");
|
|
68
|
+
const explicitPackageBeforeSafety = parseArgs(["--package-install-policy", "block", "-s", "normal", "--provider", "mock", "hello"]);
|
|
69
|
+
assert(
|
|
70
|
+
explicitPackageBeforeSafety.packageInstallPolicy === "block",
|
|
71
|
+
"CLI -s normal should not override an explicit earlier --package-install-policy block"
|
|
72
|
+
);
|
|
73
|
+
const explicitSandboxBeforeSafety = parseArgs(["--sandbox-mode", "docker-readonly", "-s", "danger", "--provider", "mock", "hello"]);
|
|
74
|
+
assert(
|
|
75
|
+
explicitSandboxBeforeSafety.sandboxMode === "docker-readonly",
|
|
76
|
+
"CLI -s danger should not override an explicit earlier --sandbox-mode docker-readonly"
|
|
77
|
+
);
|
|
68
78
|
|
|
69
79
|
const safe = configFor("safe");
|
|
70
80
|
assert(safe.permissionMode === "safe", "runtime safe mode not set");
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ensureAgintiWebApp } from "../src/web-autostart.js";
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-web-autostart-"));
|
|
10
|
+
const preferredPort = 44500 + Math.floor(Math.random() * 1000);
|
|
11
|
+
let childPid = 0;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const first = await ensureAgintiWebApp({
|
|
15
|
+
packageDir: repoRoot,
|
|
16
|
+
cwd: runtimeDir,
|
|
17
|
+
preferredPort,
|
|
18
|
+
host: "127.0.0.1",
|
|
19
|
+
});
|
|
20
|
+
if (!first.ok || !first.started || !first.url.includes(`:${preferredPort}`)) {
|
|
21
|
+
throw new Error(`expected auto-start on preferred port, got ${JSON.stringify(first)}`);
|
|
22
|
+
}
|
|
23
|
+
childPid = Number(first.pid) || 0;
|
|
24
|
+
const health = await fetch(`${first.url}/health`).then((response) => response.json());
|
|
25
|
+
if (!health.ok || Number(health.port) !== preferredPort) {
|
|
26
|
+
throw new Error(`auto-started web health was invalid: ${JSON.stringify(health)}`);
|
|
27
|
+
}
|
|
28
|
+
const second = await ensureAgintiWebApp({
|
|
29
|
+
packageDir: repoRoot,
|
|
30
|
+
cwd: runtimeDir,
|
|
31
|
+
preferredPort,
|
|
32
|
+
host: "127.0.0.1",
|
|
33
|
+
});
|
|
34
|
+
if (!second.ok || !second.reused || second.url !== first.url) {
|
|
35
|
+
throw new Error(`expected second auto-start call to reuse existing webapp, got ${JSON.stringify(second)}`);
|
|
36
|
+
}
|
|
37
|
+
console.log(`web auto-start smoke passed: ${first.url}`);
|
|
38
|
+
} finally {
|
|
39
|
+
if (childPid) {
|
|
40
|
+
try {
|
|
41
|
+
process.kill(childPid, "SIGTERM");
|
|
42
|
+
} catch {
|
|
43
|
+
// The child may have already exited during a failed smoke test.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await fs.rm(runtimeDir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-web-port-fallback-"));
|
|
11
|
+
const host = "127.0.0.1";
|
|
12
|
+
const occupiedPort = 43100 + Math.floor(Math.random() * 1200);
|
|
13
|
+
|
|
14
|
+
function listenOccupier(port) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const server = net.createServer();
|
|
17
|
+
server.once("error", reject);
|
|
18
|
+
server.listen(port, host, () => resolve(server));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function delay(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function waitForPrintedPort(child, stdout, stderr) {
|
|
27
|
+
const deadline = Date.now() + 12000;
|
|
28
|
+
while (Date.now() < deadline) {
|
|
29
|
+
if (child.exitCode !== null) break;
|
|
30
|
+
const match = stdout().match(/http:\/\/127\.0\.0\.1:(\d+)/);
|
|
31
|
+
if (match) return Number(match[1]);
|
|
32
|
+
await delay(100);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`web fallback URL was not printed. stdout=${stdout()} stderr=${stderr()}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function waitForHealth(port, child, stdout, stderr) {
|
|
38
|
+
const deadline = Date.now() + 12000;
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
if (child.exitCode !== null) break;
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`http://${host}:${port}/health`);
|
|
43
|
+
const body = await response.json();
|
|
44
|
+
if (response.ok && body.ok && Number(body.port) === port) return body;
|
|
45
|
+
} catch {
|
|
46
|
+
await delay(200);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`web fallback health failed. stdout=${stdout()} stderr=${stderr()}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const occupier = await listenOccupier(occupiedPort);
|
|
53
|
+
let stdout = "";
|
|
54
|
+
let stderr = "";
|
|
55
|
+
const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "web", "--port", String(occupiedPort), "--host", host], {
|
|
56
|
+
cwd: runtimeDir,
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
AGINTIFLOW_RUNTIME_DIR: runtimeDir,
|
|
60
|
+
AGINTIFLOW_HOME: path.join(runtimeDir, ".agintiflow-home"),
|
|
61
|
+
},
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
child.stdout.on("data", (chunk) => {
|
|
65
|
+
stdout += chunk.toString();
|
|
66
|
+
});
|
|
67
|
+
child.stderr.on("data", (chunk) => {
|
|
68
|
+
stderr += chunk.toString();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const actualPort = await waitForPrintedPort(child, () => stdout.slice(-500), () => stderr.slice(-500));
|
|
73
|
+
if (actualPort <= occupiedPort) {
|
|
74
|
+
throw new Error(`web command did not move past occupied port ${occupiedPort}; got ${actualPort}`);
|
|
75
|
+
}
|
|
76
|
+
await waitForHealth(actualPort, child, () => stdout.slice(-500), () => stderr.slice(-500));
|
|
77
|
+
console.log(`web port fallback smoke passed: ${occupiedPort} -> ${actualPort}`);
|
|
78
|
+
} finally {
|
|
79
|
+
child.kill("SIGTERM");
|
|
80
|
+
occupier.close();
|
|
81
|
+
await fs.rm(runtimeDir, { recursive: true, force: true });
|
|
82
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ import { handleSkillMeshCommand } from "./skillmesh.js";
|
|
|
37
37
|
import { handleAapsCliCommand } from "./aaps-adapter.js";
|
|
38
38
|
import { formatInstructionTemplateList, normalizeInstructionTemplate } from "./behavior-contract.js";
|
|
39
39
|
import { applyPermissionMode, normalizePermissionMode } from "./permission-modes.js";
|
|
40
|
+
import { ensureAgintiWebApp } from "./web-autostart.js";
|
|
40
41
|
import fs from "node:fs/promises";
|
|
41
42
|
import path from "node:path";
|
|
42
43
|
import { fileURLToPath } from "node:url";
|
|
@@ -91,6 +92,21 @@ function printUnknownCliOptions(options = []) {
|
|
|
91
92
|
console.error("Use `--` before a prompt that intentionally starts with a dash.");
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
async function maybeEnsureDefaultWebApp(args = {}, { commandCwd = process.cwd() } = {}) {
|
|
96
|
+
if (args.web) return { ok: false, url: "" };
|
|
97
|
+
try {
|
|
98
|
+
return await ensureAgintiWebApp({
|
|
99
|
+
packageDir,
|
|
100
|
+
cwd: args.commandCwd || commandCwd || process.cwd(),
|
|
101
|
+
host: args.host || process.env.AGINTI_WEB_HOST || "127.0.0.1",
|
|
102
|
+
preferredPort: args.port || process.env.AGINTI_WEB_PORT || 3210,
|
|
103
|
+
language: args.language ? resolveLanguage(args.language) : "",
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { ok: false, url: "", error: error instanceof Error ? error.message : String(error) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
const CLI_VALUE_OPTIONS = new Set([
|
|
95
111
|
"--port",
|
|
96
112
|
"--host",
|
|
@@ -513,7 +529,7 @@ export function parseArgs(argv) {
|
|
|
513
529
|
}
|
|
514
530
|
if (arg === "-s" || arg === "--safety" || arg === "--permission-mode") {
|
|
515
531
|
const mode = normalizePermissionMode(readOption(argv, i), "");
|
|
516
|
-
if (mode) applyPermissionMode(result, mode, { override:
|
|
532
|
+
if (mode) applyPermissionMode(result, mode, { override: false });
|
|
517
533
|
i += 1;
|
|
518
534
|
continue;
|
|
519
535
|
}
|
|
@@ -1606,19 +1622,20 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1606
1622
|
if (commandArgv[0] === "doctor") {
|
|
1607
1623
|
const parsed = parseArgs(commandArgv.slice(1).filter((arg) => arg !== "--json" && arg !== "--capabilities"));
|
|
1608
1624
|
exitOnUnknownOptions(parsed);
|
|
1625
|
+
const effectiveCommandCwd = parsed.commandCwd || commandCwd;
|
|
1609
1626
|
const config = loadConfig(
|
|
1610
1627
|
{
|
|
1611
1628
|
...parsed,
|
|
1612
1629
|
goal: "doctor",
|
|
1613
|
-
commandCwd,
|
|
1630
|
+
commandCwd: effectiveCommandCwd,
|
|
1614
1631
|
allowShellTool: parsed.allowShellTool ?? true,
|
|
1615
1632
|
allowFileTools: parsed.allowFileTools ?? true,
|
|
1616
1633
|
},
|
|
1617
|
-
{ packageDir, baseDir:
|
|
1634
|
+
{ packageDir, baseDir: effectiveCommandCwd }
|
|
1618
1635
|
);
|
|
1619
1636
|
const report = commandArgv.includes("--capabilities")
|
|
1620
|
-
? await buildCapabilityReport(
|
|
1621
|
-
: await doctorReport(
|
|
1637
|
+
? await buildCapabilityReport(effectiveCommandCwd, packageJson.version, config)
|
|
1638
|
+
: await doctorReport(effectiveCommandCwd, packageJson.version, config);
|
|
1622
1639
|
if (commandArgv.includes("--json")) console.log(JSON.stringify(report, null, 2));
|
|
1623
1640
|
else if (commandArgv.includes("--capabilities")) printCapabilityReport(report);
|
|
1624
1641
|
else printDoctorReport(report);
|
|
@@ -1628,17 +1645,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1628
1645
|
if (commandArgv[0] === "capabilities") {
|
|
1629
1646
|
const parsed = parseArgs(commandArgv.slice(1).filter((arg) => arg !== "--json"));
|
|
1630
1647
|
exitOnUnknownOptions(parsed);
|
|
1648
|
+
const effectiveCommandCwd = parsed.commandCwd || commandCwd;
|
|
1631
1649
|
const config = loadConfig(
|
|
1632
1650
|
{
|
|
1633
1651
|
...parsed,
|
|
1634
1652
|
goal: "capabilities",
|
|
1635
|
-
commandCwd,
|
|
1653
|
+
commandCwd: effectiveCommandCwd,
|
|
1636
1654
|
allowShellTool: parsed.allowShellTool ?? true,
|
|
1637
1655
|
allowFileTools: parsed.allowFileTools ?? true,
|
|
1638
1656
|
},
|
|
1639
|
-
{ packageDir, baseDir:
|
|
1657
|
+
{ packageDir, baseDir: effectiveCommandCwd }
|
|
1640
1658
|
);
|
|
1641
|
-
const report = await buildCapabilityReport(
|
|
1659
|
+
const report = await buildCapabilityReport(effectiveCommandCwd, packageJson.version, config);
|
|
1642
1660
|
if (commandArgv.includes("--json")) console.log(JSON.stringify(report, null, 2));
|
|
1643
1661
|
else printCapabilityReport(report);
|
|
1644
1662
|
return;
|
|
@@ -1732,22 +1750,26 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1732
1750
|
if (!prompt) {
|
|
1733
1751
|
const parsedResumeOptions = parseArgs(resumeOptions.optionArgv);
|
|
1734
1752
|
exitOnUnknownOptions(parsedResumeOptions);
|
|
1735
|
-
await
|
|
1753
|
+
const webLaunch = await maybeEnsureDefaultWebApp(parsedResumeOptions, { commandCwd });
|
|
1754
|
+
await startInteractiveCli(agentDefaults({ ...parsedResumeOptions, resume: sessionId, commandCwd: parsedResumeOptions.commandCwd || commandCwd }), {
|
|
1736
1755
|
packageDir,
|
|
1737
1756
|
packageVersion: packageJson.version,
|
|
1757
|
+
webAppUrl: webLaunch.ok ? webLaunch.url : "",
|
|
1738
1758
|
});
|
|
1739
1759
|
return;
|
|
1740
1760
|
}
|
|
1741
1761
|
const parsedResumeArgs = parseArgs([...resumeOptions.optionArgv, prompt]);
|
|
1742
1762
|
exitOnUnknownOptions(parsedResumeArgs);
|
|
1743
|
-
const resumeArgs = agentDefaults({ ...parsedResumeArgs, resume: sessionId, goal: prompt, commandCwd });
|
|
1763
|
+
const resumeArgs = agentDefaults({ ...parsedResumeArgs, resume: sessionId, goal: prompt, commandCwd: parsedResumeArgs.commandCwd || commandCwd });
|
|
1744
1764
|
if (!(await ensureDeepSeekKeyForOneShot(resumeArgs))) process.exit(1);
|
|
1765
|
+
await maybeEnsureDefaultWebApp(resumeArgs, { commandCwd });
|
|
1745
1766
|
const config = loadConfig(resumeArgs, { packageDir });
|
|
1746
1767
|
await runAgent(config);
|
|
1747
1768
|
return;
|
|
1748
1769
|
}
|
|
1749
1770
|
|
|
1750
|
-
const
|
|
1771
|
+
const parsedArgs = parseArgs(commandArgv);
|
|
1772
|
+
const args = { ...parsedArgs, commandCwd: parsedArgs.commandCwd || commandCwd };
|
|
1751
1773
|
exitOnUnknownOptions(args);
|
|
1752
1774
|
|
|
1753
1775
|
if (args.web) {
|
|
@@ -1760,7 +1782,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1760
1782
|
}
|
|
1761
1783
|
|
|
1762
1784
|
if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
|
|
1763
|
-
await
|
|
1785
|
+
const webLaunch = await maybeEnsureDefaultWebApp(args, { commandCwd });
|
|
1786
|
+
await startInteractiveCli(agentDefaults(args), { packageDir, packageVersion: packageJson.version, webAppUrl: webLaunch.ok ? webLaunch.url : "" });
|
|
1764
1787
|
return;
|
|
1765
1788
|
}
|
|
1766
1789
|
|
|
@@ -1805,6 +1828,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1805
1828
|
|
|
1806
1829
|
const finalArgs = agentDefaults(args);
|
|
1807
1830
|
if (!(await ensureDeepSeekKeyForOneShot(finalArgs))) process.exit(1);
|
|
1831
|
+
await maybeEnsureDefaultWebApp(finalArgs, { commandCwd });
|
|
1808
1832
|
const config = loadConfig(finalArgs, { packageDir });
|
|
1809
1833
|
await runAgent(config);
|
|
1810
1834
|
}
|
package/src/interactive-cli.js
CHANGED
|
@@ -772,10 +772,10 @@ function launchTitleLines(contentWidth) {
|
|
|
772
772
|
return largeWidth <= contentWidth ? LARGE_LAUNCH_TITLE : COMPACT_LAUNCH_TITLE;
|
|
773
773
|
}
|
|
774
774
|
|
|
775
|
-
export function buildLaunchHeaderLines({ packageVersion = "", frame = 2, width = terminalWidth(), animated = true, language = cliLanguage } = {}) {
|
|
775
|
+
export function buildLaunchHeaderLines({ packageVersion = "", frame = 2, width = terminalWidth(), animated = true, language = cliLanguage, webAppUrl = "" } = {}) {
|
|
776
776
|
const subtitle = t("launchSubtitle", language);
|
|
777
777
|
const credit = t("launchCredit", language);
|
|
778
|
-
const tagline = t("launchTagline", language);
|
|
778
|
+
const tagline = webAppUrl ? `webapp: ${webAppUrl}` : t("launchTagline", language);
|
|
779
779
|
const version = packageVersion ? `v${packageVersion}` : "";
|
|
780
780
|
const terminalColumns = Math.max(Number(width) || 80, 50);
|
|
781
781
|
const contentWidth = Math.min(Math.max(terminalColumns - 8, 58), 112);
|
|
@@ -801,9 +801,9 @@ export function buildLaunchHeaderLines({ packageVersion = "", frame = 2, width =
|
|
|
801
801
|
return boxLines.map((line) => `${indent}${line}`);
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
-
async function renderLaunchHeader(packageVersion = "", language = cliLanguage) {
|
|
804
|
+
async function renderLaunchHeader(packageVersion = "", language = cliLanguage, webAppUrl = "") {
|
|
805
805
|
if (!useColor || process.env.AGINTIFLOW_NO_ANIMATION === "1") {
|
|
806
|
-
console.log(buildLaunchHeaderLines({ packageVersion, animated: false, language }).join("\n"));
|
|
806
|
+
console.log(buildLaunchHeaderLines({ packageVersion, animated: false, language, webAppUrl }).join("\n"));
|
|
807
807
|
return;
|
|
808
808
|
}
|
|
809
809
|
|
|
@@ -812,7 +812,7 @@ async function renderLaunchHeader(packageVersion = "", language = cliLanguage) {
|
|
|
812
812
|
let previousLineCount = 0;
|
|
813
813
|
output.write(ansi.cursorHide);
|
|
814
814
|
for (let frame = 0; frame < 18; frame += 1) {
|
|
815
|
-
const lines = [...paddingLines, ...buildLaunchHeaderLines({ packageVersion, frame, animated: true, language })];
|
|
815
|
+
const lines = [...paddingLines, ...buildLaunchHeaderLines({ packageVersion, frame, animated: true, language, webAppUrl })];
|
|
816
816
|
if (previousLineCount > 0) output.write(`\x1b[${previousLineCount}A`);
|
|
817
817
|
output.write(lines.map((line) => `\r${ansi.clearLine}${line}`).join("\n"));
|
|
818
818
|
output.write("\n");
|
|
@@ -3808,7 +3808,7 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3808
3808
|
return queuedAfterFinish;
|
|
3809
3809
|
}
|
|
3810
3810
|
|
|
3811
|
-
export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
|
|
3811
|
+
export async function startInteractiveCli(args = {}, { packageDir, packageVersion, webAppUrl = "" } = {}) {
|
|
3812
3812
|
const state = createState(args);
|
|
3813
3813
|
const detachProcessInterrupts = installProcessInterruptHandlers();
|
|
3814
3814
|
setCliLanguage(state.language);
|
|
@@ -3822,7 +3822,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
|
|
|
3822
3822
|
completer: commandCompleter,
|
|
3823
3823
|
});
|
|
3824
3824
|
|
|
3825
|
-
await renderLaunchHeader(packageVersion, state.language);
|
|
3825
|
+
await renderLaunchHeader(packageVersion, state.language, webAppUrl);
|
|
3826
3826
|
printSystemLine(`${tr("project")}: ${process.cwd()}`);
|
|
3827
3827
|
await maybeOnboardDeepSeekKey(state);
|
|
3828
3828
|
printAgentMessage(tr("interactiveIntro"));
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
7
|
+
const DEFAULT_PORT = 3210;
|
|
8
|
+
const MAX_PORT_ATTEMPTS = 50;
|
|
9
|
+
|
|
10
|
+
function normalizeHost(host) {
|
|
11
|
+
return String(host || process.env.AGINTI_WEB_HOST || process.env.HOST || DEFAULT_HOST).trim() || DEFAULT_HOST;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizePort(port) {
|
|
15
|
+
const parsed = Number(port || process.env.AGINTI_WEB_PORT || process.env.PORT || DEFAULT_PORT);
|
|
16
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed < 65536 ? parsed : DEFAULT_PORT;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function webUrl(host, port) {
|
|
20
|
+
return `http://${host}:${port}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fetchHealth(host, port, timeoutMs = 450) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const req = http.get(`${webUrl(host, port)}/health`, { timeout: timeoutMs }, (res) => {
|
|
26
|
+
let body = "";
|
|
27
|
+
res.setEncoding("utf8");
|
|
28
|
+
res.on("data", (chunk) => {
|
|
29
|
+
body += chunk;
|
|
30
|
+
});
|
|
31
|
+
res.on("end", () => {
|
|
32
|
+
try {
|
|
33
|
+
const json = JSON.parse(body || "{}");
|
|
34
|
+
resolve(Boolean(res.statusCode === 200 && json.ok && (json.app === "agintiflow" || Number(json.port) === port)));
|
|
35
|
+
} catch {
|
|
36
|
+
resolve(false);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
req.on("timeout", () => {
|
|
41
|
+
req.destroy();
|
|
42
|
+
resolve(false);
|
|
43
|
+
});
|
|
44
|
+
req.on("error", () => resolve(false));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function canListen(host, port) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const server = net.createServer();
|
|
51
|
+
server.once("error", () => resolve(false));
|
|
52
|
+
server.listen(port, host, () => {
|
|
53
|
+
server.close(() => resolve(true));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function waitForHealth(host, port, timeoutMs = 7000) {
|
|
59
|
+
const deadline = Date.now() + timeoutMs;
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
if (await fetchHealth(host, port, 350)) return true;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function findReusableOrFreeWebPort({ host = DEFAULT_HOST, preferredPort = DEFAULT_PORT, attempts = MAX_PORT_ATTEMPTS } = {}) {
|
|
68
|
+
const startPort = normalizePort(preferredPort);
|
|
69
|
+
const normalizedHost = normalizeHost(host);
|
|
70
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
71
|
+
const port = startPort + offset;
|
|
72
|
+
if (port >= 65536) break;
|
|
73
|
+
if (await fetchHealth(normalizedHost, port)) {
|
|
74
|
+
return { port, host: normalizedHost, url: webUrl(normalizedHost, port), reused: true, available: false };
|
|
75
|
+
}
|
|
76
|
+
if (await canListen(normalizedHost, port)) {
|
|
77
|
+
return { port, host: normalizedHost, url: webUrl(normalizedHost, port), reused: false, available: true };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { port: 0, host: normalizedHost, url: "", reused: false, available: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function ensureAgintiWebApp({
|
|
84
|
+
packageDir = process.cwd(),
|
|
85
|
+
cwd = process.cwd(),
|
|
86
|
+
host = DEFAULT_HOST,
|
|
87
|
+
preferredPort = DEFAULT_PORT,
|
|
88
|
+
language = "",
|
|
89
|
+
} = {}) {
|
|
90
|
+
if (process.env.AGINTI_NO_WEB_AUTO_START === "1" || process.env.AGINTIFLOW_NO_WEB_AUTO_START === "1") {
|
|
91
|
+
return { ok: false, disabled: true, url: "" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const candidate = await findReusableOrFreeWebPort({ host, preferredPort });
|
|
95
|
+
if (!candidate.port) {
|
|
96
|
+
return { ok: false, error: `No available AgInTiFlow web port from ${normalizePort(preferredPort)}.`, url: "" };
|
|
97
|
+
}
|
|
98
|
+
if (candidate.reused) {
|
|
99
|
+
return { ok: true, reused: true, started: false, ...candidate };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const child = spawn(process.execPath, [path.join(packageDir, "web.js")], {
|
|
103
|
+
cwd,
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: "ignore",
|
|
106
|
+
env: {
|
|
107
|
+
...process.env,
|
|
108
|
+
HOST: candidate.host,
|
|
109
|
+
PORT: String(candidate.port),
|
|
110
|
+
AGINTIFLOW_RUNTIME_DIR: cwd,
|
|
111
|
+
AGINTIFLOW_PACKAGE_DIR: packageDir,
|
|
112
|
+
...(language ? { AGINTI_LANGUAGE: language } : {}),
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
child.unref();
|
|
116
|
+
|
|
117
|
+
const healthy = await waitForHealth(candidate.host, candidate.port);
|
|
118
|
+
return healthy
|
|
119
|
+
? { ok: true, reused: false, started: true, pid: child.pid, ...candidate }
|
|
120
|
+
: { ok: false, error: `Started web process ${child.pid}, but ${candidate.url}/health did not become ready.`, url: "" };
|
|
121
|
+
}
|
package/web.js
CHANGED
|
@@ -58,7 +58,7 @@ loadProjectEnv(baseDir);
|
|
|
58
58
|
await ensureProjectSessionStorage(baseDir);
|
|
59
59
|
|
|
60
60
|
const app = express();
|
|
61
|
-
|
|
61
|
+
let port = Number(process.env.PORT || 3210);
|
|
62
62
|
const host = process.env.HOST || "127.0.0.1";
|
|
63
63
|
const runs = new Map();
|
|
64
64
|
const db = new WebDatabase(baseDir);
|
|
@@ -1337,12 +1337,39 @@ app.post("/api/runs/:sessionId/stop", async (req, res) => {
|
|
|
1337
1337
|
});
|
|
1338
1338
|
|
|
1339
1339
|
app.get("/health", (_req, res) => {
|
|
1340
|
-
res.json({ ok: true, port });
|
|
1340
|
+
res.json({ ok: true, app: "agintiflow", port, url: `http://${host}:${port}` });
|
|
1341
1341
|
});
|
|
1342
1342
|
|
|
1343
1343
|
await fs.mkdir(sessionsDir, { recursive: true });
|
|
1344
1344
|
await syncStoredSessions();
|
|
1345
1345
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
});
|
|
1346
|
+
function listenOnPort(candidatePort) {
|
|
1347
|
+
return new Promise((resolve, reject) => {
|
|
1348
|
+
const server = app.listen({ port: candidatePort, host });
|
|
1349
|
+
server.once("error", (error) => reject(error));
|
|
1350
|
+
server.once("listening", () => resolve(server));
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function listenWithFallback(preferredPort, attempts = 50) {
|
|
1355
|
+
const startPort = Number.isInteger(Number(preferredPort)) && Number(preferredPort) > 0 ? Number(preferredPort) : 3210;
|
|
1356
|
+
let lastError = null;
|
|
1357
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
1358
|
+
const candidatePort = startPort + offset;
|
|
1359
|
+
if (candidatePort >= 65536) break;
|
|
1360
|
+
try {
|
|
1361
|
+
return await listenOnPort(candidatePort);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
lastError = error;
|
|
1364
|
+
if (error?.code !== "EADDRINUSE") throw error;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
const error = new Error(`No available AgInTiFlow web port from ${startPort} after ${attempts} attempts.`);
|
|
1368
|
+
error.cause = lastError;
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export const webServer = await listenWithFallback(port);
|
|
1373
|
+
port = webServer.address()?.port || port;
|
|
1374
|
+
export const webUrl = `http://${host}:${port}`;
|
|
1375
|
+
console.log(`Website control agent UI running on ${webUrl}`);
|