@lazyingart/agintiflow 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -3,10 +3,21 @@ import { loadConfig } from "./config.js";
3
3
  import { listAgentWrappers } from "./tool-wrappers.js";
4
4
  import { getModelPresets } from "./model-routing.js";
5
5
  import { getDockerSandboxStatus, runDockerPreflight } from "./docker-sandbox.js";
6
+ import {
7
+ doctorReport,
8
+ initProject,
9
+ listProjectSessions,
10
+ providerKeyStatus,
11
+ setProviderKey,
12
+ showProjectSession,
13
+ } from "./project.js";
14
+ import { listTaskProfiles } from "./task-profiles.js";
15
+ import fs from "node:fs/promises";
6
16
  import path from "node:path";
7
17
  import { fileURLToPath } from "node:url";
8
18
 
9
19
  const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const packageJson = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf8"));
10
21
 
11
22
  function readOption(argv, index) {
12
23
  const value = argv[index + 1];
@@ -30,6 +41,7 @@ export function parseArgs(argv) {
30
41
  allowFileTools: undefined,
31
42
  allowWrapperTools: undefined,
32
43
  preferredWrapper: "",
44
+ taskProfile: "",
33
45
  useDockerSandbox: undefined,
34
46
  headless: undefined,
35
47
  maxSteps: undefined,
@@ -40,6 +52,7 @@ export function parseArgs(argv) {
40
52
  web: false,
41
53
  port: "",
42
54
  host: "",
55
+ listProfiles: false,
43
56
  };
44
57
 
45
58
  const parts = [];
@@ -135,6 +148,11 @@ export function parseArgs(argv) {
135
148
  i += 1;
136
149
  continue;
137
150
  }
151
+ if (arg === "--profile" || arg === "--task-profile") {
152
+ result.taskProfile = readOption(argv, i);
153
+ i += 1;
154
+ continue;
155
+ }
138
156
  if (arg === "--docker-sandbox") {
139
157
  result.useDockerSandbox = true;
140
158
  result.sandboxMode = result.sandboxMode || "docker-readonly";
@@ -152,6 +170,10 @@ export function parseArgs(argv) {
152
170
  result.listWrappers = true;
153
171
  continue;
154
172
  }
173
+ if (arg === "--list-profiles") {
174
+ result.listProfiles = true;
175
+ continue;
176
+ }
155
177
  if (arg === "--sandbox-status") {
156
178
  result.sandboxStatus = true;
157
179
  continue;
@@ -183,7 +205,159 @@ function printWrappers() {
183
205
  }
184
206
  }
185
207
 
208
+ function printProfiles() {
209
+ for (const profile of listTaskProfiles()) {
210
+ console.log(`${profile.id}: ${profile.label} - ${profile.prompt}`);
211
+ }
212
+ }
213
+
214
+ function printInitResult(result) {
215
+ console.log(`AgInTiFlow project initialized: ${result.projectRoot}`);
216
+ console.log(`control=${result.controlDir}`);
217
+ console.log(`sessions=${result.sessionsDir}`);
218
+ console.log(`created=${result.created.length} updated=${result.updated.length} skipped=${result.skipped.length}`);
219
+ }
220
+
221
+ function printDoctorReport(report) {
222
+ console.log(`AgInTiFlow ${report.package.version} (npm latest: ${report.package.npmLatest})`);
223
+ console.log(`node=${report.node.version} ok=${report.node.ok}`);
224
+ console.log(`project=${report.project.root}`);
225
+ console.log(`sessions=${report.project.sessionsDir}`);
226
+ console.log(`sessionDb=${report.project.sessionDbPath}`);
227
+ console.log(
228
+ `keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
229
+ report.keys.openai ? "available" : "missing"
230
+ } mock=available localEnv=${report.project.localEnvPresent}`
231
+ );
232
+ console.log(
233
+ `sandbox=${report.sandbox?.sandboxMode || "unknown"} docker=${
234
+ report.sandbox?.dockerAvailable ? "available" : "missing"
235
+ } imageReady=${Boolean(report.sandbox?.imageReady)}`
236
+ );
237
+ console.log(
238
+ `wrappers=${report.wrappers.map((wrapper) => `${wrapper.name}:${wrapper.available ? "ok" : "missing"}`).join(" ")}`
239
+ );
240
+ console.log(`sessions=${report.sessions.length}`);
241
+ }
242
+
243
+ async function readStdin() {
244
+ let input = "";
245
+ process.stdin.setEncoding("utf8");
246
+ for await (const chunk of process.stdin) input += chunk;
247
+ return input.trim();
248
+ }
249
+
250
+ async function handleKeyCommand(argv) {
251
+ const [verb = "status", provider = ""] = argv;
252
+ if (verb === "status") {
253
+ const status = providerKeyStatus(process.cwd());
254
+ console.log(
255
+ `keys: deepseek=${status.deepseek ? "available" : "missing"} openai=${
256
+ status.openai ? "available" : "missing"
257
+ } mock=available localEnv=${status.localEnv}`
258
+ );
259
+ console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY");
260
+ return;
261
+ }
262
+
263
+ if (verb === "set") {
264
+ const target = provider || "deepseek";
265
+ if (!argv.includes("--stdin")) {
266
+ console.error(`Usage: aginti keys set ${target} --stdin`);
267
+ process.exit(1);
268
+ }
269
+ const key = await readStdin();
270
+ const result = await setProviderKey(process.cwd(), target, key);
271
+ console.log(`saved ${result.provider} key to project-local ignored env (${result.keyName})`);
272
+ return;
273
+ }
274
+
275
+ console.error("Usage: aginti keys status OR aginti keys set deepseek --stdin");
276
+ process.exit(1);
277
+ }
278
+
279
+ async function handleSessionsCommand(argv) {
280
+ const [verb = "list", sessionId = ""] = argv;
281
+ if (verb === "list") {
282
+ const sessions = await listProjectSessions(process.cwd(), 80);
283
+ if (sessions.length === 0) {
284
+ console.log("No project-local sessions found.");
285
+ return;
286
+ }
287
+ for (const session of sessions) {
288
+ const goal = session.goal ? ` ${session.goal.slice(0, 90)}` : "";
289
+ console.log(`${session.sessionId} ${session.provider}/${session.model} ${session.updatedAt}${goal}`);
290
+ }
291
+ return;
292
+ }
293
+
294
+ if (verb === "show") {
295
+ if (!sessionId) {
296
+ console.error("Usage: aginti sessions show <session-id>");
297
+ process.exit(1);
298
+ }
299
+ const session = await showProjectSession(process.cwd(), sessionId);
300
+ console.log(JSON.stringify(session, null, 2));
301
+ return;
302
+ }
303
+
304
+ console.error("Usage: aginti sessions list OR aginti sessions show <session-id>");
305
+ process.exit(1);
306
+ }
307
+
186
308
  export async function main(argv = process.argv.slice(2)) {
309
+ if (argv[0] === "init") {
310
+ printInitResult(await initProject(process.cwd()));
311
+ return;
312
+ }
313
+
314
+ if (argv[0] === "doctor") {
315
+ const config = loadConfig({ goal: "doctor" }, { packageDir, baseDir: process.cwd() });
316
+ const report = await doctorReport(process.cwd(), packageJson.version, config);
317
+ if (argv.includes("--json")) console.log(JSON.stringify(report, null, 2));
318
+ else printDoctorReport(report);
319
+ return;
320
+ }
321
+
322
+ if (argv[0] === "keys/status") {
323
+ await handleKeyCommand(["status"]);
324
+ return;
325
+ }
326
+
327
+ if (argv[0] === "keys") {
328
+ await handleKeyCommand(argv.slice(1));
329
+ return;
330
+ }
331
+
332
+ if (argv[0] === "login") {
333
+ const provider = argv[1] || "deepseek";
334
+ if (!argv.includes("--stdin") && process.stdin.isTTY) {
335
+ console.error(`Usage: printf '%s' '<key>' | aginti login ${provider} --stdin`);
336
+ process.exit(1);
337
+ }
338
+ const key = await readStdin();
339
+ const result = await setProviderKey(process.cwd(), provider, key);
340
+ console.log(`saved ${result.provider} key to project-local ignored env (${result.keyName})`);
341
+ return;
342
+ }
343
+
344
+ if (argv[0] === "sessions") {
345
+ await handleSessionsCommand(argv.slice(1));
346
+ return;
347
+ }
348
+
349
+ if (argv[0] === "resume") {
350
+ const sessionId = argv[1] || "";
351
+ const prompt = argv.slice(2).join(" ").trim();
352
+ if (!sessionId || !prompt) {
353
+ console.error('Usage: aginti resume <session-id> "new prompt"');
354
+ process.exit(1);
355
+ }
356
+ const config = loadConfig({ ...parseArgs([prompt]), resume: sessionId, goal: prompt }, { packageDir });
357
+ await runAgent(config);
358
+ return;
359
+ }
360
+
187
361
  const args = parseArgs(argv);
188
362
 
189
363
  if (args.web) {
@@ -204,6 +378,11 @@ export async function main(argv = process.argv.slice(2)) {
204
378
  return;
205
379
  }
206
380
 
381
+ if (args.listProfiles) {
382
+ printProfiles();
383
+ return;
384
+ }
385
+
207
386
  if (args.sandboxStatus || args.sandboxPreflight) {
208
387
  const config = loadConfig({ ...args, goal: args.goal || "sandbox preflight" }, { packageDir });
209
388
  const result = args.sandboxPreflight
package/src/config.js CHANGED
@@ -3,6 +3,8 @@ import crypto from "node:crypto";
3
3
  import { getProviderDefaults, normalizeRoutingMode, selectModelRoute } from "./model-routing.js";
4
4
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
5
5
  import { normalizeWrapperName } from "./tool-wrappers.js";
6
+ import { loadProjectEnv, resolveProjectRoot } from "./project.js";
7
+ import { normalizeTaskProfile } from "./task-profiles.js";
6
8
 
7
9
  function parseBoolean(value, fallback) {
8
10
  if (value === undefined) return fallback;
@@ -23,6 +25,8 @@ function parseList(value) {
23
25
  }
24
26
 
25
27
  export function resolveRuntimeConfig(args, overrides = {}) {
28
+ const baseDir = resolveProjectRoot(overrides.baseDir || process.cwd());
29
+ loadProjectEnv(baseDir);
26
30
  const requestedProvider =
27
31
  overrides.provider ||
28
32
  args.provider ||
@@ -37,7 +41,6 @@ export function resolveRuntimeConfig(args, overrides = {}) {
37
41
  });
38
42
 
39
43
  const defaults = getProviderDefaults(route.provider);
40
- const baseDir = path.resolve(overrides.baseDir || process.cwd());
41
44
  const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
42
45
  const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, false);
43
46
  const requestedSandboxMode =
@@ -53,6 +56,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
53
56
  resume: args.resume || "",
54
57
  sessionId: overrides.sessionId || args.sessionId || process.env.SESSION_ID || `web-agent-${crypto.randomUUID()}`,
55
58
  routingMode,
59
+ taskProfile: normalizeTaskProfile(overrides.taskProfile || args.taskProfile || process.env.AGINTI_TASK_PROFILE || "auto"),
56
60
  routeReason: route.reason,
57
61
  routeComplexityScore: route.complexityScore,
58
62
  requestedProvider,
@@ -84,7 +88,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
84
88
  ),
85
89
  useDockerSandbox: sandboxMode !== "host" || dockerRequested,
86
90
  dockerSandboxImage: overrides.dockerSandboxImage || process.env.DOCKER_SANDBOX_IMAGE || "agintiflow-sandbox:latest",
87
- commandCwd: path.resolve(overrides.commandCwd || args.commandCwd || process.env.COMMAND_CWD || process.cwd()),
91
+ commandCwd: path.resolve(overrides.commandCwd || args.commandCwd || process.env.COMMAND_CWD || baseDir),
88
92
  sessionsDir: path.resolve(baseDir, ".sessions"),
89
93
  onLog: overrides.onLog,
90
94
  onEvent: overrides.onEvent,
@@ -1,5 +1,6 @@
1
1
  import OpenAI from "openai";
2
2
  import { normalizeWrapperName, wrapperStatusText } from "./tool-wrappers.js";
3
+ import { getTaskProfile } from "./task-profiles.js";
3
4
 
4
5
  export function createClient(config) {
5
6
  if (config.provider === "mock") {
@@ -27,14 +28,19 @@ function mockToolCall(name, args = {}) {
27
28
  }
28
29
 
29
30
  function latestToolPayload(messages) {
30
- const toolMessage = [...messages].reverse().find((message) => message.role === "tool" && message.content);
31
- if (!toolMessage) return null;
32
-
33
- try {
34
- return JSON.parse(toolMessage.content);
35
- } catch {
36
- return null;
31
+ for (const message of [...messages].reverse()) {
32
+ if (message.role === "user" && /^Continue with this new request:|^Goal:/i.test(String(message.content || ""))) {
33
+ return null;
34
+ }
35
+ if (message.role !== "tool" || !message.content) continue;
36
+ try {
37
+ const payload = JSON.parse(message.content);
38
+ return payload?.done ? null : payload;
39
+ } catch {
40
+ return null;
41
+ }
37
42
  }
43
+ return null;
38
44
  }
39
45
 
40
46
  function prepareMessages(config, messages) {
@@ -57,6 +63,10 @@ function mockPathForGoal(goal = "") {
57
63
  const text = String(goal);
58
64
  const explicit = text.match(/(?:file|path):\s*`?([A-Za-z0-9_./-]+)`?/i)?.[1];
59
65
  if (explicit) return explicit;
66
+ const createPath = text.match(
67
+ /\b(?:create|write|make|save|generate)\s+(?:a\s+|an\s+|the\s+)?(?:file\s+)?`?((?:\.\/)?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+|[A-Za-z0-9_.-]+\.(?:md|txt|js|ts|json|py|tex|html|css|svg|csv|yml|yaml))`?/i
68
+ )?.[1];
69
+ if (createPath) return createPath.replace(/^[`'"]+|[`'",.]+$/g, "");
60
70
  if (/\.env/i.test(text)) return ".env";
61
71
  if (/outside|escape/i.test(text)) return "../outside-workspace.txt";
62
72
  if (/patch/i.test(text)) return "patch-target.txt";
@@ -111,6 +121,7 @@ function mockChatResponse(content, toolCalls = []) {
111
121
  }
112
122
 
113
123
  export async function createPlan(client, config, state) {
124
+ const taskProfile = getTaskProfile(config.taskProfile);
114
125
  if (client.mock) {
115
126
  return [
116
127
  "1. Inspect the request and prefer the local shell when available.",
@@ -143,6 +154,7 @@ export async function createPlan(client, config, state) {
143
154
  config.allowWrapperTools
144
155
  ? `Agent wrappers are enabled. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Status: ${wrapperStatusText()}.`
145
156
  : "",
157
+ `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
146
158
  "A canvas/artifacts tunnel is available through send_to_canvas. Use it when an output should be highlighted visually, such as screenshots, image files, important markdown, diffs, or generated artifact paths. It is optional for ordinary text answers.",
147
159
  "When the user asks to draw, plot, graph, chart, diagram, create a figure, or visualize something, include a canvas artifact even if the user does not mention canvas. Prefer a small SVG file or concise markdown figure when file tools are available.",
148
160
  "When the user asks for LaTeX, TeX, a paper, manuscript, report, or PDF, plan to create the needed source/assets, compile with the available allowlisted TeX toolchain, and publish the PDF through the canvas tunnel. For subfolder documents, keep outputs beside the source. For generated figures, use pdflatex-compatible formats such as PDF or PNG.",
@@ -445,6 +457,15 @@ export async function requestNextStep(client, config, messages) {
445
457
  if (client.mock) {
446
458
  const toolPayload = latestToolPayload(messages);
447
459
  if (toolPayload) {
460
+ if (toolPayload.ok === false && !toolPayload.blocked) {
461
+ return mockChatResponse("Mock mode detected a tool failure and will stop instead of masking it.", [
462
+ mockToolCall("finish", {
463
+ result: `Mock run failed because ${toolPayload.toolName || "a tool"} failed: ${
464
+ toolPayload.error || toolPayload.reason || "unknown error"
465
+ }`,
466
+ }),
467
+ ]);
468
+ }
448
469
  const output = [
449
470
  toolPayload.stdout,
450
471
  toolPayload.stderr,
package/src/project.js ADDED
@@ -0,0 +1,332 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { listAgentWrappers } from "./tool-wrappers.js";
7
+ import { getDockerSandboxStatus } from "./docker-sandbox.js";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const LOCAL_ENV_KEYS = new Set([
11
+ "DEEPSEEK_API_KEY",
12
+ "OPENAI_API_KEY",
13
+ "LLM_API_KEY",
14
+ "LLM_BASE_URL",
15
+ "DEEPSEEK_FAST_MODEL",
16
+ "DEEPSEEK_PRO_MODEL",
17
+ "OPENAI_DEFAULT_MODEL",
18
+ ]);
19
+
20
+ export function resolveProjectRoot(input = process.cwd()) {
21
+ return path.resolve(input || process.cwd());
22
+ }
23
+
24
+ export function projectPaths(projectRoot = process.cwd()) {
25
+ const root = resolveProjectRoot(projectRoot);
26
+ return {
27
+ root,
28
+ controlDir: path.join(root, ".aginti"),
29
+ envPath: path.join(root, ".aginti", ".env"),
30
+ envExamplePath: path.join(root, ".aginti", ".env.example"),
31
+ controlReadmePath: path.join(root, ".aginti", "README.md"),
32
+ notesDir: path.join(root, "notes"),
33
+ notesReadmePath: path.join(root, "notes", "README.md"),
34
+ sessionsDir: path.join(root, ".sessions"),
35
+ sessionDbPath: path.join(root, ".sessions", "web-state.sqlite"),
36
+ gitignorePath: path.join(root, ".gitignore"),
37
+ };
38
+ }
39
+
40
+ export function parseEnvText(text = "") {
41
+ const values = {};
42
+ for (const rawLine of String(text).split(/\r?\n/)) {
43
+ const line = rawLine.trim();
44
+ if (!line || line.startsWith("#") || !line.includes("=")) continue;
45
+ const index = line.indexOf("=");
46
+ const key = line.slice(0, index).trim();
47
+ let value = line.slice(index + 1).trim();
48
+ if (!LOCAL_ENV_KEYS.has(key)) continue;
49
+ if (
50
+ (value.startsWith('"') && value.endsWith('"')) ||
51
+ (value.startsWith("'") && value.endsWith("'"))
52
+ ) {
53
+ value = value.slice(1, -1);
54
+ }
55
+ values[key] = value;
56
+ }
57
+ return values;
58
+ }
59
+
60
+ export function loadProjectEnv(projectRoot = process.cwd(), { override = false } = {}) {
61
+ const paths = projectPaths(projectRoot);
62
+ let loaded = false;
63
+ try {
64
+ const parsed = parseEnvText(fs.readFileSync(paths.envPath, "utf8"));
65
+ for (const [key, value] of Object.entries(parsed)) {
66
+ if (override || !process.env[key]) process.env[key] = value;
67
+ }
68
+ loaded = true;
69
+ } catch {
70
+ loaded = false;
71
+ }
72
+ return {
73
+ loaded,
74
+ path: paths.envPath,
75
+ };
76
+ }
77
+
78
+ async function ensureLine(filePath, lines) {
79
+ const desired = Array.isArray(lines) ? lines : [lines];
80
+ let current = "";
81
+ try {
82
+ current = await fsp.readFile(filePath, "utf8");
83
+ } catch {
84
+ current = "";
85
+ }
86
+
87
+ const existing = new Set(current.split(/\r?\n/).map((line) => line.trim()));
88
+ const missing = desired.filter((line) => !existing.has(line));
89
+ if (missing.length === 0) return { changed: false, path: filePath };
90
+
91
+ const prefix = current && !current.endsWith("\n") ? "\n" : "";
92
+ await fsp.writeFile(filePath, `${current}${prefix}${missing.join("\n")}\n`, "utf8");
93
+ return { changed: true, path: filePath, added: missing };
94
+ }
95
+
96
+ export async function initProject(projectRoot = process.cwd()) {
97
+ const paths = projectPaths(projectRoot);
98
+ const created = [];
99
+ const updated = [];
100
+ const skipped = [];
101
+
102
+ async function ensureDir(dirPath) {
103
+ const existed = await fsp.stat(dirPath).then(() => true).catch(() => false);
104
+ await fsp.mkdir(dirPath, { recursive: true });
105
+ (existed ? skipped : created).push(dirPath);
106
+ }
107
+
108
+ async function ensureFile(filePath, content, mode) {
109
+ const existed = await fsp.stat(filePath).then(() => true).catch(() => false);
110
+ if (!existed) {
111
+ await fsp.writeFile(filePath, content, mode ? { mode } : "utf8");
112
+ created.push(filePath);
113
+ } else {
114
+ skipped.push(filePath);
115
+ }
116
+ }
117
+
118
+ await ensureDir(paths.controlDir);
119
+ await ensureDir(paths.notesDir);
120
+ await ensureDir(paths.sessionsDir);
121
+ await ensureFile(
122
+ paths.controlReadmePath,
123
+ [
124
+ "# AgInTi Project Control",
125
+ "",
126
+ "This folder stores project-local AgInTiFlow configuration.",
127
+ "",
128
+ "- `.env` is ignored and can hold local provider keys.",
129
+ "- `.env.example` documents accepted variable names.",
130
+ "- `.sessions/` at the project root stores CLI and web run history.",
131
+ "",
132
+ ].join("\n")
133
+ );
134
+ await ensureFile(
135
+ paths.envExamplePath,
136
+ [
137
+ "# Copy values into .aginti/.env. Never commit real secrets.",
138
+ "DEEPSEEK_API_KEY=",
139
+ "OPENAI_API_KEY=",
140
+ "DEEPSEEK_FAST_MODEL=deepseek-v4-flash",
141
+ "DEEPSEEK_PRO_MODEL=deepseek-v4-pro",
142
+ "",
143
+ ].join("\n")
144
+ );
145
+ await ensureFile(
146
+ paths.notesReadmePath,
147
+ [
148
+ "# Notes",
149
+ "",
150
+ "Use this folder for agent-generated notes, drafts, and smoke-test files.",
151
+ "",
152
+ ].join("\n")
153
+ );
154
+
155
+ const gitignore = await ensureLine(paths.gitignorePath, [
156
+ ".aginti/.env",
157
+ ".aginti/.env.*",
158
+ "!.aginti/.env.example",
159
+ ".sessions/",
160
+ ]);
161
+ if (gitignore.changed) updated.push(paths.gitignorePath);
162
+ else skipped.push(paths.gitignorePath);
163
+
164
+ return {
165
+ ok: true,
166
+ projectRoot: paths.root,
167
+ controlDir: paths.controlDir,
168
+ sessionsDir: paths.sessionsDir,
169
+ created,
170
+ updated,
171
+ skipped,
172
+ };
173
+ }
174
+
175
+ export function providerKeyStatus(projectRoot = process.cwd()) {
176
+ const env = loadProjectEnv(projectRoot);
177
+ return {
178
+ openai: Boolean(process.env.OPENAI_API_KEY || process.env.LLM_API_KEY),
179
+ deepseek: Boolean(process.env.DEEPSEEK_API_KEY || process.env.LLM_API_KEY),
180
+ mock: true,
181
+ localEnv: env.loaded,
182
+ localEnvPath: env.path,
183
+ envVars: {
184
+ openai: ["OPENAI_API_KEY", "LLM_API_KEY"],
185
+ deepseek: ["DEEPSEEK_API_KEY", "LLM_API_KEY"],
186
+ },
187
+ };
188
+ }
189
+
190
+ export async function setProviderKey(projectRoot, provider, value) {
191
+ const normalizedProvider = String(provider || "").toLowerCase();
192
+ const keyName = normalizedProvider === "openai" ? "OPENAI_API_KEY" : "DEEPSEEK_API_KEY";
193
+ if (!["deepseek", "openai"].includes(normalizedProvider)) {
194
+ throw new Error("Provider must be deepseek or openai.");
195
+ }
196
+
197
+ const keyValue = String(value || "").trim();
198
+ if (!keyValue) throw new Error("Key value is required.");
199
+
200
+ const paths = projectPaths(projectRoot);
201
+ await fsp.mkdir(paths.controlDir, { recursive: true });
202
+ let parsed = {};
203
+ try {
204
+ parsed = parseEnvText(await fsp.readFile(paths.envPath, "utf8"));
205
+ } catch {
206
+ parsed = {};
207
+ }
208
+ parsed[keyName] = keyValue;
209
+ const output = Object.entries(parsed)
210
+ .filter(([key]) => LOCAL_ENV_KEYS.has(key))
211
+ .map(([key, envValue]) => `${key}=${JSON.stringify(envValue)}`)
212
+ .join("\n");
213
+ await fsp.writeFile(paths.envPath, `${output}\n`, { mode: 0o600 });
214
+ await fsp.chmod(paths.envPath, 0o600).catch(() => {});
215
+ loadProjectEnv(projectRoot, { override: true });
216
+ return {
217
+ ok: true,
218
+ provider: normalizedProvider,
219
+ keyName,
220
+ path: paths.envPath,
221
+ };
222
+ }
223
+
224
+ export async function listProjectSessions(projectRoot = process.cwd(), limit = 50) {
225
+ const paths = projectPaths(projectRoot);
226
+ const entries = await fsp.readdir(paths.sessionsDir, { withFileTypes: true }).catch(() => []);
227
+ const sessions = [];
228
+ for (const entry of entries) {
229
+ if (!entry.isDirectory()) continue;
230
+ const statePath = path.join(paths.sessionsDir, entry.name, "state.json");
231
+ try {
232
+ const state = JSON.parse(await fsp.readFile(statePath, "utf8"));
233
+ sessions.push({
234
+ sessionId: state.sessionId || entry.name,
235
+ provider: state.provider || "",
236
+ model: state.model || "",
237
+ goal: state.goal || "",
238
+ createdAt: state.createdAt || "",
239
+ updatedAt: state.updatedAt || state.createdAt || "",
240
+ stepsCompleted: state.stepsCompleted || 0,
241
+ });
242
+ } catch {
243
+ // Ignore malformed or unrelated session folders.
244
+ }
245
+ }
246
+ return sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))).slice(0, limit);
247
+ }
248
+
249
+ export async function showProjectSession(projectRoot, sessionId) {
250
+ const paths = projectPaths(projectRoot);
251
+ const safeId = String(sessionId || "");
252
+ if (!/^[A-Za-z0-9._:-]+$/.test(safeId) || safeId.includes("..")) {
253
+ throw new Error("Invalid session id.");
254
+ }
255
+ const sessionDir = path.join(paths.sessionsDir, safeId);
256
+ const state = JSON.parse(await fsp.readFile(path.join(sessionDir, "state.json"), "utf8"));
257
+ let events = [];
258
+ try {
259
+ const raw = await fsp.readFile(path.join(sessionDir, "events.jsonl"), "utf8");
260
+ events = raw
261
+ .split("\n")
262
+ .filter(Boolean)
263
+ .map((line) => JSON.parse(line));
264
+ } catch {
265
+ events = [];
266
+ }
267
+ return {
268
+ sessionId: state.sessionId || safeId,
269
+ provider: state.provider || "",
270
+ model: state.model || "",
271
+ goal: state.goal || "",
272
+ createdAt: state.createdAt || "",
273
+ updatedAt: state.updatedAt || "",
274
+ chat: state.chat || [],
275
+ events: events.slice(-80),
276
+ };
277
+ }
278
+
279
+ export async function npmLatestVersion(packageName = "@lazyingart/agintiflow") {
280
+ try {
281
+ const { stdout } = await execFileAsync("npm", ["view", packageName, "version", "--json"], {
282
+ timeout: 6000,
283
+ maxBuffer: 100 * 1024,
284
+ env: { PATH: process.env.PATH || "/usr/bin:/bin" },
285
+ });
286
+ return JSON.parse(stdout.trim());
287
+ } catch {
288
+ return "";
289
+ }
290
+ }
291
+
292
+ export async function doctorReport(projectRoot, packageVersion, config) {
293
+ const paths = projectPaths(projectRoot);
294
+ const keyStatus = providerKeyStatus(projectRoot);
295
+ const [sessions, dockerStatus, latestVersion] = await Promise.all([
296
+ listProjectSessions(projectRoot, 8),
297
+ getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
298
+ npmLatestVersion(),
299
+ ]);
300
+
301
+ return {
302
+ ok: true,
303
+ package: {
304
+ name: "@lazyingart/agintiflow",
305
+ version: packageVersion,
306
+ npmLatest: latestVersion || "unknown",
307
+ },
308
+ node: {
309
+ version: process.version,
310
+ ok: Number(process.versions.node.split(".")[0]) >= 22,
311
+ },
312
+ project: {
313
+ root: paths.root,
314
+ controlDir: paths.controlDir,
315
+ sessionsDir: paths.sessionsDir,
316
+ sessionDbPath: paths.sessionDbPath,
317
+ localEnvPresent: keyStatus.localEnv,
318
+ },
319
+ keys: {
320
+ openai: keyStatus.openai,
321
+ deepseek: keyStatus.deepseek,
322
+ mock: true,
323
+ },
324
+ sandbox: dockerStatus,
325
+ wrappers: listAgentWrappers().map((wrapper) => ({
326
+ name: wrapper.name,
327
+ label: wrapper.label,
328
+ available: wrapper.available,
329
+ })),
330
+ sessions,
331
+ };
332
+ }