@lazyingart/agintiflow 0.20.130 → 0.20.132
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 +4 -2
- package/package.json +4 -2
- package/scripts/smoke-runtime-compat.js +110 -0
- package/scripts/smoke-webapp-command.js +11 -0
- package/src/cli.js +20 -5
- package/src/i18n.js +1 -1
- package/src/interactive-cli.js +13 -4
- package/src/platform.js +3 -0
- package/src/project.js +4 -0
- package/src/session-index.js +119 -18
- package/src/sqlite.js +112 -4
- package/src/web-autostart.js +28 -1
- package/src/web-db.js +154 -29
- package/web.js +1 -0
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ 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 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 by another project. The active URL is shown in the CLI launch header. If startup is blocked, stale, or unavailable, the same header row shows the recovery hint; run `/webapp [port]` inside the CLI to retry, `/webapp stop [port]` to stop the compatible local webapp, or `/webapp restart [port]` to stop and relaunch the local webapp with the current project and canonical `~/.agintiflow` session home. After a successful `aginti update` or accepted startup auto-update, AgInTiFlow
|
|
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 by another project. The active URL is shown in the CLI launch header. If startup is blocked, stale, or unavailable, the same header row shows the recovery hint; run `/webapp [port]` inside the CLI to retry, `/webapp stop [port]` to stop the compatible local webapp, or `/webapp restart [port]` to stop and relaunch the local webapp with the current project and canonical `~/.agintiflow` session home. Use `/webapp disable` or `aginti webapp disable` to persistently disable automatic webapp startup and update-time restarts; use `/webapp enable` or `aginti webapp enable` to restore them. After a successful `aginti update` or accepted startup auto-update, AgInTiFlow restarts the compatible local webapp only when webapp auto-start is enabled.
|
|
107
107
|
|
|
108
108
|
Package installation also makes a best-effort, non-blocking webapp initialization. Install never fails because the optional local webapp could not start.
|
|
109
109
|
|
|
@@ -111,6 +111,8 @@ Launch the web UI explicitly when you want a foreground web server:
|
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
113
|
aginti webapp
|
|
114
|
+
aginti webapp disable
|
|
115
|
+
aginti webapp enable
|
|
114
116
|
aginti webapp stop
|
|
115
117
|
aginti webapp restart
|
|
116
118
|
aginti web --port 3210
|
|
@@ -146,7 +148,7 @@ aginti --language de
|
|
|
146
148
|
| Goal | Command |
|
|
147
149
|
| --- | --- |
|
|
148
150
|
| Start interactive chat | `aginti` or `aginti chat` |
|
|
149
|
-
| Start local web app | Auto-starts with `aginti`; detached command is `aginti webapp`; stop with `aginti webapp stop`; restart with `aginti webapp restart`; foreground mode is `aginti web --port 3210` |
|
|
151
|
+
| Start local web app | Auto-starts with `aginti`; detached command is `aginti webapp`; disable/enable auto-start with `aginti webapp disable` / `aginti webapp enable`; stop with `aginti webapp stop`; restart with `aginti webapp restart`; foreground mode is `aginti web --port 3210` |
|
|
150
152
|
| Save provider keys | `aginti auth`, `/auth`, `/login` |
|
|
151
153
|
| Review current repo | `/review [focus]` |
|
|
152
154
|
| 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.132",
|
|
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",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"scripts/smoke-platform.js",
|
|
67
67
|
"scripts/smoke-perception-research.js",
|
|
68
68
|
"scripts/smoke-permission-modes.js",
|
|
69
|
+
"scripts/smoke-runtime-compat.js",
|
|
69
70
|
"scripts/smoke-skills.js",
|
|
70
71
|
"scripts/smoke-skillmesh.js",
|
|
71
72
|
"scripts/smoke-tmux-tools.js",
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
"smoke:platform": "node scripts/smoke-platform.js",
|
|
105
106
|
"smoke:perception-research": "node scripts/smoke-perception-research.js",
|
|
106
107
|
"smoke:permission-modes": "node scripts/smoke-permission-modes.js",
|
|
108
|
+
"smoke:runtime-compat": "node scripts/smoke-runtime-compat.js",
|
|
107
109
|
"smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
|
|
108
110
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
109
111
|
"smoke:web-autostart": "node scripts/smoke-web-autostart.js",
|
|
@@ -114,7 +116,7 @@
|
|
|
114
116
|
"postinstall": "node scripts/postinstall-webapp.js",
|
|
115
117
|
"supervision:seed": "node scripts/seed-supervised-homework.js",
|
|
116
118
|
"storage:migrate": "node bin/aginti-cli.js storage migrate",
|
|
117
|
-
"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:docker-command && 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",
|
|
119
|
+
"test": "npm run check && npm run smoke:runtime-compat && 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:docker-command && 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",
|
|
118
120
|
"pack:dry-run": "npm pack --dry-run",
|
|
119
121
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
120
122
|
},
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
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 tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-runtime-compat-"));
|
|
11
|
+
const agintiflowHome = path.join(tempRoot, ".agintiflow-home");
|
|
12
|
+
process.env.AGINTIFLOW_HOME = agintiflowHome;
|
|
13
|
+
process.env.AGINTIFLOW_FORCE_JSON_DB = "1";
|
|
14
|
+
|
|
15
|
+
const { formatNodeSqliteRecovery, nodeSqliteStatus } = await import("../src/sqlite.js");
|
|
16
|
+
const { WebDatabase } = await import("../src/web-db.js");
|
|
17
|
+
const { deleteSessionIndex, listSessionIndex, renameSessionIndex, upsertSessionIndex } = await import("../src/session-index.js");
|
|
18
|
+
|
|
19
|
+
const sqliteStatus = nodeSqliteStatus();
|
|
20
|
+
assert.equal(sqliteStatus.ok, false, "forced sqlite fallback should report unavailable node:sqlite");
|
|
21
|
+
assert.equal(sqliteStatus.forcedJson, true, "forced sqlite fallback should be marked as forced");
|
|
22
|
+
const recovery = formatNodeSqliteRecovery(sqliteStatus);
|
|
23
|
+
assert.match(recovery, /Node\.js 22\+/);
|
|
24
|
+
assert.match(recovery, /npm install -g @lazyingart\/agintiflow@latest/);
|
|
25
|
+
|
|
26
|
+
const db = new WebDatabase(tempRoot);
|
|
27
|
+
assert.equal(db.driver, "json", "WebDatabase should use JSON fallback when node:sqlite is unavailable");
|
|
28
|
+
|
|
29
|
+
const preferences = db.getPreferences();
|
|
30
|
+
db.savePreferences({ ...preferences, model: "runtime-compat-model" });
|
|
31
|
+
assert.equal(db.getPreferences().model, "runtime-compat-model", "JSON fallback preferences should persist");
|
|
32
|
+
|
|
33
|
+
const session = {
|
|
34
|
+
sessionId: "runtime-compat-session",
|
|
35
|
+
projectRoot: tempRoot,
|
|
36
|
+
commandCwd: tempRoot,
|
|
37
|
+
provider: "mock",
|
|
38
|
+
model: "mock-model",
|
|
39
|
+
goal: "runtime compatibility",
|
|
40
|
+
title: "Runtime Compatibility",
|
|
41
|
+
status: "finished",
|
|
42
|
+
startedAt: "2026-05-14T00:00:00.000Z",
|
|
43
|
+
updatedAt: "2026-05-14T00:01:00.000Z",
|
|
44
|
+
result: "ok",
|
|
45
|
+
};
|
|
46
|
+
db.upsertSession(session);
|
|
47
|
+
assert.equal(db.getSession(session.sessionId)?.title, "Runtime Compatibility", "JSON fallback session should persist");
|
|
48
|
+
assert.equal(db.listSessions(5).length, 1, "JSON fallback session listing should work");
|
|
49
|
+
assert.equal(db.renameSession(session.sessionId, "Renamed Runtime Compatibility"), true, "JSON fallback rename should work");
|
|
50
|
+
assert.equal(db.getSession(session.sessionId)?.title, "Renamed Runtime Compatibility", "JSON fallback rename should persist");
|
|
51
|
+
assert.equal(db.deleteSession(session.sessionId), true, "JSON fallback delete should work");
|
|
52
|
+
assert.equal(db.getSession(session.sessionId), null, "JSON fallback delete should remove the session");
|
|
53
|
+
|
|
54
|
+
assert.equal(upsertSessionIndex(session), true, "JSON fallback global index upsert should work");
|
|
55
|
+
assert.equal(listSessionIndex({ projectRoot: tempRoot }).length, 1, "JSON fallback global index listing should work");
|
|
56
|
+
assert.equal(renameSessionIndex(session.sessionId, "Indexed Runtime Compatibility"), true, "JSON fallback global index rename should work");
|
|
57
|
+
assert.equal(deleteSessionIndex(session.sessionId), true, "JSON fallback global index delete should work");
|
|
58
|
+
|
|
59
|
+
const port = 45200 + Math.floor(Math.random() * 500);
|
|
60
|
+
const server = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "web", "--port", String(port), "--host", "127.0.0.1"], {
|
|
61
|
+
cwd: tempRoot,
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
AGINTIFLOW_HOME: agintiflowHome,
|
|
65
|
+
AGINTIFLOW_RUNTIME_DIR: tempRoot,
|
|
66
|
+
AGINTIFLOW_FORCE_JSON_DB: "1",
|
|
67
|
+
},
|
|
68
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let stdout = "";
|
|
72
|
+
let stderr = "";
|
|
73
|
+
server.stdout.on("data", (chunk) => {
|
|
74
|
+
stdout += chunk.toString();
|
|
75
|
+
});
|
|
76
|
+
server.stderr.on("data", (chunk) => {
|
|
77
|
+
stderr += chunk.toString();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function delay(ms) {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function waitForHealth() {
|
|
85
|
+
const deadline = Date.now() + 15000;
|
|
86
|
+
while (Date.now() < deadline) {
|
|
87
|
+
if (server.exitCode !== null) break;
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
|
90
|
+
const health = await response.json();
|
|
91
|
+
if (health.ok) return health;
|
|
92
|
+
} catch {
|
|
93
|
+
await delay(250);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`forced JSON web health failed. stdout=${stdout.slice(-500)} stderr=${stderr.slice(-500)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const health = await waitForHealth();
|
|
101
|
+
assert.equal(health.storageDriver, "json", "web health should advertise JSON fallback storage");
|
|
102
|
+
assert.equal(health.runtimeDir, tempRoot, "web runtime should use the requested project root");
|
|
103
|
+
} finally {
|
|
104
|
+
if (server.exitCode === null && !server.killed) {
|
|
105
|
+
server.kill("SIGTERM");
|
|
106
|
+
await new Promise((resolve) => server.once("exit", resolve));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log("runtime compatibility smoke ok");
|
|
@@ -14,6 +14,10 @@ function delay(ms) {
|
|
|
14
14
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function occurrenceCount(value, pattern) {
|
|
18
|
+
return String(value || "").split(pattern).length - 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
async function waitFor(predicate, child, label, output) {
|
|
18
22
|
const deadline = Date.now() + 12000;
|
|
19
23
|
while (Date.now() < deadline) {
|
|
@@ -74,6 +78,13 @@ async function runCase({ port, env = {}, expectHeader, label }) {
|
|
|
74
78
|
if (path.resolve(health.agintiflowHome) !== path.resolve(path.join(runtimeDir, `.agintiflow-web-home-${label}`))) {
|
|
75
79
|
throw new Error(`webapp command inherited the wrong home for ${label}: ${JSON.stringify(health)}`);
|
|
76
80
|
}
|
|
81
|
+
child.stdin.write(`/webapp disable\n`);
|
|
82
|
+
await waitFor(() => output.stdout.includes("webapp auto-start=disabled"), child, `${label} /webapp disable command`, output);
|
|
83
|
+
const disabledCount = occurrenceCount(output.stdout, "webapp auto-start=disabled");
|
|
84
|
+
child.stdin.write(`/webapp status\n`);
|
|
85
|
+
await waitFor(() => occurrenceCount(output.stdout, "webapp auto-start=disabled") > disabledCount, child, `${label} /webapp status disabled command`, output);
|
|
86
|
+
child.stdin.write(`/webapp enable\n`);
|
|
87
|
+
await waitFor(() => output.stdout.includes("webapp auto-start=enabled"), child, `${label} /webapp enable command`, output);
|
|
77
88
|
child.stdin.write(`/webapp stop ${port}\n`);
|
|
78
89
|
await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port} stopped`), child, `${label} /webapp stop command`, output);
|
|
79
90
|
let stopped = false;
|
package/src/cli.js
CHANGED
|
@@ -37,7 +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, stopAgintiWebApp } from "./web-autostart.js";
|
|
40
|
+
import { ensureAgintiWebApp, readWebAppPreference, stopAgintiWebApp, writeWebAppPreference } from "./web-autostart.js";
|
|
41
41
|
import { dockerHostInstallPlan, formatDockerSetupText, summarizeDockerSetup } from "./docker-setup.js";
|
|
42
42
|
import fs from "node:fs/promises";
|
|
43
43
|
import path from "node:path";
|
|
@@ -403,7 +403,7 @@ export function parseArgs(argv) {
|
|
|
403
403
|
parts.push(...argv.slice(i + 1));
|
|
404
404
|
break;
|
|
405
405
|
}
|
|
406
|
-
if ((arg === "web" || arg === "--web") && ["restart", "stop"].includes(String(argv[i + 1] || "").toLowerCase())) {
|
|
406
|
+
if ((arg === "web" || arg === "--web") && ["restart", "stop", "enable", "disable", "status"].includes(String(argv[i + 1] || "").toLowerCase())) {
|
|
407
407
|
result.webapp = true;
|
|
408
408
|
result.webAction = String(argv[i + 1] || "").toLowerCase();
|
|
409
409
|
i += 1;
|
|
@@ -902,7 +902,7 @@ async function restartWebAppAfterUpdate({ commandCwd = process.cwd(), language =
|
|
|
902
902
|
preferredPort: process.env.AGINTI_WEB_PORT || process.env.PORT || 3210,
|
|
903
903
|
language,
|
|
904
904
|
restart: true,
|
|
905
|
-
respectAutoStartDisable:
|
|
905
|
+
respectAutoStartDisable: true,
|
|
906
906
|
}).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: "" }));
|
|
907
907
|
}
|
|
908
908
|
|
|
@@ -1098,6 +1098,7 @@ function parseInitOptions(argv = []) {
|
|
|
1098
1098
|
function printDoctorReport(report) {
|
|
1099
1099
|
console.log(`AgInTiFlow ${report.package.version} (npm latest: ${report.package.npmLatest})`);
|
|
1100
1100
|
console.log(`node=${report.node.version} ok=${report.node.ok}`);
|
|
1101
|
+
console.log(`nodeSqlite=${report.node.sqlite?.ok ? "available" : "fallback-json"} storage=${report.node.sqlite?.ok ? "sqlite" : "json"}`);
|
|
1101
1102
|
console.log(`platform=${report.platform.label} family=${report.platform.linuxFamily || report.platform.platform}`);
|
|
1102
1103
|
console.log(`project=${report.project.root}`);
|
|
1103
1104
|
console.log(`instructions=${report.project.instructionsPath} present=${report.project.instructionsPresent}`);
|
|
@@ -1126,6 +1127,10 @@ function printDoctorReport(report) {
|
|
|
1126
1127
|
console.log("platform setup hints:");
|
|
1127
1128
|
for (const hint of report.platform.setupHints) console.log(`- ${hint}`);
|
|
1128
1129
|
}
|
|
1130
|
+
if (report.node.recovery?.length) {
|
|
1131
|
+
console.log("node runtime recovery:");
|
|
1132
|
+
for (const line of report.node.recovery) console.log(`- ${line}`);
|
|
1133
|
+
}
|
|
1129
1134
|
}
|
|
1130
1135
|
|
|
1131
1136
|
async function readStdin() {
|
|
@@ -1949,8 +1954,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1949
1954
|
|
|
1950
1955
|
if (args.webapp) {
|
|
1951
1956
|
const action = String(args.webAction || "start").toLowerCase();
|
|
1952
|
-
if (!["start", "stop", "restart", "reuse"].includes(action)) {
|
|
1953
|
-
console.error("Usage: aginti webapp [start|stop|restart|reuse] [--port 3210] [--host 127.0.0.1]");
|
|
1957
|
+
if (!["start", "stop", "restart", "reuse", "enable", "disable", "status"].includes(action)) {
|
|
1958
|
+
console.error("Usage: aginti webapp [start|stop|restart|reuse|enable|disable|status] [--port 3210] [--host 127.0.0.1]");
|
|
1954
1959
|
process.exit(1);
|
|
1955
1960
|
}
|
|
1956
1961
|
const webOptions = {
|
|
@@ -1960,6 +1965,16 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1960
1965
|
host: args.host || process.env.AGINTI_WEB_HOST || "127.0.0.1",
|
|
1961
1966
|
preferredPort: args.port || process.env.AGINTI_WEB_PORT || 3210,
|
|
1962
1967
|
};
|
|
1968
|
+
if (action === "enable" || action === "disable" || action === "status") {
|
|
1969
|
+
const preference =
|
|
1970
|
+
action === "status"
|
|
1971
|
+
? await readWebAppPreference({ home: webOptions.home })
|
|
1972
|
+
: await writeWebAppPreference({ home: webOptions.home, autoStart: action === "enable" });
|
|
1973
|
+
console.log(`webapp auto-start: ${preference.autoStart ? "enabled" : "disabled"}`);
|
|
1974
|
+
console.log(`preference: ${preference.path}`);
|
|
1975
|
+
if (!preference.autoStart) console.log("Use `aginti webapp stop` or `/webapp stop` to stop a currently running webapp.");
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1963
1978
|
const result = await (action === "stop"
|
|
1964
1979
|
? stopAgintiWebApp(webOptions)
|
|
1965
1980
|
: ensureAgintiWebApp({
|
package/src/i18n.js
CHANGED
|
@@ -139,7 +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, reuse, stop, or
|
|
142
|
+
helpWebapp: "Start, reuse, stop, restart, or configure the local webapp.",
|
|
143
143
|
helpWebSearch: "Enable or disable the web_search tool.",
|
|
144
144
|
helpEnableScs: "Toggle Student-Committee-Supervisor gated execution.",
|
|
145
145
|
helpScouts: "Enable parallel DeepSeek scouts and set scout count.",
|
package/src/interactive-cli.js
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
permissionModeForApprovalCategory,
|
|
46
46
|
permissionModeLabel,
|
|
47
47
|
} from "./permission-modes.js";
|
|
48
|
-
import { ensureAgintiWebApp, stopAgintiWebApp } from "./web-autostart.js";
|
|
48
|
+
import { ensureAgintiWebApp, readWebAppPreference, stopAgintiWebApp, writeWebAppPreference } from "./web-autostart.js";
|
|
49
49
|
|
|
50
50
|
const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
|
|
51
51
|
const ansi = {
|
|
@@ -881,7 +881,7 @@ function printHelp() {
|
|
|
881
881
|
` ${command("/skills [query]", "List Markdown skills selected for a topic.", "helpSkills")}`,
|
|
882
882
|
` ${command("/skillmesh [status|off|record|share|sync]", "Manage strict reviewed skill sharing.", "helpSkillMesh")}`,
|
|
883
883
|
` ${command("/profile <name>", "Set task profile, e.g. code, website, latex, maintenance.", "helpProfile")}`,
|
|
884
|
-
` ${command("/webapp [port|start|stop|restart|reuse]", "Start, reuse, stop, or
|
|
884
|
+
` ${command("/webapp [port|start|stop|restart|reuse|enable|disable|status]", "Start, reuse, stop, restart, or configure the local webapp.", "helpWebapp")}`,
|
|
885
885
|
` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
|
|
886
886
|
` ${command("/web-research <query>", "Run a sourced web_research turn with persisted evidence.", "helpWebSearch")}`,
|
|
887
887
|
` ${command("/image-read <path> [question]", "Run read_image on a workspace screenshot/image.", "helpWebSearch")}`,
|
|
@@ -3011,17 +3011,26 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3011
3011
|
}
|
|
3012
3012
|
if (command === "webapp" || command === "web") {
|
|
3013
3013
|
const words = value.split(/\s+/).filter(Boolean);
|
|
3014
|
-
const action = words.find((word) => ["start", "stop", "restart", "reuse"].includes(word.toLowerCase()))?.toLowerCase() || "start";
|
|
3014
|
+
const action = words.find((word) => ["start", "stop", "restart", "reuse", "enable", "disable", "status"].includes(word.toLowerCase()))?.toLowerCase() || "start";
|
|
3015
3015
|
const restart = words.some((word) => word.toLowerCase() === "restart");
|
|
3016
3016
|
const portValue = words.find((word) => /^\d+$/.test(word));
|
|
3017
3017
|
const port = Number(portValue) || Number(process.env.AGINTI_WEB_PORT || process.env.PORT || 3210);
|
|
3018
|
-
printSystemLine(action === "stop" ? "webapp=stopping" : restart ? "webapp=restarting" : "webapp=starting");
|
|
3019
3018
|
const webOptions = {
|
|
3020
3019
|
packageDir,
|
|
3021
3020
|
cwd: state.commandCwd || process.cwd(),
|
|
3022
3021
|
host: process.env.AGINTI_WEB_HOST || process.env.HOST || "127.0.0.1",
|
|
3023
3022
|
preferredPort: port,
|
|
3024
3023
|
};
|
|
3024
|
+
if (action === "enable" || action === "disable" || action === "status") {
|
|
3025
|
+
const preference =
|
|
3026
|
+
action === "status"
|
|
3027
|
+
? await readWebAppPreference()
|
|
3028
|
+
: await writeWebAppPreference({ autoStart: action === "enable" });
|
|
3029
|
+
printSystemLine(`webapp auto-start=${preference.autoStart ? "enabled" : "disabled"}`);
|
|
3030
|
+
if (!preference.autoStart) printSystemLine("webapp disabled for future starts; use /webapp stop to stop a running webapp");
|
|
3031
|
+
return true;
|
|
3032
|
+
}
|
|
3033
|
+
printSystemLine(action === "stop" ? "webapp=stopping" : restart ? "webapp=restarting" : "webapp=starting");
|
|
3025
3034
|
const result = await (action === "stop"
|
|
3026
3035
|
? stopAgintiWebApp(webOptions)
|
|
3027
3036
|
: ensureAgintiWebApp({
|
package/src/platform.js
CHANGED
|
@@ -58,6 +58,7 @@ export function platformSetupHints(info = platformInfo()) {
|
|
|
58
58
|
if (info.isWsl) {
|
|
59
59
|
return [
|
|
60
60
|
"Use the Linux/WSL shell as the primary AgInTiFlow environment; keep projects under the WSL filesystem for best file and Docker performance.",
|
|
61
|
+
"Use Node.js 22+ from nvm/fnm/NodeSource; if an old ~/.npm-global/bin/aginti is earlier on PATH, reinstall AgInTiFlow after switching Node.",
|
|
61
62
|
"Use Docker Desktop with WSL integration or Docker Engine inside WSL for docker-workspace mode.",
|
|
62
63
|
"For LaTeX, reuse WSL latexmk/pdflatex when installed, or use the companion Docker sandbox image.",
|
|
63
64
|
];
|
|
@@ -65,6 +66,7 @@ export function platformSetupHints(info = platformInfo()) {
|
|
|
65
66
|
if (info.isMac) {
|
|
66
67
|
return [
|
|
67
68
|
"Use Node.js 22+ from Homebrew, nvm, fnm, or the official installer.",
|
|
69
|
+
"After changing Node versions, verify `node -v`, `which aginti`, and reinstall with `npm install -g @lazyingart/agintiflow@latest` so the CLI uses the same Node that installed it.",
|
|
68
70
|
"Use Docker Desktop or Colima for docker-workspace mode; the Ubuntu Docker installer is intentionally not used on macOS.",
|
|
69
71
|
"For LaTeX, reuse MacTeX/BasicTeX when latexmk and pdflatex are on PATH; otherwise use the companion Docker sandbox image.",
|
|
70
72
|
"Use Homebrew for optional host tools such as ripgrep, git, python, and latexmk when you choose host mode.",
|
|
@@ -80,6 +82,7 @@ export function platformSetupHints(info = platformInfo()) {
|
|
|
80
82
|
if (info.linuxFamily === "debian") {
|
|
81
83
|
return [
|
|
82
84
|
"Use Node.js 22+ from nvm/fnm, NodeSource, or distro packages.",
|
|
85
|
+
"After changing Node versions, verify `node -v`, `which aginti`, and reinstall with `npm install -g @lazyingart/agintiflow@latest` to avoid stale npm-global shims.",
|
|
83
86
|
"Docker can be installed with scripts/install-docker-ubuntu.sh on Ubuntu/Debian-like hosts.",
|
|
84
87
|
"For LaTeX, reuse host latexmk/pdflatex when available; otherwise use the companion Docker sandbox image.",
|
|
85
88
|
];
|
package/src/project.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { listAgentWrappers } from "./tool-wrappers.js";
|
|
7
7
|
import { getDockerSandboxStatus } from "./docker-sandbox.js";
|
|
8
8
|
import { platformInfo, platformLabel, platformSetupHints } from "./platform.js";
|
|
9
|
+
import { nodeSqliteRecoveryLines, nodeSqliteStatus } from "./sqlite.js";
|
|
9
10
|
import { buildAgintiInstructions, normalizeInstructionTemplate } from "./behavior-contract.js";
|
|
10
11
|
import {
|
|
11
12
|
LEGACY_PROJECT_SESSIONS_DIR_NAME,
|
|
@@ -758,6 +759,7 @@ export async function doctorReport(projectRoot, packageVersion, config) {
|
|
|
758
759
|
const paths = projectPaths(projectRoot);
|
|
759
760
|
const keyStatus = providerKeyStatus(projectRoot);
|
|
760
761
|
const platform = platformInfo();
|
|
762
|
+
const sqliteStatus = nodeSqliteStatus();
|
|
761
763
|
const [sessions, dockerStatus, latestVersion, instructions] = await Promise.all([
|
|
762
764
|
listProjectSessions(projectRoot, 8),
|
|
763
765
|
getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
|
|
@@ -775,6 +777,8 @@ export async function doctorReport(projectRoot, packageVersion, config) {
|
|
|
775
777
|
node: {
|
|
776
778
|
version: process.version,
|
|
777
779
|
ok: Number(process.versions.node.split(".")[0]) >= 22,
|
|
780
|
+
sqlite: sqliteStatus,
|
|
781
|
+
recovery: sqliteStatus.ok ? [] : nodeSqliteRecoveryLines(sqliteStatus),
|
|
778
782
|
},
|
|
779
783
|
platform: {
|
|
780
784
|
...platform,
|
package/src/session-index.js
CHANGED
|
@@ -17,6 +17,7 @@ export function globalSessionPaths(sessionId = "") {
|
|
|
17
17
|
home,
|
|
18
18
|
sessionsDir,
|
|
19
19
|
indexDbPath: path.join(sessionsDir, "index.sqlite"),
|
|
20
|
+
indexJsonPath: path.join(sessionsDir, "index.json"),
|
|
20
21
|
sessionDir: sessionId ? path.join(sessionsDir, sessionId) : "",
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -29,7 +30,8 @@ export function isSafeSessionId(sessionId) {
|
|
|
29
30
|
function ensureIndexDb() {
|
|
30
31
|
const paths = globalSessionPaths();
|
|
31
32
|
fs.mkdirSync(paths.sessionsDir, { recursive: true });
|
|
32
|
-
const DatabaseSync = loadDatabaseSync();
|
|
33
|
+
const DatabaseSync = loadDatabaseSync({ optional: true });
|
|
34
|
+
if (!DatabaseSync) return null;
|
|
33
35
|
const db = new DatabaseSync(paths.indexDbPath);
|
|
34
36
|
db.exec(`
|
|
35
37
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -57,14 +59,86 @@ function ensureIndexDb() {
|
|
|
57
59
|
return db;
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
function emptyIndexState() {
|
|
63
|
+
return {
|
|
64
|
+
sessions: {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readIndexJson() {
|
|
69
|
+
const paths = globalSessionPaths();
|
|
70
|
+
fs.mkdirSync(paths.sessionsDir, { recursive: true });
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(fs.readFileSync(paths.indexJsonPath, "utf8"));
|
|
73
|
+
return {
|
|
74
|
+
...emptyIndexState(),
|
|
75
|
+
...parsed,
|
|
76
|
+
sessions: parsed && typeof parsed.sessions === "object" && parsed.sessions ? parsed.sessions : {},
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return emptyIndexState();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeIndexJson(state) {
|
|
84
|
+
const paths = globalSessionPaths();
|
|
85
|
+
fs.mkdirSync(paths.sessionsDir, { recursive: true });
|
|
86
|
+
const tmpPath = `${paths.indexJsonPath}.${process.pid}.${Date.now()}.tmp`;
|
|
87
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`);
|
|
88
|
+
fs.renameSync(tmpPath, paths.indexJsonPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeIndexRecord(record = {}, sessionId = "") {
|
|
92
|
+
const now = new Date().toISOString();
|
|
93
|
+
const paths = globalSessionPaths(sessionId);
|
|
94
|
+
const createdAt = record.createdAt || record.startedAt || record.created_at || now;
|
|
95
|
+
const updatedAt = record.updatedAt || record.updated_at || createdAt || now;
|
|
96
|
+
return {
|
|
97
|
+
sessionId,
|
|
98
|
+
projectRoot: String(record.projectRoot || record.project_root || ""),
|
|
99
|
+
commandCwd: String(record.commandCwd || record.command_cwd || record.projectRoot || record.project_root || ""),
|
|
100
|
+
projectSessionsDir: String(record.projectSessionsDir || record.project_sessions_dir || ""),
|
|
101
|
+
sessionDir: String(record.sessionDir || record.session_dir || paths.sessionDir),
|
|
102
|
+
provider: String(record.provider || ""),
|
|
103
|
+
model: String(record.model || ""),
|
|
104
|
+
goal: String(record.goal || ""),
|
|
105
|
+
title: String(record.title || ""),
|
|
106
|
+
status: String(record.status || ""),
|
|
107
|
+
createdAt,
|
|
108
|
+
updatedAt,
|
|
109
|
+
endedAt: record.endedAt || record.ended_at || null,
|
|
110
|
+
result: String(record.result || ""),
|
|
111
|
+
error: String(record.error || ""),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
60
115
|
export function upsertSessionIndex(record = {}) {
|
|
61
116
|
const sessionId = String(record.sessionId || record.session_id || "").trim();
|
|
62
117
|
if (!isSafeSessionId(sessionId)) return false;
|
|
63
|
-
const now = new Date().toISOString();
|
|
64
118
|
const db = ensureIndexDb();
|
|
119
|
+
const normalized = normalizeIndexRecord(record, sessionId);
|
|
120
|
+
if (!db) {
|
|
121
|
+
const state = readIndexJson();
|
|
122
|
+
const previous = state.sessions[sessionId] || {};
|
|
123
|
+
state.sessions[sessionId] = {
|
|
124
|
+
...previous,
|
|
125
|
+
...normalized,
|
|
126
|
+
projectRoot: normalized.projectRoot || previous.projectRoot || "",
|
|
127
|
+
commandCwd: normalized.commandCwd || previous.commandCwd || "",
|
|
128
|
+
projectSessionsDir: normalized.projectSessionsDir || previous.projectSessionsDir || "",
|
|
129
|
+
sessionDir: normalized.sessionDir || previous.sessionDir || globalSessionPaths(sessionId).sessionDir,
|
|
130
|
+
provider: normalized.provider || previous.provider || "",
|
|
131
|
+
model: normalized.model || previous.model || "",
|
|
132
|
+
goal: normalized.goal || previous.goal || "",
|
|
133
|
+
title: normalized.title || previous.title || "",
|
|
134
|
+
status: normalized.status || previous.status || "",
|
|
135
|
+
result: normalized.result || previous.result || "",
|
|
136
|
+
error: normalized.error || previous.error || "",
|
|
137
|
+
};
|
|
138
|
+
writeIndexJson(state);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
65
141
|
const paths = globalSessionPaths(sessionId);
|
|
66
|
-
const createdAt = record.createdAt || record.startedAt || record.created_at || now;
|
|
67
|
-
const updatedAt = record.updatedAt || record.updated_at || createdAt || now;
|
|
68
142
|
db.prepare(
|
|
69
143
|
`INSERT INTO sessions (
|
|
70
144
|
session_id, project_root, command_cwd, project_sessions_dir, session_dir,
|
|
@@ -86,20 +160,20 @@ export function upsertSessionIndex(record = {}) {
|
|
|
86
160
|
error = CASE WHEN excluded.error != '' THEN excluded.error ELSE sessions.error END`
|
|
87
161
|
).run(
|
|
88
162
|
sessionId,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
createdAt,
|
|
99
|
-
updatedAt,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
163
|
+
normalized.projectRoot,
|
|
164
|
+
normalized.commandCwd,
|
|
165
|
+
normalized.projectSessionsDir,
|
|
166
|
+
normalized.sessionDir || paths.sessionDir,
|
|
167
|
+
normalized.provider,
|
|
168
|
+
normalized.model,
|
|
169
|
+
normalized.goal,
|
|
170
|
+
normalized.title,
|
|
171
|
+
normalized.status,
|
|
172
|
+
normalized.createdAt,
|
|
173
|
+
normalized.updatedAt,
|
|
174
|
+
normalized.endedAt,
|
|
175
|
+
normalized.result,
|
|
176
|
+
normalized.error
|
|
103
177
|
);
|
|
104
178
|
return true;
|
|
105
179
|
}
|
|
@@ -107,6 +181,14 @@ export function upsertSessionIndex(record = {}) {
|
|
|
107
181
|
export function renameSessionIndex(sessionId, title) {
|
|
108
182
|
if (!isSafeSessionId(sessionId)) return false;
|
|
109
183
|
const db = ensureIndexDb();
|
|
184
|
+
if (!db) {
|
|
185
|
+
const state = readIndexJson();
|
|
186
|
+
if (!state.sessions[sessionId]) return false;
|
|
187
|
+
state.sessions[sessionId].title = String(title || "").trim();
|
|
188
|
+
state.sessions[sessionId].updatedAt = new Date().toISOString();
|
|
189
|
+
writeIndexJson(state);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
110
192
|
const result = db
|
|
111
193
|
.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE session_id = ?")
|
|
112
194
|
.run(String(title || "").trim(), new Date().toISOString(), sessionId);
|
|
@@ -116,6 +198,13 @@ export function renameSessionIndex(sessionId, title) {
|
|
|
116
198
|
export function deleteSessionIndex(sessionId) {
|
|
117
199
|
if (!isSafeSessionId(sessionId)) return false;
|
|
118
200
|
const db = ensureIndexDb();
|
|
201
|
+
if (!db) {
|
|
202
|
+
const state = readIndexJson();
|
|
203
|
+
if (!state.sessions[sessionId]) return false;
|
|
204
|
+
delete state.sessions[sessionId];
|
|
205
|
+
writeIndexJson(state);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
119
208
|
const result = db.prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
|
|
120
209
|
return result.changes > 0;
|
|
121
210
|
}
|
|
@@ -123,6 +212,18 @@ export function deleteSessionIndex(sessionId) {
|
|
|
123
212
|
export function listSessionIndex({ projectRoot = "", commandCwd = "", limit = 100 } = {}) {
|
|
124
213
|
const db = ensureIndexDb();
|
|
125
214
|
const maxRows = Math.min(Math.max(Number(limit) || 100, 1), 1000);
|
|
215
|
+
if (!db) {
|
|
216
|
+
const resolvedProjectRoot = projectRoot ? path.resolve(projectRoot) : "";
|
|
217
|
+
const resolvedCommandCwd = commandCwd ? path.resolve(commandCwd) : "";
|
|
218
|
+
return Object.values(readIndexJson().sessions)
|
|
219
|
+
.filter((session) => {
|
|
220
|
+
if (resolvedProjectRoot && session.projectRoot !== resolvedProjectRoot) return false;
|
|
221
|
+
if (resolvedCommandCwd && session.commandCwd !== resolvedCommandCwd) return false;
|
|
222
|
+
return true;
|
|
223
|
+
})
|
|
224
|
+
.sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")))
|
|
225
|
+
.slice(0, maxRows);
|
|
226
|
+
}
|
|
126
227
|
const columns = `session_id AS sessionId, project_root AS projectRoot, command_cwd AS commandCwd, project_sessions_dir AS projectSessionsDir,
|
|
127
228
|
session_dir AS sessionDir, provider, model, goal, title, status,
|
|
128
229
|
created_at AS createdAt, updated_at AS updatedAt, ended_at AS endedAt, result, error`;
|
package/src/sqlite.js
CHANGED
|
@@ -2,18 +2,126 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
let cachedDatabaseSync = null;
|
|
5
|
+
let cachedUnavailableError = null;
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
|
|
7
|
+
export const NODE_SQLITE_UNAVAILABLE_CODE = "AGINTIFLOW_NODE_SQLITE_UNAVAILABLE";
|
|
8
|
+
|
|
9
|
+
function nodeMajorVersion(version = process.versions.node) {
|
|
10
|
+
return Number(String(version || "").split(".")[0]) || 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function requireNodeSqliteSilently() {
|
|
8
14
|
const originalEmitWarning = process.emitWarning;
|
|
9
15
|
process.emitWarning = function filteredSqliteWarning(warning, ...args) {
|
|
10
16
|
if (String(warning || "").includes("SQLite is an experimental feature")) return;
|
|
11
17
|
return originalEmitWarning.call(process, warning, ...args);
|
|
12
18
|
};
|
|
13
19
|
try {
|
|
14
|
-
|
|
15
|
-
return cachedDatabaseSync;
|
|
20
|
+
return require("node:sqlite");
|
|
16
21
|
} finally {
|
|
17
22
|
process.emitWarning = originalEmitWarning;
|
|
18
23
|
}
|
|
19
24
|
}
|
|
25
|
+
|
|
26
|
+
export function nodeSqliteStatus() {
|
|
27
|
+
const nodeVersion = process.version;
|
|
28
|
+
const nodeMajor = nodeMajorVersion();
|
|
29
|
+
const forcedJson = process.env.AGINTIFLOW_FORCE_JSON_DB === "1" || process.env.AGINTIFLOW_FORCE_SQLITE_UNAVAILABLE === "1";
|
|
30
|
+
if (forcedJson) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
forcedJson: true,
|
|
34
|
+
nodeVersion,
|
|
35
|
+
nodeMajor,
|
|
36
|
+
requiredNodeMajor: 22,
|
|
37
|
+
code: NODE_SQLITE_UNAVAILABLE_CODE,
|
|
38
|
+
message: "AGINTIFLOW_FORCE_JSON_DB is enabled; using JSON fallback storage.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
requireNodeSqliteSilently();
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
forcedJson: false,
|
|
46
|
+
nodeVersion,
|
|
47
|
+
nodeMajor,
|
|
48
|
+
requiredNodeMajor: 22,
|
|
49
|
+
code: "",
|
|
50
|
+
message: "node:sqlite is available.",
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
forcedJson: false,
|
|
56
|
+
nodeVersion,
|
|
57
|
+
nodeMajor,
|
|
58
|
+
requiredNodeMajor: 22,
|
|
59
|
+
code: NODE_SQLITE_UNAVAILABLE_CODE,
|
|
60
|
+
errorCode: error?.code || "",
|
|
61
|
+
message: error?.message || String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function nodeSqliteRecoveryLines(status = nodeSqliteStatus()) {
|
|
67
|
+
const lines = [
|
|
68
|
+
`AgInTiFlow web/session SQLite storage needs Node.js 22+ with node:sqlite; current node is ${status.nodeVersion || process.version}.`,
|
|
69
|
+
"AgInTiFlow will use a project-local JSON fallback when possible, but Node 22+ is still recommended for the full durable session index.",
|
|
70
|
+
"Recommended fix after installing Node 22+: npm install -g @lazyingart/agintiflow@latest",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
lines.push("Windows: prefer WSL2 with Node 22+ and Docker Desktop WSL integration, or install Node 22+ with the official installer/nvm-windows.");
|
|
75
|
+
} else {
|
|
76
|
+
lines.push('nvm quick fix: export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"; nvm install 22; nvm alias default 22; nvm use 22');
|
|
77
|
+
lines.push("If `which aginti` points to ~/.npm-global/bin/aginti after switching Node, remove the stale install or put the nvm bin directory before ~/.npm-global/bin in PATH.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (process.platform === "darwin") {
|
|
81
|
+
lines.push("macOS alternative: brew install node@22, then reinstall AgInTiFlow globally with that node on PATH.");
|
|
82
|
+
} else if (process.platform === "linux") {
|
|
83
|
+
lines.push("Linux alternative: install Node 22+ from NodeSource/fnm/nvm/distro packages, then open a new shell and verify `node -v` and `which aginti`.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatNodeSqliteRecovery(status = nodeSqliteStatus()) {
|
|
90
|
+
return nodeSqliteRecoveryLines(status).join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class AgintiSqliteUnavailableError extends Error {
|
|
94
|
+
constructor(cause, status = nodeSqliteStatus()) {
|
|
95
|
+
super(formatNodeSqliteRecovery(status));
|
|
96
|
+
this.name = "AgintiSqliteUnavailableError";
|
|
97
|
+
this.code = NODE_SQLITE_UNAVAILABLE_CODE;
|
|
98
|
+
this.status = status;
|
|
99
|
+
if (cause) this.cause = cause;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unavailableError(cause = null) {
|
|
104
|
+
cachedUnavailableError = new AgintiSqliteUnavailableError(cause);
|
|
105
|
+
return cachedUnavailableError;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function loadDatabaseSync(options = {}) {
|
|
109
|
+
if (cachedUnavailableError) {
|
|
110
|
+
if (options.optional) return null;
|
|
111
|
+
throw cachedUnavailableError;
|
|
112
|
+
}
|
|
113
|
+
if (cachedDatabaseSync) return cachedDatabaseSync;
|
|
114
|
+
if (process.env.AGINTIFLOW_FORCE_JSON_DB === "1" || process.env.AGINTIFLOW_FORCE_SQLITE_UNAVAILABLE === "1") {
|
|
115
|
+
const error = unavailableError(new Error("Forced JSON database fallback."));
|
|
116
|
+
if (options.optional) return null;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
cachedDatabaseSync = requireNodeSqliteSilently().DatabaseSync;
|
|
121
|
+
return cachedDatabaseSync;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const wrapped = unavailableError(error);
|
|
124
|
+
if (options.optional) return null;
|
|
125
|
+
throw wrapped;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/web-autostart.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
2
3
|
import http from "node:http";
|
|
3
4
|
import net from "node:net";
|
|
4
5
|
import os from "node:os";
|
|
@@ -54,6 +55,32 @@ function resolveRuntimeDir(cwd = "") {
|
|
|
54
55
|
return path.resolve(process.env.AGINTIFLOW_WEB_RUNTIME_DIR || cwd || process.cwd());
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function webPreferencePath(home = "") {
|
|
59
|
+
return path.join(resolveWebHome(home), "webapp.json");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function readWebAppPreference({ home = "" } = {}) {
|
|
63
|
+
const file = webPreferencePath(home);
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(await fs.readFile(file, "utf8"));
|
|
66
|
+
return { autoStart: data.autoStart !== false, path: file };
|
|
67
|
+
} catch {
|
|
68
|
+
return { autoStart: true, path: file };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function writeWebAppPreference({ home = "", autoStart = true } = {}) {
|
|
73
|
+
const file = webPreferencePath(home);
|
|
74
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
75
|
+
await fs.writeFile(file, `${JSON.stringify({ autoStart: Boolean(autoStart), updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
76
|
+
return await readWebAppPreference({ home });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function webAutoStartDisabled(home = "") {
|
|
80
|
+
if (process.env.AGINTI_NO_WEB_AUTO_START === "1" || process.env.AGINTIFLOW_NO_WEB_AUTO_START === "1") return true;
|
|
81
|
+
return (await readWebAppPreference({ home })).autoStart === false;
|
|
82
|
+
}
|
|
83
|
+
|
|
57
84
|
function fetchHealthDetails(host, port, timeoutMs = 450) {
|
|
58
85
|
return new Promise((resolve) => {
|
|
59
86
|
const req = http.get(`${webUrl(host, port)}/health`, { timeout: timeoutMs }, (res) => {
|
|
@@ -274,7 +301,7 @@ export async function ensureAgintiWebApp({
|
|
|
274
301
|
restart = false,
|
|
275
302
|
respectAutoStartDisable = true,
|
|
276
303
|
} = {}) {
|
|
277
|
-
if (respectAutoStartDisable && (
|
|
304
|
+
if (respectAutoStartDisable && (await webAutoStartDisabled(home))) {
|
|
278
305
|
return { ok: false, disabled: true, url: "" };
|
|
279
306
|
}
|
|
280
307
|
|
package/src/web-db.js
CHANGED
|
@@ -9,6 +9,38 @@ import { permissionModeDefaults } from "./permission-modes.js";
|
|
|
9
9
|
|
|
10
10
|
const PREFERENCES_SCHEMA_VERSION = 8;
|
|
11
11
|
|
|
12
|
+
function jsonStatePath(dbPath) {
|
|
13
|
+
return dbPath.replace(/\.sqlite$/i, ".json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function emptyJsonState() {
|
|
17
|
+
return {
|
|
18
|
+
preferences: {},
|
|
19
|
+
sessions: {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readJsonFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
26
|
+
return {
|
|
27
|
+
...emptyJsonState(),
|
|
28
|
+
...parsed,
|
|
29
|
+
preferences: parsed && typeof parsed.preferences === "object" && parsed.preferences ? parsed.preferences : {},
|
|
30
|
+
sessions: parsed && typeof parsed.sessions === "object" && parsed.sessions ? parsed.sessions : {},
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
return emptyJsonState();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeJsonFile(filePath, state) {
|
|
38
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
40
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`);
|
|
41
|
+
fs.renameSync(tmpPath, filePath);
|
|
42
|
+
}
|
|
43
|
+
|
|
12
44
|
function defaultPreferences(baseDir) {
|
|
13
45
|
const presets = getModelPresets();
|
|
14
46
|
const roles = getModelRoleDefaults();
|
|
@@ -63,33 +95,40 @@ export class WebDatabase {
|
|
|
63
95
|
this.paths = projectPaths(this.baseDir);
|
|
64
96
|
this.dbDir = this.paths.sessionsDir;
|
|
65
97
|
this.dbPath = this.paths.sessionDbPath;
|
|
98
|
+
this.jsonPath = jsonStatePath(this.dbPath);
|
|
66
99
|
fs.mkdirSync(this.dbDir, { recursive: true });
|
|
67
|
-
const DatabaseSync = loadDatabaseSync();
|
|
68
|
-
this.
|
|
69
|
-
this.db.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
const DatabaseSync = loadDatabaseSync({ optional: true });
|
|
101
|
+
this.driver = DatabaseSync ? "sqlite" : "json";
|
|
102
|
+
this.db = DatabaseSync ? new DatabaseSync(this.dbPath) : null;
|
|
103
|
+
if (this.db) {
|
|
104
|
+
this.db.exec(`
|
|
105
|
+
CREATE TABLE IF NOT EXISTS preferences (
|
|
106
|
+
key TEXT PRIMARY KEY,
|
|
107
|
+
value TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL
|
|
109
|
+
);
|
|
75
110
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
112
|
+
session_id TEXT PRIMARY KEY,
|
|
113
|
+
provider TEXT NOT NULL,
|
|
114
|
+
model TEXT NOT NULL,
|
|
115
|
+
goal TEXT NOT NULL,
|
|
116
|
+
status TEXT NOT NULL,
|
|
117
|
+
started_at TEXT NOT NULL,
|
|
118
|
+
updated_at TEXT NOT NULL,
|
|
119
|
+
ended_at TEXT,
|
|
120
|
+
result TEXT,
|
|
121
|
+
error TEXT
|
|
122
|
+
);
|
|
123
|
+
`);
|
|
124
|
+
} else if (!fs.existsSync(this.jsonPath)) {
|
|
125
|
+
writeJsonFile(this.jsonPath, emptyJsonState());
|
|
126
|
+
}
|
|
89
127
|
this.migrate();
|
|
90
128
|
}
|
|
91
129
|
|
|
92
130
|
migrate() {
|
|
131
|
+
if (!this.db) return;
|
|
93
132
|
const sessionColumns = this.db.prepare("PRAGMA table_info(sessions)").all();
|
|
94
133
|
if (!sessionColumns.some((column) => column.name === "title")) {
|
|
95
134
|
this.db.exec("ALTER TABLE sessions ADD COLUMN title TEXT NOT NULL DEFAULT ''");
|
|
@@ -108,12 +147,29 @@ export class WebDatabase {
|
|
|
108
147
|
}
|
|
109
148
|
}
|
|
110
149
|
|
|
150
|
+
readJsonState() {
|
|
151
|
+
return readJsonFile(this.jsonPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
writeJsonState(state) {
|
|
155
|
+
writeJsonFile(this.jsonPath, state);
|
|
156
|
+
}
|
|
157
|
+
|
|
111
158
|
getPreferences() {
|
|
159
|
+
if (!this.db) {
|
|
160
|
+
const raw = this.readJsonState().preferences.ui;
|
|
161
|
+
if (!raw) return defaultPreferences(this.baseDir);
|
|
162
|
+
return this.normalizePreferences(raw);
|
|
163
|
+
}
|
|
112
164
|
const row = this.db.prepare("SELECT value FROM preferences WHERE key = ?").get("ui");
|
|
113
165
|
if (!row) return defaultPreferences(this.baseDir);
|
|
114
166
|
|
|
167
|
+
return this.normalizePreferences(row.value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
normalizePreferences(value) {
|
|
115
171
|
try {
|
|
116
|
-
const parsed = JSON.parse(
|
|
172
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
117
173
|
const preferences = {
|
|
118
174
|
...defaultPreferences(this.baseDir),
|
|
119
175
|
...parsed,
|
|
@@ -177,6 +233,13 @@ export class WebDatabase {
|
|
|
177
233
|
savePreferences(preferences) {
|
|
178
234
|
const value = JSON.stringify(preferences);
|
|
179
235
|
const updatedAt = new Date().toISOString();
|
|
236
|
+
if (!this.db) {
|
|
237
|
+
const state = this.readJsonState();
|
|
238
|
+
state.preferences.ui = value;
|
|
239
|
+
state.preferences.uiUpdatedAt = updatedAt;
|
|
240
|
+
this.writeJsonState(state);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
180
243
|
this.db
|
|
181
244
|
.prepare(
|
|
182
245
|
`INSERT INTO preferences (key, value, updated_at)
|
|
@@ -192,6 +255,35 @@ export class WebDatabase {
|
|
|
192
255
|
const commandCwd = session.commandCwd || projectRoot;
|
|
193
256
|
const projectSessionsDir = session.projectSessionsDir || this.paths.sessionsDir;
|
|
194
257
|
const sessionDir = session.sessionDir || path.join(this.paths.globalSessionsDir, session.sessionId);
|
|
258
|
+
const record = {
|
|
259
|
+
sessionId: session.sessionId,
|
|
260
|
+
projectRoot,
|
|
261
|
+
commandCwd,
|
|
262
|
+
projectSessionsDir,
|
|
263
|
+
sessionDir,
|
|
264
|
+
provider: session.provider,
|
|
265
|
+
model: session.model,
|
|
266
|
+
goal: session.goal,
|
|
267
|
+
title: session.title || "",
|
|
268
|
+
status: session.status,
|
|
269
|
+
startedAt: session.startedAt,
|
|
270
|
+
updatedAt,
|
|
271
|
+
endedAt: session.endedAt || null,
|
|
272
|
+
result: session.result || "",
|
|
273
|
+
error: session.error || "",
|
|
274
|
+
};
|
|
275
|
+
if (!this.db) {
|
|
276
|
+
const state = this.readJsonState();
|
|
277
|
+
const previous = state.sessions[session.sessionId] || {};
|
|
278
|
+
state.sessions[session.sessionId] = {
|
|
279
|
+
...previous,
|
|
280
|
+
...record,
|
|
281
|
+
title: record.title || previous.title || "",
|
|
282
|
+
};
|
|
283
|
+
this.writeJsonState(state);
|
|
284
|
+
this.syncSessionIndex(record);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
195
287
|
this.db
|
|
196
288
|
.prepare(
|
|
197
289
|
`INSERT INTO sessions (
|
|
@@ -230,15 +322,14 @@ export class WebDatabase {
|
|
|
230
322
|
session.result || "",
|
|
231
323
|
session.error || ""
|
|
232
324
|
);
|
|
325
|
+
this.syncSessionIndex(record);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
syncSessionIndex(record) {
|
|
233
329
|
try {
|
|
234
330
|
upsertSessionIndex({
|
|
235
|
-
...
|
|
236
|
-
|
|
237
|
-
commandCwd,
|
|
238
|
-
projectSessionsDir,
|
|
239
|
-
sessionDir,
|
|
240
|
-
createdAt: session.startedAt,
|
|
241
|
-
updatedAt,
|
|
331
|
+
...record,
|
|
332
|
+
createdAt: record.startedAt,
|
|
242
333
|
});
|
|
243
334
|
} catch {
|
|
244
335
|
// The project-local database remains the fallback if the global index cannot be updated.
|
|
@@ -246,6 +337,9 @@ export class WebDatabase {
|
|
|
246
337
|
}
|
|
247
338
|
|
|
248
339
|
getSession(sessionId) {
|
|
340
|
+
if (!this.db) {
|
|
341
|
+
return this.readJsonState().sessions[sessionId] || null;
|
|
342
|
+
}
|
|
249
343
|
return (
|
|
250
344
|
this.db
|
|
251
345
|
.prepare(
|
|
@@ -273,6 +367,12 @@ export class WebDatabase {
|
|
|
273
367
|
}
|
|
274
368
|
|
|
275
369
|
listSessions(limit = 20) {
|
|
370
|
+
if (!this.db) {
|
|
371
|
+
const maxRows = Math.min(Math.max(Number(limit) || 20, 1), 1000);
|
|
372
|
+
return Object.values(this.readJsonState().sessions)
|
|
373
|
+
.sort((left, right) => String(right.updatedAt || "").localeCompare(String(left.updatedAt || "")))
|
|
374
|
+
.slice(0, maxRows);
|
|
375
|
+
}
|
|
276
376
|
return this.db
|
|
277
377
|
.prepare(
|
|
278
378
|
`SELECT
|
|
@@ -300,6 +400,19 @@ export class WebDatabase {
|
|
|
300
400
|
|
|
301
401
|
renameSession(sessionId, title) {
|
|
302
402
|
const updatedAt = new Date().toISOString();
|
|
403
|
+
if (!this.db) {
|
|
404
|
+
const state = this.readJsonState();
|
|
405
|
+
if (!state.sessions[sessionId]) return false;
|
|
406
|
+
state.sessions[sessionId].title = title;
|
|
407
|
+
state.sessions[sessionId].updatedAt = updatedAt;
|
|
408
|
+
this.writeJsonState(state);
|
|
409
|
+
try {
|
|
410
|
+
renameSessionIndex(sessionId, title);
|
|
411
|
+
} catch {
|
|
412
|
+
// Keep the project-local JSON store as the source of truth.
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
303
416
|
const result = this.db
|
|
304
417
|
.prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE session_id = ?")
|
|
305
418
|
.run(title, updatedAt, sessionId);
|
|
@@ -308,6 +421,18 @@ export class WebDatabase {
|
|
|
308
421
|
}
|
|
309
422
|
|
|
310
423
|
deleteSession(sessionId) {
|
|
424
|
+
if (!this.db) {
|
|
425
|
+
const state = this.readJsonState();
|
|
426
|
+
if (!state.sessions[sessionId]) return false;
|
|
427
|
+
delete state.sessions[sessionId];
|
|
428
|
+
this.writeJsonState(state);
|
|
429
|
+
try {
|
|
430
|
+
deleteSessionIndex(sessionId);
|
|
431
|
+
} catch {
|
|
432
|
+
// Keep the project-local JSON store as the source of truth.
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
311
436
|
const result = this.db.prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
|
|
312
437
|
if (result.changes > 0) deleteSessionIndex(sessionId);
|
|
313
438
|
return result.changes > 0;
|