@lazyingart/agintiflow 0.6.0 → 0.8.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
@@ -4,6 +4,8 @@ import { listAgentWrappers } from "./tool-wrappers.js";
4
4
  import { getModelPresets } from "./model-routing.js";
5
5
  import { getDockerSandboxStatus, runDockerPreflight } from "./docker-sandbox.js";
6
6
  import { buildCapabilityReport, printCapabilityReport } from "./capabilities.js";
7
+ import { startInteractiveCli } from "./interactive-cli.js";
8
+ import { SessionStore } from "./session-store.js";
7
9
  import {
8
10
  doctorReport,
9
11
  initProject,
@@ -52,9 +54,11 @@ export function parseArgs(argv) {
52
54
  sandboxStatus: false,
53
55
  sandboxPreflight: false,
54
56
  web: false,
57
+ interactive: false,
55
58
  port: "",
56
59
  host: "",
57
60
  listProfiles: false,
61
+ latex: false,
58
62
  };
59
63
 
60
64
  const parts = [];
@@ -64,6 +68,10 @@ export function parseArgs(argv) {
64
68
  result.web = true;
65
69
  continue;
66
70
  }
71
+ if (arg === "chat" || arg === "interactive" || arg === "--chat" || arg === "--interactive") {
72
+ result.interactive = true;
73
+ continue;
74
+ }
67
75
  if (arg === "--port") {
68
76
  result.port = readOption(argv, i);
69
77
  i += 1;
@@ -124,6 +132,14 @@ export function parseArgs(argv) {
124
132
  result.sandboxMode = result.sandboxMode || "docker-workspace";
125
133
  continue;
126
134
  }
135
+ if (arg === "--latex") {
136
+ result.latex = true;
137
+ result.taskProfile = "latex";
138
+ result.sandboxMode = result.sandboxMode || "docker-workspace";
139
+ result.packageInstallPolicy = result.packageInstallPolicy || "allow";
140
+ result.maxSteps = result.maxSteps || 30;
141
+ continue;
142
+ }
127
143
  if (arg === "--max-steps") {
128
144
  result.maxSteps = Number(readOption(argv, i));
129
145
  i += 1;
@@ -133,6 +149,10 @@ export function parseArgs(argv) {
133
149
  result.allowShellTool = true;
134
150
  continue;
135
151
  }
152
+ if (arg === "--no-shell") {
153
+ result.allowShellTool = false;
154
+ continue;
155
+ }
136
156
  if (arg === "--allow-destructive" || arg === "--trusted-host-shell") {
137
157
  result.allowDestructive = true;
138
158
  continue;
@@ -197,6 +217,31 @@ export function parseArgs(argv) {
197
217
  return result;
198
218
  }
199
219
 
220
+ function printUsage() {
221
+ console.log(
222
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti queue <session-id> "message" OR aginti [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
223
+ );
224
+ }
225
+
226
+ function agentDefaults(args) {
227
+ const defaults = {
228
+ ...args,
229
+ allowShellTool: args.allowShellTool ?? true,
230
+ allowFileTools: args.allowFileTools ?? true,
231
+ sandboxMode: args.sandboxMode || "docker-workspace",
232
+ packageInstallPolicy: args.packageInstallPolicy || "allow",
233
+ useDockerSandbox: args.useDockerSandbox ?? true,
234
+ maxSteps: args.maxSteps || (args.latex || args.taskProfile === "latex" ? 30 : 24),
235
+ };
236
+
237
+ if (defaults.sandboxMode === "host") {
238
+ defaults.useDockerSandbox = false;
239
+ defaults.packageInstallPolicy = args.packageInstallPolicy || "prompt";
240
+ }
241
+
242
+ return defaults;
243
+ }
244
+
200
245
  function printRoutes() {
201
246
  const presets = getModelPresets();
202
247
  for (const preset of Object.values(presets)) {
@@ -311,7 +356,26 @@ async function handleSessionsCommand(argv) {
311
356
  process.exit(1);
312
357
  }
313
358
 
359
+ async function handleQueueCommand(argv) {
360
+ const sessionId = argv[0] || "";
361
+ const content = argv.slice(1).join(" ").trim();
362
+ if (!/^[A-Za-z0-9._:-]+$/.test(sessionId) || !content) {
363
+ console.error('Usage: aginti queue <session-id> "message to apply during the next agent step"');
364
+ process.exit(1);
365
+ }
366
+
367
+ const store = new SessionStore(path.resolve(process.cwd(), ".sessions"), sessionId);
368
+ await store.appendInbox(content, { source: "cli" });
369
+ await store.appendEvent("conversation.queued_input", { prompt: content, source: "cli" }).catch(() => {});
370
+ console.log(`queued message for ${sessionId}`);
371
+ }
372
+
314
373
  export async function main(argv = process.argv.slice(2)) {
374
+ if (argv[0] === "--help" || argv[0] === "help" || argv[0] === "-h") {
375
+ printUsage();
376
+ return;
377
+ }
378
+
315
379
  if (argv[0] === "--version" || argv[0] === "version" || argv[0] === "-v") {
316
380
  console.log(packageJson.version);
317
381
  return;
@@ -386,6 +450,11 @@ export async function main(argv = process.argv.slice(2)) {
386
450
  return;
387
451
  }
388
452
 
453
+ if (argv[0] === "queue") {
454
+ await handleQueueCommand(argv.slice(1));
455
+ return;
456
+ }
457
+
389
458
  if (argv[0] === "resume") {
390
459
  const sessionId = argv[1] || "";
391
460
  const prompt = argv.slice(2).join(" ").trim();
@@ -393,13 +462,18 @@ export async function main(argv = process.argv.slice(2)) {
393
462
  console.error('Usage: aginti resume <session-id> "new prompt"');
394
463
  process.exit(1);
395
464
  }
396
- const config = loadConfig({ ...parseArgs([prompt]), resume: sessionId, goal: prompt }, { packageDir });
465
+ const config = loadConfig(agentDefaults({ ...parseArgs([prompt]), resume: sessionId, goal: prompt }), { packageDir });
397
466
  await runAgent(config);
398
467
  return;
399
468
  }
400
469
 
401
470
  const args = parseArgs(argv);
402
471
 
472
+ if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
473
+ await startInteractiveCli(agentDefaults(args), { packageDir, packageVersion: packageJson.version });
474
+ return;
475
+ }
476
+
403
477
  if (args.web) {
404
478
  if (args.port) process.env.PORT = String(args.port);
405
479
  if (args.host) process.env.HOST = String(args.host);
@@ -433,12 +507,10 @@ export async function main(argv = process.argv.slice(2)) {
433
507
  }
434
508
 
435
509
  if (!args.goal && !args.resume) {
436
- console.error(
437
- 'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
438
- );
510
+ printUsage();
439
511
  process.exit(1);
440
512
  }
441
513
 
442
- const config = loadConfig(args, { packageDir });
514
+ const config = loadConfig(agentDefaults(args), { packageDir });
443
515
  await runAgent(config);
444
516
  }
@@ -39,6 +39,7 @@ const TEST_PATTERNS = [
39
39
  ];
40
40
 
41
41
  const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
42
+ const PERMISSION_CHANGE_PATTERNS = [/^(?:sudo\s+)?chmod\s+[-+=,rwxugoXst0-7]+\s+[-\w./]+$/];
42
43
 
43
44
  const NETWORK_FETCH_PATTERNS = [
44
45
  /^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
@@ -153,15 +154,6 @@ function classifySimpleCommand(normalized) {
153
154
  return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
154
155
  }
155
156
 
156
- const lowered = ` ${normalized.toLowerCase()} `;
157
- if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
158
- return {
159
- category: "general-shell",
160
- needsNetwork: true,
161
- writesWorkspace: true,
162
- reason: `Command uses general shell syntax: ${normalized}`,
163
- };
164
- }
165
157
  if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
166
158
  const target = normalized.replace(/^mkdir\s+-p\s+/, "");
167
159
  const virtualWorkspacePath = isSafeVirtualWorkspaceDir(target);
@@ -170,6 +162,16 @@ function classifySimpleCommand(normalized) {
170
162
  }
171
163
  return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
172
164
  }
165
+ if (matchAny(PERMISSION_CHANGE_PATTERNS, normalized)) {
166
+ return {
167
+ category: "permission-change",
168
+ needsNetwork: false,
169
+ writesWorkspace: true,
170
+ reason: `Command changes workspace file mode: ${normalized}`,
171
+ };
172
+ }
173
+
174
+ const lowered = ` ${normalized.toLowerCase()} `;
173
175
  if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
174
176
  return {
175
177
  category: "destructive",
@@ -178,6 +180,14 @@ function classifySimpleCommand(normalized) {
178
180
  reason: `Command contains a write-capable or destructive token: ${normalized}`,
179
181
  };
180
182
  }
183
+ if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
184
+ return {
185
+ category: "general-shell",
186
+ needsNetwork: true,
187
+ writesWorkspace: true,
188
+ reason: `Command uses general shell syntax: ${normalized}`,
189
+ };
190
+ }
181
191
 
182
192
  if (matchAny(READ_ONLY_PATTERNS, normalized)) {
183
193
  return { category: "read-only", needsNetwork: false, writesWorkspace: false };
@@ -306,6 +316,20 @@ export function evaluateCommandPolicy(command, config) {
306
316
  };
307
317
  }
308
318
 
319
+ if (classification.category === "permission-change" && !trustedDockerShell && !trustedHostShell) {
320
+ return {
321
+ allowed: false,
322
+ ...classification,
323
+ needsApproval: true,
324
+ reason:
325
+ sandboxMode === "host"
326
+ ? "Workspace permission changes on the host require Allow destructive actions. Prefer Docker workspace mode."
327
+ : "Docker permission changes require docker-workspace mode with Package installs = allow.",
328
+ sandboxMode,
329
+ packageInstallPolicy,
330
+ };
331
+ }
332
+
309
333
  if (classification.category === "destructive" && !config.allowDestructive) {
310
334
  return {
311
335
  allowed: false,
package/src/config.js CHANGED
@@ -42,9 +42,9 @@ export function resolveRuntimeConfig(args, overrides = {}) {
42
42
 
43
43
  const defaults = getProviderDefaults(route.provider);
44
44
  const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
45
- const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, false);
45
+ const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, true);
46
46
  const requestedSandboxMode =
47
- overrides.sandboxMode || args.sandboxMode || process.env.SANDBOX_MODE || (dockerRequested ? "docker-readonly" : "host");
47
+ overrides.sandboxMode || args.sandboxMode || process.env.SANDBOX_MODE || (dockerRequested ? "docker-workspace" : "host");
48
48
  const sandboxMode = normalizeSandboxMode(requestedSandboxMode);
49
49
 
50
50
  return {
@@ -65,14 +65,14 @@ export function resolveRuntimeConfig(args, overrides = {}) {
65
65
  apiKey: overrides.apiKey || defaults.apiKey,
66
66
  baseURL: overrides.baseURL || defaults.baseURL,
67
67
  model: route.model || defaults.model,
68
- maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, 15),
68
+ maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, 24),
69
69
  headless: parseBoolean(overrides.headless ?? args.headless ?? process.env.HEADLESS, false),
70
70
  allowedDomains: Array.isArray(overrides.allowedDomains)
71
71
  ? overrides.allowedDomains
72
72
  : parseList(process.env.ALLOWED_DOMAINS),
73
73
  allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
74
74
  allowDestructive: parseBoolean(overrides.allowDestructive ?? args.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
75
- allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, false),
75
+ allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, true),
76
76
  allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
77
77
  allowWrapperTools: parseBoolean(
78
78
  overrides.allowWrapperTools ?? args.allowWrapperTools ?? process.env.ALLOW_WRAPPER_TOOLS,
@@ -84,9 +84,9 @@ export function resolveRuntimeConfig(args, overrides = {}) {
84
84
  wrapperTimeoutMs: parseNumber(overrides.wrapperTimeoutMs ?? process.env.WRAPPER_TIMEOUT_MS, 120000),
85
85
  sandboxMode,
86
86
  packageInstallPolicy: normalizePackageInstallPolicy(
87
- overrides.packageInstallPolicy || args.packageInstallPolicy || process.env.PACKAGE_INSTALL_POLICY || "prompt"
87
+ overrides.packageInstallPolicy || args.packageInstallPolicy || process.env.PACKAGE_INSTALL_POLICY || (sandboxMode === "host" ? "prompt" : "allow")
88
88
  ),
89
- useDockerSandbox: sandboxMode !== "host" || dockerRequested,
89
+ useDockerSandbox: sandboxMode !== "host",
90
90
  dockerSandboxImage: overrides.dockerSandboxImage || process.env.DOCKER_SANDBOX_IMAGE || "agintiflow-sandbox:latest",
91
91
  commandCwd: path.resolve(overrides.commandCwd || args.commandCwd || process.env.COMMAND_CWD || baseDir),
92
92
  sessionsDir: path.resolve(baseDir, ".sessions"),
@@ -1,5 +1,6 @@
1
1
  import { constants as fsConstants } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { execFile as execFileCallback } from "node:child_process";
5
6
  import { promisify } from "node:util";
@@ -8,6 +9,9 @@ import { redactSensitiveText, redactValue } from "./redaction.js";
8
9
 
9
10
  const execFile = promisify(execFileCallback);
10
11
  const DOCKER_WORKSPACE = "/workspace";
12
+ const DOCKER_HOME = "/aginti-home";
13
+ const DOCKER_CACHE = "/aginti-cache";
14
+ const DOCKER_ENV = "/aginti-env";
11
15
  const READY_IMAGES = new Set();
12
16
  const SANDBOX_LOG_LIMIT = 80;
13
17
  const sandboxLogs = [];
@@ -101,10 +105,31 @@ function sandboxDockerfilePath(config) {
101
105
  return path.join(sandboxPackageDir(config), "docker", "sandbox.Dockerfile");
102
106
  }
103
107
 
108
+ function persistentDockerDirs(config) {
109
+ const root = path.resolve(
110
+ config.dockerStateDir || process.env.AGINTIFLOW_DOCKER_STATE_DIR || path.join(os.homedir(), ".agintiflow", "docker")
111
+ );
112
+ return {
113
+ root,
114
+ home: path.join(root, "home"),
115
+ cache: path.join(root, "cache"),
116
+ env: path.join(root, "env"),
117
+ };
118
+ }
119
+
120
+ async function ensurePersistentDockerDirs(config) {
121
+ const dirs = persistentDockerDirs(config);
122
+ await Promise.all([fs.mkdir(dirs.home, { recursive: true }), fs.mkdir(dirs.cache, { recursive: true }), fs.mkdir(dirs.env, { recursive: true })]);
123
+ return dirs;
124
+ }
125
+
104
126
  export async function getDockerSandboxStatus(config) {
105
127
  const image = config.dockerSandboxImage;
106
128
  const dockerfilePath = sandboxDockerfilePath(config);
107
129
  const workspace = config.commandCwd;
130
+ if (config.useDockerSandbox) {
131
+ await ensurePersistentDockerDirs(config).catch(() => null);
132
+ }
108
133
  const [dockerReady, dockerfileExists, workspaceExists, workspaceReadable, workspaceWritable] = await Promise.all([
109
134
  dockerAvailable(),
110
135
  pathAccess(dockerfilePath, fsConstants.R_OK),
@@ -114,6 +139,12 @@ export async function getDockerSandboxStatus(config) {
114
139
  ]);
115
140
 
116
141
  const imageReady = dockerReady ? await dockerImageExists(image) : false;
142
+ const persistentDirs = persistentDockerDirs(config);
143
+ const [persistentHomeExists, persistentCacheExists, persistentEnvExists] = await Promise.all([
144
+ pathAccess(persistentDirs.home, fsConstants.F_OK),
145
+ pathAccess(persistentDirs.cache, fsConstants.F_OK),
146
+ pathAccess(persistentDirs.env, fsConstants.F_OK),
147
+ ]);
117
148
  return {
118
149
  sandboxMode: normalizeSandboxMode(config.sandboxMode),
119
150
  useDockerSandbox: Boolean(config.useDockerSandbox),
@@ -127,6 +158,18 @@ export async function getDockerSandboxStatus(config) {
127
158
  workspaceReadable,
128
159
  workspaceWritable,
129
160
  dockerfileExists,
161
+ persistentDocker: {
162
+ home: persistentDirs.home,
163
+ cache: persistentDirs.cache,
164
+ env: persistentDirs.env,
165
+ homeExists: persistentHomeExists,
166
+ cacheExists: persistentCacheExists,
167
+ envExists: persistentEnvExists,
168
+ containerHome: DOCKER_HOME,
169
+ containerCache: DOCKER_CACHE,
170
+ containerEnv: DOCKER_ENV,
171
+ pythonVenv: `${DOCKER_ENV}/python`,
172
+ },
130
173
  logs: getSandboxLogs(),
131
174
  };
132
175
  }
@@ -165,7 +208,30 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
165
208
  await buildDockerSandboxImage(config, observers);
166
209
  }
167
210
 
168
- function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config)) {
211
+ function dockerCommand(command, policy) {
212
+ const envLines = [
213
+ `export HOME=${DOCKER_HOME}`,
214
+ `export XDG_CACHE_HOME=${DOCKER_CACHE}`,
215
+ `export PIP_CACHE_DIR=${DOCKER_CACHE}/pip`,
216
+ `export UV_CACHE_DIR=${DOCKER_CACHE}/uv`,
217
+ `export NPM_CONFIG_CACHE=${DOCKER_CACHE}/npm`,
218
+ `export CONDA_PKGS_DIRS=${DOCKER_CACHE}/conda-pkgs`,
219
+ `export PYTHONUSERBASE=${DOCKER_ENV}/python-user`,
220
+ `export PATH=${DOCKER_ENV}/python/bin:${DOCKER_ENV}/python-user/bin:${DOCKER_ENV}/miniforge/bin:${DOCKER_ENV}/bin:$PATH`,
221
+ `cd ${DOCKER_WORKSPACE}`,
222
+ ];
223
+
224
+ if (!policy.requiresDockerRoot) {
225
+ envLines.push(
226
+ `if command -v python3 >/dev/null 2>&1 && [ ! -x ${DOCKER_ENV}/python/bin/python ]; then python3 -m venv --system-site-packages ${DOCKER_ENV}/python >/dev/null 2>&1 || true; fi`,
227
+ `if [ -f ${DOCKER_ENV}/python/bin/activate ]; then . ${DOCKER_ENV}/python/bin/activate; fi`
228
+ );
229
+ }
230
+
231
+ return [...envLines, String(command)].join("\n");
232
+ }
233
+
234
+ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config), persistentDirs = persistentDockerDirs(config)) {
169
235
  const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
170
236
  const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
171
237
  const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
@@ -196,24 +262,29 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
196
262
  ...readOnlyArgs,
197
263
  ...userArgs,
198
264
  "-e",
199
- "HOME=/tmp",
200
- "-e",
201
265
  "NPM_CONFIG_USERCONFIG=/tmp/.npmrc",
202
266
  "-e",
203
267
  "PIP_DISABLE_PIP_VERSION_CHECK=1",
204
268
  "-v",
205
269
  `${config.commandCwd}:${DOCKER_WORKSPACE}:${mountMode}`,
270
+ "-v",
271
+ `${persistentDirs.home}:${DOCKER_HOME}:rw`,
272
+ "-v",
273
+ `${persistentDirs.cache}:${DOCKER_CACHE}:rw`,
274
+ "-v",
275
+ `${persistentDirs.env}:${DOCKER_ENV}:rw`,
206
276
  "-w",
207
277
  DOCKER_WORKSPACE,
208
278
  config.dockerSandboxImage,
209
279
  "bash",
210
280
  "-lc",
211
- String(command),
281
+ dockerCommand(command, policy),
212
282
  ];
213
283
  }
214
284
 
215
285
  export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
216
- const result = await execDocker(dockerRunArgs(command, config, policy), {
286
+ const persistentDirs = await ensurePersistentDockerDirs(config);
287
+ const result = await execDocker(dockerRunArgs(command, config, policy, persistentDirs), {
217
288
  timeout: policy.needsNetwork ? 120000 : policy.category === "toolchain" ? 90000 : 15000,
218
289
  maxBuffer: 300 * 1024,
219
290
  });
@@ -227,6 +298,7 @@ export async function runDockerSandboxCommand(command, config, policy = evaluate
227
298
  category: policy.category,
228
299
  sandboxMode: policy.sandboxMode,
229
300
  network: policy.needsNetwork ? "bridge" : "none",
301
+ persistentDocker: persistentDirs,
230
302
  stdout: payload.stdout.slice(0, 1200),
231
303
  stderr: payload.stderr.slice(0, 1200),
232
304
  });
@@ -274,6 +346,7 @@ export async function runDockerPreflight(config, options = {}) {
274
346
 
275
347
  let status = await getDockerSandboxStatus(config);
276
348
  const runChecks = async () => {
349
+ const persistentDirs = await ensurePersistentDockerDirs(config);
277
350
  const results = [];
278
351
  for (const command of [
279
352
  "node -v",
@@ -287,7 +360,7 @@ export async function runDockerPreflight(config, options = {}) {
287
360
  "rg --version",
288
361
  ]) {
289
362
  try {
290
- const result = await execDocker(dockerRunArgs(command, config, { needsNetwork: false, category: "preflight" }), {
363
+ const result = await execDocker(dockerRunArgs(command, config, { needsNetwork: false, category: "preflight" }, persistentDirs), {
291
364
  timeout: 15000,
292
365
  maxBuffer: 100 * 1024,
293
366
  });
@@ -0,0 +1,241 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { runAgent } from "./agent-runner.js";
4
+ import { loadConfig } from "./config.js";
5
+ import { initProject, listProjectSessions } from "./project.js";
6
+ import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
7
+ import { normalizeTaskProfile } from "./task-profiles.js";
8
+
9
+ function printHelp() {
10
+ console.log(
11
+ [
12
+ "Commands:",
13
+ " /help Show this help.",
14
+ " /status Show active route, workspace, sandbox, and session.",
15
+ " /new Start a fresh session on the next message.",
16
+ " /resume <session-id> Continue a saved session.",
17
+ " /sessions List recent sessions in this project.",
18
+ " /profile <name> Set task profile, e.g. code, website, latex, maintenance.",
19
+ " /routing <mode> Set routing: smart, fast, complex, manual.",
20
+ " /provider <name> Set provider: deepseek, openai, mock.",
21
+ " /model <name> Set an explicit model, or /model auto.",
22
+ " /docker on Use docker-workspace with approved package installs.",
23
+ " /docker off Use host shell policy.",
24
+ " /latex on Use the LaTeX/PDF profile in Docker with a larger step budget.",
25
+ " /installs block|prompt|allow",
26
+ " /cwd <path> Change command workspace.",
27
+ " /exit Quit.",
28
+ "",
29
+ "Type a normal request to run the agent. Example: write a Python CLI app with tests",
30
+ ].join("\n")
31
+ );
32
+ }
33
+
34
+ function printStatus(state) {
35
+ console.log(`project=${process.cwd()}`);
36
+ console.log(`cwd=${state.commandCwd || process.cwd()}`);
37
+ console.log(`session=${state.sessionId || "new"}`);
38
+ console.log(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
39
+ console.log(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
40
+ console.log(
41
+ `shell=${state.allowShellTool} files=${state.allowFileTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
42
+ );
43
+ if (state.sandboxMode !== "host") {
44
+ console.log(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
45
+ }
46
+ }
47
+
48
+ function createState(args = {}) {
49
+ return {
50
+ provider: args.provider || "",
51
+ model: args.model || "",
52
+ routingMode: args.routingMode || "smart",
53
+ commandCwd: args.commandCwd || process.cwd(),
54
+ sandboxMode: normalizeSandboxMode(args.sandboxMode || "docker-workspace"),
55
+ packageInstallPolicy: normalizePackageInstallPolicy(args.packageInstallPolicy || "allow"),
56
+ allowShellTool: args.allowShellTool ?? true,
57
+ allowFileTools: args.allowFileTools ?? true,
58
+ allowWrapperTools: args.allowWrapperTools ?? false,
59
+ allowDestructive: args.allowDestructive ?? false,
60
+ preferredWrapper: args.preferredWrapper || "codex",
61
+ taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
62
+ headless: args.headless ?? false,
63
+ maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps : args.latex ? 30 : 24,
64
+ sessionId: args.resume || "",
65
+ };
66
+ }
67
+
68
+ async function handleCommand(line, state, packageDir) {
69
+ const [command, ...rest] = line.slice(1).trim().split(/\s+/);
70
+ const value = rest.join(" ").trim();
71
+
72
+ if (!command || command === "help" || command === "?") {
73
+ printHelp();
74
+ return true;
75
+ }
76
+ if (command === "exit" || command === "quit" || command === "q") return false;
77
+ if (command === "status") {
78
+ printStatus(state);
79
+ return true;
80
+ }
81
+ if (command === "new") {
82
+ state.sessionId = "";
83
+ console.log("Next message will start a new session.");
84
+ return true;
85
+ }
86
+ if (command === "resume") {
87
+ if (!value) console.log("Usage: /resume <session-id>");
88
+ else {
89
+ state.sessionId = value;
90
+ console.log(`Resuming ${state.sessionId}`);
91
+ }
92
+ return true;
93
+ }
94
+ if (command === "sessions") {
95
+ const sessions = await listProjectSessions(process.cwd(), 20);
96
+ if (sessions.length === 0) console.log("No project-local sessions found.");
97
+ else {
98
+ for (const session of sessions) {
99
+ const goal = session.goal ? ` ${session.goal.slice(0, 80)}` : "";
100
+ console.log(`${session.sessionId} ${session.provider}/${session.model} ${session.updatedAt}${goal}`);
101
+ }
102
+ }
103
+ return true;
104
+ }
105
+ if (command === "profile") {
106
+ state.taskProfile = normalizeTaskProfile(value || "auto");
107
+ console.log(`profile=${state.taskProfile}`);
108
+ return true;
109
+ }
110
+ if (command === "routing") {
111
+ state.routingMode = value || "smart";
112
+ console.log(`routing=${state.routingMode}`);
113
+ return true;
114
+ }
115
+ if (command === "provider") {
116
+ state.provider = value === "auto" ? "" : value;
117
+ console.log(`provider=${state.provider || "auto"}`);
118
+ return true;
119
+ }
120
+ if (command === "model") {
121
+ state.model = value === "auto" ? "" : value;
122
+ console.log(`model=${state.model || "auto"}`);
123
+ return true;
124
+ }
125
+ if (command === "installs") {
126
+ state.packageInstallPolicy = normalizePackageInstallPolicy(value || "prompt");
127
+ console.log(`installs=${state.packageInstallPolicy}`);
128
+ return true;
129
+ }
130
+ if (command === "docker") {
131
+ if (value === "on") {
132
+ state.sandboxMode = "docker-workspace";
133
+ state.packageInstallPolicy = "allow";
134
+ console.log(`docker=on /workspace -> ${state.commandCwd || process.cwd()} installs=allow`);
135
+ } else if (value === "off") {
136
+ state.sandboxMode = "host";
137
+ state.packageInstallPolicy = "prompt";
138
+ console.log("docker=off sandbox=host installs=prompt");
139
+ } else {
140
+ console.log("Usage: /docker on OR /docker off");
141
+ }
142
+ return true;
143
+ }
144
+ if (command === "latex") {
145
+ if (value === "on" || value === "") {
146
+ state.taskProfile = "latex";
147
+ state.sandboxMode = "docker-workspace";
148
+ state.packageInstallPolicy = "allow";
149
+ state.maxSteps = Math.max(state.maxSteps, 30);
150
+ console.log("latex=on profile=latex sandbox=docker-workspace installs=allow maxSteps=30");
151
+ } else if (value === "off") {
152
+ state.taskProfile = "auto";
153
+ console.log("latex=off profile=auto");
154
+ } else {
155
+ console.log("Usage: /latex on OR /latex off");
156
+ }
157
+ return true;
158
+ }
159
+ if (command === "cwd") {
160
+ state.commandCwd = value || process.cwd();
161
+ console.log(`cwd=${state.commandCwd}`);
162
+ return true;
163
+ }
164
+ if (command === "init") {
165
+ const result = await initProject(process.cwd());
166
+ console.log(`initialized project=${result.projectRoot}`);
167
+ return true;
168
+ }
169
+ if (command === "web") {
170
+ const port = value || "3220";
171
+ console.log(`Run in another terminal: aginti web --port ${port}`);
172
+ return true;
173
+ }
174
+
175
+ console.log(`Unknown command: /${command}. Use /help.`);
176
+ return true;
177
+ }
178
+
179
+ async function runPrompt(prompt, state, packageDir) {
180
+ const config = loadConfig(
181
+ {
182
+ provider: state.provider,
183
+ model: state.model,
184
+ routingMode: state.routingMode,
185
+ commandCwd: state.commandCwd,
186
+ sandboxMode: state.sandboxMode,
187
+ packageInstallPolicy: state.packageInstallPolicy,
188
+ allowShellTool: state.allowShellTool,
189
+ allowFileTools: state.allowFileTools,
190
+ allowWrapperTools: state.allowWrapperTools,
191
+ allowDestructive: state.allowDestructive,
192
+ preferredWrapper: state.preferredWrapper,
193
+ taskProfile: state.taskProfile,
194
+ maxSteps: state.maxSteps,
195
+ headless: state.headless,
196
+ resume: state.sessionId,
197
+ goal: prompt,
198
+ },
199
+ { packageDir, baseDir: process.cwd() }
200
+ );
201
+
202
+ const result = await runAgent(config);
203
+ state.sessionId = result.sessionId || state.sessionId;
204
+ }
205
+
206
+ export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
207
+ const state = createState(args);
208
+ const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
209
+
210
+ console.log(`AgInTiFlow ${packageVersion || ""}`.trim());
211
+ console.log(`Project: ${process.cwd()}`);
212
+ console.log("Interactive agent chat. Type /help for commands, /exit to quit.");
213
+ printStatus(state);
214
+
215
+ try {
216
+ while (true) {
217
+ let answer = "";
218
+ try {
219
+ answer = await rl.question("\naginti> ");
220
+ } catch (error) {
221
+ if (error?.code === "ERR_USE_AFTER_CLOSE") break;
222
+ throw error;
223
+ }
224
+ const line = answer.trim();
225
+ if (!line) continue;
226
+ if (line.startsWith("/")) {
227
+ const keepGoing = await handleCommand(line, state, packageDir);
228
+ if (!keepGoing) break;
229
+ continue;
230
+ }
231
+
232
+ try {
233
+ await runPrompt(line, state, packageDir);
234
+ } catch (error) {
235
+ console.error(`error: ${error.message}`);
236
+ }
237
+ }
238
+ } finally {
239
+ rl.close();
240
+ }
241
+ }