@lazyingart/agintiflow 0.20.123 → 0.20.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,7 +103,9 @@ 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
- 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.
106
+ The CLI quietly 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. If startup is blocked or unavailable, the same header row shows the recovery hint; run `/webapp [port]` inside the CLI to retry.
107
+
108
+ Package installation also makes a best-effort, non-blocking webapp initialization. Install never fails because the optional local webapp could not start.
107
109
 
108
110
  Launch the web UI explicitly when you want a foreground web server:
109
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.123",
3
+ "version": "0.20.124",
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",
@@ -50,6 +50,7 @@
50
50
  "scripts/install-docker-ubuntu.sh",
51
51
  "scripts/setup-agent-toolchain-docker.sh",
52
52
  "scripts/real-deepseek-capabilities.js",
53
+ "scripts/postinstall-webapp.js",
53
54
  "scripts/smoke-auxiliary-tools.js",
54
55
  "scripts/smoke-auth.js",
55
56
  "scripts/smoke-aaps-adapter.js",
@@ -70,6 +71,7 @@
70
71
  "scripts/smoke-toolchain-docker.js",
71
72
  "scripts/smoke-web-autostart.js",
72
73
  "scripts/smoke-web-api.js",
74
+ "scripts/smoke-webapp-command.js",
73
75
  "scripts/smoke-web-port-fallback.js",
74
76
  "src/",
75
77
  "run.js",
@@ -103,12 +105,14 @@
103
105
  "smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
104
106
  "smoke:web-api": "node scripts/smoke-web-api.js",
105
107
  "smoke:web-autostart": "node scripts/smoke-web-autostart.js",
108
+ "smoke:webapp-command": "node scripts/smoke-webapp-command.js",
106
109
  "smoke:web-port-fallback": "node scripts/smoke-web-port-fallback.js",
107
110
  "smoke:autoupdate": "node scripts/smoke-auto-update.js",
108
111
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
112
+ "postinstall": "node scripts/postinstall-webapp.js",
109
113
  "supervision:seed": "node scripts/seed-supervised-homework.js",
110
114
  "storage:migrate": "node bin/aginti-cli.js storage migrate",
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",
115
+ "test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:web-autostart && npm run smoke:webapp-command && 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",
112
116
  "pack:dry-run": "npm pack --dry-run",
113
117
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
114
118
  },
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { ensureAgintiWebApp } from "../src/web-autostart.js";
5
+
6
+ const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const cwd = process.env.INIT_CWD || process.cwd();
8
+
9
+ if (process.env.CI === "true" || process.env.AGINTIFLOW_SKIP_POSTINSTALL_WEBAPP === "1") {
10
+ process.exit(0);
11
+ }
12
+
13
+ try {
14
+ await ensureAgintiWebApp({
15
+ packageDir,
16
+ cwd,
17
+ host: process.env.AGINTI_WEB_HOST || "127.0.0.1",
18
+ preferredPort: process.env.AGINTI_WEB_PORT || 3210,
19
+ language: process.env.AGINTI_LANGUAGE || "",
20
+ });
21
+ } catch {
22
+ // Installation must not fail because the optional local webapp could not be started.
23
+ }
@@ -326,6 +326,15 @@ try {
326
326
  if (!launchHeaderWithWeb.includes("webapp: http://127.0.0.1:3210")) {
327
327
  throw new Error("launch header did not render the active webapp URL in the tagline row");
328
328
  }
329
+ const launchHeaderWithWebError = buildLaunchHeaderLines({
330
+ width: 120,
331
+ packageVersion: "0.0.0",
332
+ animated: false,
333
+ webAppNotice: "webapp unavailable - use /webapp to retry; error: smoke failure",
334
+ }).join("\n");
335
+ if (!launchHeaderWithWebError.includes("webapp unavailable - use /webapp to retry")) {
336
+ throw new Error("launch header did not render webapp recovery advice in the tagline row");
337
+ }
329
338
  if (
330
339
  formatElapsedDuration(0) !== "00:00" ||
331
340
  formatElapsedDuration(65_000) !== "01:05" ||
@@ -650,6 +659,7 @@ try {
650
659
  !helpResult.stdout.includes("/auxiliary") ||
651
660
  !helpResult.stdout.includes("/review") ||
652
661
  !helpResult.stdout.includes("/scs") ||
662
+ !helpResult.stdout.includes("/webapp") ||
653
663
  !helpResult.stdout.includes("/safe") ||
654
664
  helpResult.stdout.includes("helpReview") ||
655
665
  helpResult.stdout.includes("helpRename") ||
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import { execFile as execFileCallback, 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 { promisify } from "node:util";
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-webapp-command-"));
11
+ const execFile = promisify(execFileCallback);
12
+
13
+ function delay(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+
17
+ async function waitFor(predicate, child, label, output) {
18
+ const deadline = Date.now() + 12000;
19
+ while (Date.now() < deadline) {
20
+ if (predicate()) return true;
21
+ if (child.exitCode !== null) break;
22
+ await delay(150);
23
+ }
24
+ const { stdout, stderr } = output;
25
+ throw new Error(`${label} timed out. stdout=${stdout.slice(-1000)} stderr=${stderr.slice(-1000)}`);
26
+ }
27
+
28
+ async function killPort(port) {
29
+ try {
30
+ const { stdout: pids } = await execFile("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"]);
31
+ for (const pid of pids.split(/\s+/).filter(Boolean)) {
32
+ try {
33
+ process.kill(Number(pid), "SIGTERM");
34
+ } catch {
35
+ // Ignore already-exited listeners.
36
+ }
37
+ }
38
+ } catch {
39
+ // lsof may be unavailable; the temporary port listener will otherwise be reusable by later tests.
40
+ }
41
+ }
42
+
43
+ async function runCase({ port, env = {}, expectHeader, label }) {
44
+ const output = { stdout: "", stderr: "" };
45
+ const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "chat", "--provider", "mock", "--routing", "manual", "--port", String(port)], {
46
+ cwd: runtimeDir,
47
+ env: {
48
+ ...process.env,
49
+ AGINTIFLOW_NO_ANIMATION: "1",
50
+ AGINTIFLOW_HOME: path.join(runtimeDir, `.agintiflow-home-${label}`),
51
+ ...env,
52
+ },
53
+ stdio: ["pipe", "pipe", "pipe"],
54
+ });
55
+
56
+ child.stdout.on("data", (chunk) => {
57
+ output.stdout += chunk.toString();
58
+ });
59
+ child.stderr.on("data", (chunk) => {
60
+ output.stderr += chunk.toString();
61
+ });
62
+
63
+ try {
64
+ await waitFor(() => output.stdout.includes(expectHeader), child, `${label} launch header`, output);
65
+ child.stdin.write(`/webapp ${port}\n`);
66
+ await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port}`), child, `${label} /webapp command`, output);
67
+ const health = await fetch(`http://127.0.0.1:${port}/health`).then((response) => response.json());
68
+ if (!health.ok || health.app !== "agintiflow" || Number(health.port) !== port) {
69
+ throw new Error(`invalid /webapp health response for ${label}: ${JSON.stringify(health)}`);
70
+ }
71
+ } finally {
72
+ child.kill("SIGTERM");
73
+ await killPort(port);
74
+ }
75
+ }
76
+
77
+ const autoPort = 45600 + Math.floor(Math.random() * 300);
78
+ const manualPort = autoPort + 301;
79
+
80
+ try {
81
+ await runCase({
82
+ port: autoPort,
83
+ expectHeader: `webapp: http://127.0.0.1:${autoPort}`,
84
+ label: "auto-start",
85
+ });
86
+ await runCase({
87
+ port: manualPort,
88
+ env: { AGINTIFLOW_NO_WEB_AUTO_START: "1" },
89
+ expectHeader: "webapp auto-start disabled - use /webapp to start manually",
90
+ label: "manual-disabled",
91
+ });
92
+ console.log(`webapp slash command smoke passed: http://127.0.0.1:${autoPort}, http://127.0.0.1:${manualPort}`);
93
+ } finally {
94
+ await killPort(autoPort);
95
+ await killPort(manualPort);
96
+ await fs.rm(runtimeDir, { recursive: true, force: true });
97
+ }
package/src/cli.js CHANGED
@@ -107,6 +107,14 @@ async function maybeEnsureDefaultWebApp(args = {}, { commandCwd = process.cwd()
107
107
  }
108
108
  }
109
109
 
110
+ function webAppNotice(result) {
111
+ if (!result || result.ok) return "";
112
+ if (result.disabled) return "webapp auto-start disabled - use /webapp to start manually";
113
+ const error = String(result.error || "unknown").replace(/\s+/g, " ").trim();
114
+ const compact = error.length > 84 ? `${error.slice(0, 83)}…` : error;
115
+ return `webapp unavailable - use /webapp to retry; error: ${compact}`;
116
+ }
117
+
110
118
  const CLI_VALUE_OPTIONS = new Set([
111
119
  "--port",
112
120
  "--host",
@@ -1755,6 +1763,7 @@ export async function main(argv = process.argv.slice(2)) {
1755
1763
  packageDir,
1756
1764
  packageVersion: packageJson.version,
1757
1765
  webAppUrl: webLaunch.ok ? webLaunch.url : "",
1766
+ webAppNotice: webAppNotice(webLaunch),
1758
1767
  });
1759
1768
  return;
1760
1769
  }
@@ -1783,7 +1792,12 @@ export async function main(argv = process.argv.slice(2)) {
1783
1792
 
1784
1793
  if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
1785
1794
  const webLaunch = await maybeEnsureDefaultWebApp(args, { commandCwd });
1786
- await startInteractiveCli(agentDefaults(args), { packageDir, packageVersion: packageJson.version, webAppUrl: webLaunch.ok ? webLaunch.url : "" });
1795
+ await startInteractiveCli(agentDefaults(args), {
1796
+ packageDir,
1797
+ packageVersion: packageJson.version,
1798
+ webAppUrl: webLaunch.ok ? webLaunch.url : "",
1799
+ webAppNotice: webAppNotice(webLaunch),
1800
+ });
1787
1801
  return;
1788
1802
  }
1789
1803
 
package/src/i18n.js CHANGED
@@ -139,6 +139,7 @@ const TRANSLATIONS = {
139
139
  helpSkills: "List Markdown skills selected for a topic.",
140
140
  helpSkillMesh: "Manage strict reviewed skill sharing.",
141
141
  helpProfile: "Set task profile, e.g. code, website, latex, maintenance.",
142
+ helpWebapp: "Start or reuse the local webapp and print its URL.",
142
143
  helpWebSearch: "Enable or disable the web_search tool.",
143
144
  helpEnableScs: "Toggle Student-Committee-Supervisor gated execution.",
144
145
  helpScouts: "Enable parallel DeepSeek scouts and set scout count.",
@@ -43,6 +43,7 @@ import {
43
43
  permissionModeForApprovalCategory,
44
44
  permissionModeLabel,
45
45
  } from "./permission-modes.js";
46
+ import { ensureAgintiWebApp } from "./web-autostart.js";
46
47
 
47
48
  const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
48
49
  const ansi = {
@@ -95,6 +96,8 @@ const SLASH_COMMANDS = [
95
96
  "/skillsync",
96
97
  "/profile",
97
98
  "/web-search",
99
+ "/webapp",
100
+ "/web",
98
101
  "/web-research",
99
102
  "/image-read",
100
103
  "/research-wrapper",
@@ -772,10 +775,19 @@ function launchTitleLines(contentWidth) {
772
775
  return largeWidth <= contentWidth ? LARGE_LAUNCH_TITLE : COMPACT_LAUNCH_TITLE;
773
776
  }
774
777
 
775
- export function buildLaunchHeaderLines({ packageVersion = "", frame = 2, width = terminalWidth(), animated = true, language = cliLanguage, webAppUrl = "" } = {}) {
778
+ export function buildLaunchHeaderLines({
779
+ packageVersion = "",
780
+ frame = 2,
781
+ width = terminalWidth(),
782
+ animated = true,
783
+ language = cliLanguage,
784
+ webAppUrl = "",
785
+ webAppNotice = "",
786
+ } = {}) {
776
787
  const subtitle = t("launchSubtitle", language);
777
788
  const credit = t("launchCredit", language);
778
- const tagline = webAppUrl ? `webapp: ${webAppUrl}` : t("launchTagline", language);
789
+ const webappLine = webAppUrl ? `webapp: ${webAppUrl}` : webAppNotice;
790
+ const tagline = webappLine || t("launchTagline", language);
779
791
  const version = packageVersion ? `v${packageVersion}` : "";
780
792
  const terminalColumns = Math.max(Number(width) || 80, 50);
781
793
  const contentWidth = Math.min(Math.max(terminalColumns - 8, 58), 112);
@@ -794,16 +806,16 @@ export function buildLaunchHeaderLines({ packageVersion = "", frame = 2, width =
794
806
  row(centerLine(subtitle, contentWidth), ansi.dim),
795
807
  row(centerLine(credit, contentWidth), ansi.dim),
796
808
  tagline ? mid : "",
797
- tagline ? row(centerLine(tagline, contentWidth), ansi.cyan) : "",
809
+ tagline ? row(centerLine(compactLine(tagline, contentWidth), contentWidth), webAppUrl ? ansi.cyan : ansi.yellow) : "",
798
810
  bottom,
799
811
  ].filter(Boolean);
800
812
  const indent = " ".repeat(Math.max(Math.floor((terminalColumns - Math.max(...boxLines.map((line) => visualLength(line)))) / 2), 0));
801
813
  return boxLines.map((line) => `${indent}${line}`);
802
814
  }
803
815
 
804
- async function renderLaunchHeader(packageVersion = "", language = cliLanguage, webAppUrl = "") {
816
+ async function renderLaunchHeader(packageVersion = "", language = cliLanguage, webAppUrl = "", webAppNotice = "") {
805
817
  if (!useColor || process.env.AGINTIFLOW_NO_ANIMATION === "1") {
806
- console.log(buildLaunchHeaderLines({ packageVersion, animated: false, language, webAppUrl }).join("\n"));
818
+ console.log(buildLaunchHeaderLines({ packageVersion, animated: false, language, webAppUrl, webAppNotice }).join("\n"));
807
819
  return;
808
820
  }
809
821
 
@@ -812,7 +824,7 @@ async function renderLaunchHeader(packageVersion = "", language = cliLanguage, w
812
824
  let previousLineCount = 0;
813
825
  output.write(ansi.cursorHide);
814
826
  for (let frame = 0; frame < 18; frame += 1) {
815
- const lines = [...paddingLines, ...buildLaunchHeaderLines({ packageVersion, frame, animated: true, language, webAppUrl })];
827
+ const lines = [...paddingLines, ...buildLaunchHeaderLines({ packageVersion, frame, animated: true, language, webAppUrl, webAppNotice })];
816
828
  if (previousLineCount > 0) output.write(`\x1b[${previousLineCount}A`);
817
829
  output.write(lines.map((line) => `\r${ansi.clearLine}${line}`).join("\n"));
818
830
  output.write("\n");
@@ -849,6 +861,7 @@ function printHelp() {
849
861
  ` ${command("/skills [query]", "List Markdown skills selected for a topic.", "helpSkills")}`,
850
862
  ` ${command("/skillmesh [status|off|record|share|sync]", "Manage strict reviewed skill sharing.", "helpSkillMesh")}`,
851
863
  ` ${command("/profile <name>", "Set task profile, e.g. code, website, latex, maintenance.", "helpProfile")}`,
864
+ ` ${command("/webapp [port]", "Start or reuse the local webapp and print its URL.", "helpWebapp")}`,
852
865
  ` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
853
866
  ` ${command("/web-research <query>", "Run a sourced web_research turn with persisted evidence.", "helpWebSearch")}`,
854
867
  ` ${command("/image-read <path> [question]", "Run read_image on a workspace screenshot/image.", "helpWebSearch")}`,
@@ -2170,6 +2183,8 @@ function createState(args = {}) {
2170
2183
  dynamicStepHardCap: args.dynamicStepHardCap,
2171
2184
  dynamicStepExtensionSize: args.dynamicStepExtensionSize,
2172
2185
  sessionId: args.resume || "",
2186
+ webAppUrl: args.webAppUrl || "",
2187
+ webAppNotice: args.webAppNotice || "",
2173
2188
  };
2174
2189
  }
2175
2190
 
@@ -2975,6 +2990,28 @@ async function handleCommand(line, state, packageDir) {
2975
2990
  );
2976
2991
  return true;
2977
2992
  }
2993
+ if (command === "webapp" || command === "web") {
2994
+ const port = Number(value) || Number(process.env.AGINTI_WEB_PORT || process.env.PORT || 3210);
2995
+ printSystemLine("webapp=starting");
2996
+ const result = await ensureAgintiWebApp({
2997
+ packageDir,
2998
+ cwd: state.commandCwd || process.cwd(),
2999
+ host: process.env.AGINTI_WEB_HOST || process.env.HOST || "127.0.0.1",
3000
+ preferredPort: port,
3001
+ language: state.language,
3002
+ respectAutoStartDisable: false,
3003
+ }).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
3004
+ if (result.ok) {
3005
+ state.webAppUrl = result.url;
3006
+ state.webAppNotice = "";
3007
+ printSystemLine(`webapp=${result.url} ${result.reused ? "reused" : "started"}`);
3008
+ } else {
3009
+ state.webAppUrl = "";
3010
+ state.webAppNotice = `webapp unavailable - use /webapp to retry; error: ${compactLine(result.error || "unknown", 72)}`;
3011
+ printSystemLine(state.webAppNotice);
3012
+ }
3013
+ return true;
3014
+ }
2978
3015
  if (command === "language" || command === "lang") {
2979
3016
  if (!value) {
2980
3017
  printSystemLine(tr("languageSet", { language: state.language || cliLanguage, label: languageLabel(state.language || cliLanguage) }));
@@ -3599,12 +3636,6 @@ async function handleCommand(line, state, packageDir) {
3599
3636
  printAgentMessage(`initialized project=${result.projectRoot}\nAGINTI.md=${result.instructionsPath}\ntemplate=${result.template}`);
3600
3637
  return true;
3601
3638
  }
3602
- if (command === "web") {
3603
- const port = value || "3220";
3604
- printAgentMessage(`Run in another terminal: aginti web --port ${port}`);
3605
- return true;
3606
- }
3607
-
3608
3639
  const typed = `/${command}`;
3609
3640
  const suggestions = SLASH_COMMANDS.filter((candidate) => candidate.startsWith(typed));
3610
3641
  printAgentMessage(
@@ -3808,8 +3839,8 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
3808
3839
  return queuedAfterFinish;
3809
3840
  }
3810
3841
 
3811
- export async function startInteractiveCli(args = {}, { packageDir, packageVersion, webAppUrl = "" } = {}) {
3812
- const state = createState(args);
3842
+ export async function startInteractiveCli(args = {}, { packageDir, packageVersion, webAppUrl = "", webAppNotice = "" } = {}) {
3843
+ const state = createState({ ...args, webAppUrl, webAppNotice });
3813
3844
  const detachProcessInterrupts = installProcessInterruptHandlers();
3814
3845
  setCliLanguage(state.language);
3815
3846
  const rl =
@@ -3822,7 +3853,7 @@ export async function startInteractiveCli(args = {}, { packageDir, packageVersio
3822
3853
  completer: commandCompleter,
3823
3854
  });
3824
3855
 
3825
- await renderLaunchHeader(packageVersion, state.language, webAppUrl);
3856
+ await renderLaunchHeader(packageVersion, state.language, state.webAppUrl, state.webAppNotice);
3826
3857
  printSystemLine(`${tr("project")}: ${process.cwd()}`);
3827
3858
  await maybeOnboardDeepSeekKey(state);
3828
3859
  printAgentMessage(tr("interactiveIntro"));
@@ -86,8 +86,9 @@ export async function ensureAgintiWebApp({
86
86
  host = DEFAULT_HOST,
87
87
  preferredPort = DEFAULT_PORT,
88
88
  language = "",
89
+ respectAutoStartDisable = true,
89
90
  } = {}) {
90
- if (process.env.AGINTI_NO_WEB_AUTO_START === "1" || process.env.AGINTIFLOW_NO_WEB_AUTO_START === "1") {
91
+ if (respectAutoStartDisable && (process.env.AGINTI_NO_WEB_AUTO_START === "1" || process.env.AGINTIFLOW_NO_WEB_AUTO_START === "1")) {
91
92
  return { ok: false, disabled: true, url: "" };
92
93
  }
93
94