@lazyingart/agintiflow 0.1.0 → 0.2.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/web.js CHANGED
@@ -7,17 +7,27 @@ import { resolveRuntimeConfig } from "./src/config.js";
7
7
  import { WebDatabase } from "./src/web-db.js";
8
8
  import { SessionStore } from "./src/session-store.js";
9
9
  import { getModelPresets, getProviderDefaults, normalizeRoutingMode } from "./src/model-routing.js";
10
- import { listAgentWrappers } from "./src/tool-wrappers.js";
10
+ import { listAgentWrappers, normalizeWrapperName } from "./src/tool-wrappers.js";
11
11
  import { getDockerSandboxStatus, getSandboxLogs, runDockerPreflight } from "./src/docker-sandbox.js";
12
12
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./src/command-policy.js";
13
+ import { summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./src/workspace-tools.js";
14
+ import {
15
+ buildArtifacts,
16
+ countUnreadArtifacts,
17
+ findArtifact,
18
+ readArtifactContent,
19
+ serializeArtifacts,
20
+ } from "./src/artifact-tunnel.js";
13
21
 
14
22
  const __filename = fileURLToPath(import.meta.url);
15
23
  const __dirname = path.dirname(__filename);
16
- const baseDir = process.cwd();
24
+ const packageDir = __dirname;
25
+ const baseDir = path.resolve(process.env.AGINTIFLOW_RUNTIME_DIR || process.cwd());
17
26
  const sessionsDir = path.join(baseDir, ".sessions");
18
27
 
19
28
  const app = express();
20
29
  const port = Number(process.env.PORT || 3210);
30
+ const host = process.env.HOST || "127.0.0.1";
21
31
  const runs = new Map();
22
32
  const db = new WebDatabase(baseDir);
23
33
  const supportedLanguages = new Set([
@@ -43,6 +53,10 @@ function sessionStore(sessionId) {
43
53
  return new SessionStore(sessionsDir, sessionId);
44
54
  }
45
55
 
56
+ function isSafeSessionId(sessionId) {
57
+ return /^[A-Za-z0-9._:-]+$/.test(String(sessionId || "")) && !String(sessionId || "").includes("..");
58
+ }
59
+
46
60
  function mapEventLogs(events) {
47
61
  return events.map((event) => ({
48
62
  at: event.timestamp,
@@ -65,6 +79,7 @@ async function loadStoredRun(sessionId) {
65
79
  provider: meta.provider,
66
80
  model: meta.model,
67
81
  goal: meta.goal,
82
+ title: meta.title || "",
68
83
  startedAt: meta.startedAt,
69
84
  endedAt: meta.endedAt || null,
70
85
  result: meta.result || "",
@@ -80,6 +95,7 @@ function serializeRun(run) {
80
95
  provider: run.provider,
81
96
  model: run.model,
82
97
  goal: run.goal,
98
+ title: run.title || "",
83
99
  startedAt: run.startedAt,
84
100
  endedAt: run.endedAt || null,
85
101
  result: run.result || "",
@@ -90,13 +106,14 @@ function serializeRun(run) {
90
106
 
91
107
  function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
92
108
  const modelPresets = getModelPresets();
93
- const routingMode = normalizeRoutingMode(body.routingMode || current.routingMode || "smart");
94
- const provider =
95
- body.provider === "openai" || body.provider === "deepseek" ? body.provider : current.provider || "deepseek";
109
+ const providerCandidate = body.provider || current.provider || "deepseek";
110
+ const provider = ["openai", "deepseek", "mock"].includes(providerCandidate) ? providerCandidate : "deepseek";
111
+ const routingMode =
112
+ provider === "mock" ? "manual" : normalizeRoutingMode(body.routingMode || current.routingMode || "smart");
96
113
  const providerDefaults = getProviderDefaults(provider);
97
114
  const parsedMaxSteps = Number(body.maxSteps);
98
115
  const parsedWrapperTimeoutMs = Number(body.wrapperTimeoutMs);
99
- const sandboxMode = normalizeSandboxMode(body.sandboxMode || current.sandboxMode || "docker-readonly");
116
+ const sandboxMode = normalizeSandboxMode(body.sandboxMode || current.sandboxMode || "docker-workspace");
100
117
 
101
118
  return {
102
119
  routingMode,
@@ -117,8 +134,10 @@ function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
117
134
  ? body.commandCwd.trim()
118
135
  : current.commandCwd || path.resolve(baseDir, ".."),
119
136
  allowShellTool: typeof body.allowShellTool === "boolean" ? body.allowShellTool : Boolean(current.allowShellTool),
137
+ allowFileTools: typeof body.allowFileTools === "boolean" ? body.allowFileTools : current.allowFileTools !== false,
120
138
  allowWrapperTools:
121
139
  typeof body.allowWrapperTools === "boolean" ? body.allowWrapperTools : Boolean(current.allowWrapperTools),
140
+ preferredWrapper: normalizeWrapperName(body.preferredWrapper || current.preferredWrapper || "codex"),
122
141
  wrapperTimeoutMs:
123
142
  Number.isFinite(parsedWrapperTimeoutMs) && parsedWrapperTimeoutMs >= 10000
124
143
  ? parsedWrapperTimeoutMs
@@ -180,7 +199,9 @@ function buildRunConfig(body, overrides = {}) {
180
199
  allowPasswords: merged.allowPasswords,
181
200
  allowDestructive: merged.allowDestructive,
182
201
  allowShellTool: merged.allowShellTool,
202
+ allowFileTools: merged.allowFileTools,
183
203
  allowWrapperTools: merged.allowWrapperTools,
204
+ preferredWrapper: merged.preferredWrapper,
184
205
  wrapperTimeoutMs: merged.wrapperTimeoutMs,
185
206
  sandboxMode: merged.sandboxMode,
186
207
  packageInstallPolicy: merged.packageInstallPolicy,
@@ -188,6 +209,7 @@ function buildRunConfig(body, overrides = {}) {
188
209
  dockerSandboxImage: merged.dockerSandboxImage,
189
210
  commandCwd: merged.commandCwd,
190
211
  baseDir,
212
+ packageDir,
191
213
  sessionId: overrides.sessionId,
192
214
  }
193
215
  );
@@ -214,6 +236,7 @@ function deriveSessionRecordFromState(state, existing = null) {
214
236
  provider: state.provider || existing?.provider || "deepseek",
215
237
  model: state.model || existing?.model || getProviderDefaults(state.provider || existing?.provider || "deepseek").model,
216
238
  goal: state.goal || existing?.goal || "",
239
+ title: existing?.title || "",
217
240
  status,
218
241
  startedAt: state.createdAt || existing?.startedAt || new Date().toISOString(),
219
242
  updatedAt,
@@ -295,12 +318,35 @@ async function loadChat(sessionId) {
295
318
  return {
296
319
  sessionId,
297
320
  goal: state?.goal || meta?.goal || "",
321
+ title: meta?.title || "",
298
322
  provider: state?.provider || meta?.provider || "",
299
323
  model: state?.model || meta?.model || "",
300
324
  chat: deriveChatFromState(state, meta),
301
325
  };
302
326
  }
303
327
 
328
+ function normalizeSessionTitle(title) {
329
+ return String(title || "")
330
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
331
+ .replace(/\s+/g, " ")
332
+ .trim()
333
+ .slice(0, 90);
334
+ }
335
+
336
+ function autoSessionTitleFromChat(data) {
337
+ const candidate =
338
+ data?.chat?.find((entry) => entry.role === "user" && String(entry.content || "").trim())?.content ||
339
+ data?.goal ||
340
+ "Untitled conversation";
341
+ const cleaned = normalizeSessionTitle(candidate)
342
+ .replace(/^please\s+/i, "")
343
+ .replace(/^could you\s+/i, "")
344
+ .replace(/^can you\s+/i, "")
345
+ .replace(/^plz\s+/i, "")
346
+ .replace(/^i want to\s+/i, "");
347
+ return normalizeSessionTitle(cleaned.length > 72 ? `${cleaned.slice(0, 69).trim()}...` : cleaned) || "Untitled conversation";
348
+ }
349
+
304
350
  async function ensureNotRunning(sessionId) {
305
351
  const inMemory = runs.get(sessionId);
306
352
  if (inMemory?.status === "running") {
@@ -313,6 +359,67 @@ async function ensureNotRunning(sessionId) {
313
359
  }
314
360
  }
315
361
 
362
+ async function collectWorkspaceActivity(limit = 24) {
363
+ const activity = [];
364
+ const sessions = db.listSessions(30);
365
+
366
+ for (const session of sessions) {
367
+ const store = sessionStore(session.sessionId);
368
+ const events = await store.loadEvents();
369
+ for (const event of events) {
370
+ if (event.type === "file.changed") {
371
+ activity.push({
372
+ at: event.timestamp,
373
+ kind: "changed",
374
+ sessionId: session.sessionId,
375
+ goal: session.goal,
376
+ ...event.data,
377
+ });
378
+ }
379
+
380
+ if (
381
+ event.type === "tool.blocked" &&
382
+ (WORKSPACE_TOOL_NAMES.includes(event.data?.toolName) || WORKSPACE_TOOL_NAMES.includes(event.data?.args?.toolName))
383
+ ) {
384
+ activity.push({
385
+ at: event.timestamp,
386
+ kind: "blocked",
387
+ sessionId: session.sessionId,
388
+ goal: session.goal,
389
+ toolName: event.data?.toolName || "",
390
+ path: event.data?.args?.path || "",
391
+ reason: event.data?.reason || "",
392
+ category: event.data?.category || "",
393
+ });
394
+ }
395
+ }
396
+ }
397
+
398
+ return activity.sort((a, b) => String(b.at).localeCompare(String(a.at))).slice(0, limit);
399
+ }
400
+
401
+ async function loadArtifactBundle(sessionId) {
402
+ if (!isSafeSessionId(sessionId)) {
403
+ return { error: "Invalid session id.", status: 400 };
404
+ }
405
+
406
+ const meta = db.getSession(sessionId);
407
+ const store = sessionStore(sessionId);
408
+ const events = await store.loadEvents();
409
+ if (!meta && events.length === 0) {
410
+ return { error: "Session not found.", status: 404 };
411
+ }
412
+
413
+ const preferences = normalizePreferencePayload({}, db.getPreferences());
414
+ const config = buildRunConfig({ ...preferences, goal: "" });
415
+ const bundle = buildArtifacts({ sessionId, events, store });
416
+ return {
417
+ ...bundle,
418
+ store,
419
+ config,
420
+ };
421
+ }
422
+
316
423
  function createRunRecord(config, goal, existingLogs = []) {
317
424
  return {
318
425
  sessionId: config.sessionId,
@@ -349,11 +456,15 @@ function wireRun(record, config) {
349
456
  onEvent: (type, data = {}) => push("event", type, data),
350
457
  })
351
458
  .then((result) => {
352
- record.status = "finished";
459
+ record.status = result?.stopped ? "stopped" : "finished";
353
460
  record.result = result?.result || "";
461
+ record.error = result?.stopped ? result.reason || "Run stopped before finish." : "";
354
462
  record.updatedAt = new Date().toISOString();
355
463
  record.endedAt = new Date().toISOString();
356
- push("session", "Run finished", { result: record.result });
464
+ push("session", result?.stopped ? "Run stopped" : "Run finished", {
465
+ result: record.result,
466
+ reason: result?.reason || "",
467
+ });
357
468
  db.upsertSession(record);
358
469
  })
359
470
  .catch((error) => {
@@ -370,12 +481,15 @@ app.use(express.json({ limit: "1mb" }));
370
481
  app.use(express.static(path.join(__dirname, "public")));
371
482
  app.use("/logos", express.static(path.join(__dirname, "logos")));
372
483
 
373
- app.get("/api/config", (_req, res) => {
484
+ app.get("/api/config", async (_req, res) => {
485
+ await syncStoredSessions();
374
486
  const preferences = normalizePreferencePayload({}, db.getPreferences());
487
+ const config = buildRunConfig({ ...preferences, goal: "" });
375
488
  res.json({
376
489
  defaults: {
377
490
  openai: publicProviderDefault("openai"),
378
491
  deepseek: publicProviderDefault("deepseek"),
492
+ mock: publicProviderDefault("mock"),
379
493
  headless: true,
380
494
  maxSteps: 15,
381
495
  },
@@ -387,12 +501,14 @@ app.get("/api/config", (_req, res) => {
387
501
  sandbox: {
388
502
  logs: getSandboxLogs(),
389
503
  },
504
+ workspace: summarizeWorkspaceTools(config),
390
505
  preferences,
391
506
  keyStatus: {
392
507
  openai: Boolean(process.env.OPENAI_API_KEY),
393
508
  deepseek: Boolean(process.env.DEEPSEEK_API_KEY),
509
+ mock: true,
394
510
  },
395
- sessions: db.listSessions(20),
511
+ sessions: db.listSessions(100),
396
512
  });
397
513
  });
398
514
 
@@ -419,8 +535,73 @@ app.post("/api/preferences", (req, res) => {
419
535
  res.json({ ok: true, preferences });
420
536
  });
421
537
 
422
- app.get("/api/sessions", (_req, res) => {
423
- res.json({ sessions: db.listSessions(20) });
538
+ app.get("/api/sessions", async (_req, res) => {
539
+ await syncStoredSessions();
540
+ res.json({ sessions: db.listSessions(100) });
541
+ });
542
+
543
+ app.get("/api/workspace/changes", async (_req, res) => {
544
+ const preferences = normalizePreferencePayload({}, db.getPreferences());
545
+ const config = buildRunConfig({ ...preferences, goal: "" });
546
+ res.json({
547
+ ok: true,
548
+ workspace: summarizeWorkspaceTools(config),
549
+ activity: await collectWorkspaceActivity(),
550
+ });
551
+ });
552
+
553
+ app.get("/api/sessions/:sessionId/artifacts", async (req, res) => {
554
+ const bundle = await loadArtifactBundle(req.params.sessionId);
555
+ if (bundle.error) {
556
+ res.status(bundle.status || 500).json({ error: bundle.error });
557
+ return;
558
+ }
559
+
560
+ res.json({
561
+ ok: true,
562
+ sessionId: req.params.sessionId,
563
+ items: serializeArtifacts(bundle.items),
564
+ selectedItemId: bundle.selectedItemId,
565
+ unreadCount: countUnreadArtifacts(bundle.items, req.query.seenAfter),
566
+ });
567
+ });
568
+
569
+ app.get("/api/sessions/:sessionId/artifacts/:artifactId", async (req, res) => {
570
+ const bundle = await loadArtifactBundle(req.params.sessionId);
571
+ if (bundle.error) {
572
+ res.status(bundle.status || 500).json({ error: bundle.error });
573
+ return;
574
+ }
575
+
576
+ const artifact = findArtifact(bundle.items, req.params.artifactId);
577
+ const content = await readArtifactContent(artifact, bundle);
578
+ if (!content.ok) {
579
+ res.status(artifact ? 400 : 404).json({ error: content.error || "Artifact not found." });
580
+ return;
581
+ }
582
+ res.json(content);
583
+ });
584
+
585
+ app.post("/api/sessions/:sessionId/artifacts/select", async (req, res) => {
586
+ const bundle = await loadArtifactBundle(req.params.sessionId);
587
+ if (bundle.error) {
588
+ res.status(bundle.status || 500).json({ error: bundle.error });
589
+ return;
590
+ }
591
+
592
+ const artifactId = String(req.body?.artifactId || "");
593
+ const artifact = findArtifact(bundle.items, artifactId);
594
+ if (!artifact) {
595
+ res.status(404).json({ error: "Artifact not found." });
596
+ return;
597
+ }
598
+
599
+ await sessionStore(req.params.sessionId).appendEvent("canvas.selected", {
600
+ artifactId,
601
+ title: artifact.title,
602
+ source: "user",
603
+ });
604
+ res.json({ ok: true, artifact: serializeArtifacts([artifact])[0] });
424
605
  });
425
606
 
426
607
  app.get("/api/sessions/:sessionId/chat", async (req, res) => {
@@ -432,6 +613,75 @@ app.get("/api/sessions/:sessionId/chat", async (req, res) => {
432
613
  res.json(data);
433
614
  });
434
615
 
616
+ app.patch("/api/sessions/:sessionId", async (req, res) => {
617
+ const sessionId = req.params.sessionId;
618
+ if (!isSafeSessionId(sessionId)) {
619
+ res.status(400).json({ error: "Invalid session id." });
620
+ return;
621
+ }
622
+
623
+ const meta = db.getSession(sessionId);
624
+ if (!meta) {
625
+ res.status(404).json({ error: "Session not found." });
626
+ return;
627
+ }
628
+
629
+ const title = normalizeSessionTitle(req.body?.title);
630
+ if (!title) {
631
+ res.status(400).json({ error: "Title is required." });
632
+ return;
633
+ }
634
+
635
+ db.renameSession(sessionId, title);
636
+ await sessionStore(sessionId).appendEvent("session.renamed", { title }).catch(() => {});
637
+ res.json({ ok: true, session: db.getSession(sessionId) });
638
+ });
639
+
640
+ app.post("/api/sessions/:sessionId/auto-title", async (req, res) => {
641
+ const sessionId = req.params.sessionId;
642
+ if (!isSafeSessionId(sessionId)) {
643
+ res.status(400).json({ error: "Invalid session id." });
644
+ return;
645
+ }
646
+
647
+ const data = await loadChat(sessionId);
648
+ if (!data) {
649
+ res.status(404).json({ error: "Session not found." });
650
+ return;
651
+ }
652
+
653
+ const title = autoSessionTitleFromChat(data);
654
+ db.renameSession(sessionId, title);
655
+ await sessionStore(sessionId).appendEvent("session.auto_renamed", { title }).catch(() => {});
656
+ res.json({ ok: true, session: db.getSession(sessionId) });
657
+ });
658
+
659
+ app.delete("/api/sessions/:sessionId", async (req, res) => {
660
+ const sessionId = req.params.sessionId;
661
+ if (!isSafeSessionId(sessionId)) {
662
+ res.status(400).json({ error: "Invalid session id." });
663
+ return;
664
+ }
665
+
666
+ try {
667
+ await ensureNotRunning(sessionId);
668
+ } catch (error) {
669
+ res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
670
+ return;
671
+ }
672
+
673
+ const meta = db.getSession(sessionId);
674
+ if (!meta) {
675
+ res.status(404).json({ error: "Session not found." });
676
+ return;
677
+ }
678
+
679
+ runs.delete(sessionId);
680
+ db.deleteSession(sessionId);
681
+ await sessionStore(sessionId).remove();
682
+ res.json({ ok: true, sessionId });
683
+ });
684
+
435
685
  app.post("/api/runs", async (req, res) => {
436
686
  const body = req.body || {};
437
687
  const goal = String(body.goal || "").trim();
@@ -523,8 +773,9 @@ app.get("/health", (_req, res) => {
523
773
  res.json({ ok: true, port });
524
774
  });
525
775
 
776
+ await fs.mkdir(sessionsDir, { recursive: true });
526
777
  await syncStoredSessions();
527
778
 
528
- app.listen(port, () => {
529
- console.log(`Website control agent UI running on http://127.0.0.1:${port}`);
779
+ app.listen(port, host, () => {
780
+ console.log(`Website control agent UI running on http://${host}:${port}`);
530
781
  });