@lazyingart/agintiflow 0.20.131 → 0.20.133

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.131",
3
+ "version": "0.20.133",
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");
package/src/cli.js CHANGED
@@ -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() {
@@ -91,6 +91,12 @@ export function formatDockerSetupText(summary = {}) {
91
91
  if (!summary.dockerAvailable) {
92
92
  lines.push("Docker is not available.");
93
93
  lines.push(`Install path: ${summary.install?.command || "Install Docker, then run: aginti docker setup"}`);
94
+ if (summary.install?.supported) {
95
+ lines.push("Install command: aginti docker install-host --yes");
96
+ lines.push("After install, open a fresh shell or run newgrp docker, then run: aginti docker setup");
97
+ } else {
98
+ lines.push("After installing Docker for this platform, run: aginti docker setup");
99
+ }
94
100
  } else if (!summary.imageReady || summary.preflightOk === false) {
95
101
  lines.push("Next: aginti docker setup");
96
102
  } else {
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,
@@ -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
- String(record.projectRoot || record.project_root || ""),
90
- String(record.commandCwd || record.command_cwd || record.projectRoot || record.project_root || ""),
91
- String(record.projectSessionsDir || record.project_sessions_dir || ""),
92
- String(record.sessionDir || record.session_dir || paths.sessionDir),
93
- String(record.provider || ""),
94
- String(record.model || ""),
95
- String(record.goal || ""),
96
- String(record.title || ""),
97
- String(record.status || ""),
98
- createdAt,
99
- updatedAt,
100
- record.endedAt || record.ended_at || null,
101
- String(record.result || ""),
102
- String(record.error || "")
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 function loadDatabaseSync() {
7
- if (cachedDatabaseSync) return cachedDatabaseSync;
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
- cachedDatabaseSync = require("node:sqlite").DatabaseSync;
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-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.db = new DatabaseSync(this.dbPath);
69
- this.db.exec(`
70
- CREATE TABLE IF NOT EXISTS preferences (
71
- key TEXT PRIMARY KEY,
72
- value TEXT NOT NULL,
73
- updated_at TEXT NOT NULL
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
- CREATE TABLE IF NOT EXISTS sessions (
77
- session_id TEXT PRIMARY KEY,
78
- provider TEXT NOT NULL,
79
- model TEXT NOT NULL,
80
- goal TEXT NOT NULL,
81
- status TEXT NOT NULL,
82
- started_at TEXT NOT NULL,
83
- updated_at TEXT NOT NULL,
84
- ended_at TEXT,
85
- result TEXT,
86
- error TEXT
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(row.value);
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
- ...session,
236
- projectRoot,
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;
package/web.js CHANGED
@@ -1397,6 +1397,7 @@ app.get("/health", (_req, res) => {
1397
1397
  sessionsDir,
1398
1398
  projectSessionsDir,
1399
1399
  packageDir,
1400
+ storageDriver: db.driver || "sqlite",
1400
1401
  });
1401
1402
  });
1402
1403