@lazyingart/agintiflow 0.20.25 → 0.20.39

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.
Files changed (63) hide show
  1. package/README.md +9 -4
  2. package/docs/agent-runtime-pipe.md +5 -4
  3. package/docs/cli-i18n.md +1 -1
  4. package/docs/project-agent-lessons.md +1 -1
  5. package/docs/real-deepseek-capabilities.md +1 -1
  6. package/docs/self-development-supervision.md +1 -2
  7. package/docs/skills-and-tools.md +1 -1
  8. package/docs/supervised-capability-curriculum.md +111 -0
  9. package/package.json +9 -3
  10. package/public/app.js +4 -0
  11. package/references/backend-frontend-artifact-tunnel.md +3 -3
  12. package/references/general-task-capability-taxonomy.md +109 -0
  13. package/references/supervision-student-recursive-training.md +116 -0
  14. package/scripts/smoke-auto-update.js +96 -0
  15. package/scripts/smoke-auxiliary-tools.js +1 -0
  16. package/scripts/smoke-canvas-artifacts.js +67 -0
  17. package/scripts/smoke-capabilities.js +51 -2
  18. package/scripts/smoke-cli-chat.js +2 -0
  19. package/scripts/smoke-coding-tools.js +41 -0
  20. package/scripts/smoke-inbox.js +40 -1
  21. package/scripts/smoke-skills.js +53 -1
  22. package/scripts/smoke-toolchain-docker.js +1 -0
  23. package/scripts/smoke-web-api.js +6 -1
  24. package/skills/android/SKILL.md +2 -1
  25. package/skills/code-review/SKILL.md +24 -0
  26. package/skills/data-analysis/SKILL.md +29 -0
  27. package/skills/database/SKILL.md +27 -0
  28. package/skills/devops-deployment/SKILL.md +29 -0
  29. package/skills/docs-knowledge/SKILL.md +28 -0
  30. package/skills/dotnet-csharp/SKILL.md +24 -0
  31. package/skills/education-tutorial/SKILL.md +26 -0
  32. package/skills/go/SKILL.md +23 -0
  33. package/skills/ios-swift/SKILL.md +26 -0
  34. package/skills/java-jvm/SKILL.md +25 -0
  35. package/skills/php/SKILL.md +24 -0
  36. package/skills/presentation-slides/SKILL.md +26 -0
  37. package/skills/qa-testing/SKILL.md +27 -0
  38. package/skills/ruby/SKILL.md +24 -0
  39. package/skills/rust/SKILL.md +23 -0
  40. package/skills/security-review/SKILL.md +28 -0
  41. package/skills/supervision-student/SKILL.md +67 -0
  42. package/skills/writing-editing/SKILL.md +28 -0
  43. package/src/agent-runner.js +49 -7
  44. package/src/artifact-tunnel.js +75 -4
  45. package/src/auto-update.js +389 -0
  46. package/src/capabilities.js +6 -1
  47. package/src/cli.js +129 -14
  48. package/src/config.js +6 -2
  49. package/src/engineering-guidance.js +12 -2
  50. package/src/interactive-cli.js +42 -5
  51. package/src/model-client.js +2 -2
  52. package/src/model-routing.js +3 -1
  53. package/src/parallel-scouts.js +17 -1
  54. package/src/project.js +200 -20
  55. package/src/session-index.js +135 -0
  56. package/src/session-store.js +67 -3
  57. package/src/skill-library.js +16 -2
  58. package/src/sqlite.js +19 -0
  59. package/src/static-preview-server.js +8 -1
  60. package/src/task-profiles.js +318 -5
  61. package/src/web-db.js +58 -6
  62. package/src/workspace-tools.js +1 -0
  63. package/web.js +37 -11
package/src/project.js CHANGED
@@ -6,6 +6,15 @@ 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 {
10
+ LEGACY_PROJECT_SESSIONS_DIR_NAME,
11
+ PROJECT_SESSIONS_DIR_NAME,
12
+ globalSessionPaths,
13
+ isSafeSessionId,
14
+ listSessionIndex,
15
+ renameSessionIndex,
16
+ upsertSessionIndex,
17
+ } from "./session-index.js";
9
18
 
10
19
  const execFileAsync = promisify(execFile);
11
20
  const LOCAL_ENV_KEYS = new Set([
@@ -45,6 +54,7 @@ export function resolveProjectRoot(input = process.cwd()) {
45
54
 
46
55
  export function projectPaths(projectRoot = process.cwd()) {
47
56
  const root = resolveProjectRoot(projectRoot);
57
+ const globalPaths = globalSessionPaths();
48
58
  return {
49
59
  root,
50
60
  agintiInstructionsPath: path.join(root, "AGINTI.md"),
@@ -56,12 +66,91 @@ export function projectPaths(projectRoot = process.cwd()) {
56
66
  codebaseMapPath: path.join(root, ".aginti", "codebase-map.json"),
57
67
  notesDir: path.join(root, "notes"),
58
68
  notesReadmePath: path.join(root, "notes", "README.md"),
59
- sessionsDir: path.join(root, ".sessions"),
60
- sessionDbPath: path.join(root, ".sessions", "web-state.sqlite"),
69
+ sessionsDir: path.join(root, PROJECT_SESSIONS_DIR_NAME),
70
+ legacySessionsDir: path.join(root, LEGACY_PROJECT_SESSIONS_DIR_NAME),
71
+ sessionDbPath: path.join(root, PROJECT_SESSIONS_DIR_NAME, "web-state.sqlite"),
72
+ legacySessionDbPath: path.join(root, LEGACY_PROJECT_SESSIONS_DIR_NAME, "web-state.sqlite"),
73
+ agintiflowHome: globalPaths.home,
74
+ globalSessionsDir: globalPaths.sessionsDir,
75
+ globalSessionIndexPath: globalPaths.indexDbPath,
61
76
  gitignorePath: path.join(root, ".gitignore"),
62
77
  };
63
78
  }
64
79
 
80
+ export function sessionStoreOptions(projectRoot = process.cwd(), sessionId = "") {
81
+ const paths = projectPaths(projectRoot);
82
+ return {
83
+ projectRoot: paths.root,
84
+ projectSessionsDir: paths.sessionsDir,
85
+ legacySessionDir: sessionId ? path.join(paths.legacySessionsDir, sessionId) : "",
86
+ };
87
+ }
88
+
89
+ export async function ensureProjectSessionStorage(projectRoot = process.cwd()) {
90
+ const paths = projectPaths(projectRoot);
91
+ await fsp.mkdir(paths.sessionsDir, { recursive: true });
92
+ await fsp.mkdir(paths.globalSessionsDir, { recursive: true });
93
+
94
+ const legacyEntries = await fsp.readdir(paths.legacySessionsDir, { withFileTypes: true }).catch(() => []);
95
+ if (legacyEntries.length > 0) {
96
+ for (const entry of legacyEntries) {
97
+ if (!entry.isDirectory() || !isSafeSessionId(entry.name)) continue;
98
+ const legacyDir = path.join(paths.legacySessionsDir, entry.name);
99
+ const legacyStatePath = path.join(legacyDir, "state.json");
100
+ const state = await fsp.readFile(legacyStatePath, "utf8").then(JSON.parse).catch(() => null);
101
+ if (!state?.sessionId && !state?.createdAt) continue;
102
+
103
+ const sessionId = isSafeSessionId(state.sessionId) ? state.sessionId : entry.name;
104
+ const globalDir = path.join(paths.globalSessionsDir, sessionId);
105
+ const globalStatePath = path.join(globalDir, "state.json");
106
+ const hasGlobalState = await fsp.stat(globalStatePath).then((stat) => stat.isFile()).catch(() => false);
107
+ if (!hasGlobalState) {
108
+ await fsp.mkdir(path.dirname(globalDir), { recursive: true });
109
+ await fsp.cp(legacyDir, globalDir, { recursive: true, force: false, errorOnExist: false });
110
+ }
111
+
112
+ const pointerDir = path.join(paths.sessionsDir, sessionId);
113
+ await fsp.mkdir(pointerDir, { recursive: true });
114
+ const pointer = {
115
+ sessionId,
116
+ projectRoot: paths.root,
117
+ commandCwd: state.commandCwd || paths.root,
118
+ sessionDir: globalDir,
119
+ artifactsDir: path.join(globalDir, "artifacts"),
120
+ createdAt: state.createdAt || state.startedAt || "",
121
+ updatedAt: state.updatedAt || state.createdAt || "",
122
+ title: state.title || "",
123
+ goal: state.goal || "",
124
+ provider: state.provider || "",
125
+ model: state.model || "",
126
+ migratedFrom: legacyDir,
127
+ };
128
+ await fsp.writeFile(path.join(pointerDir, "session.json"), `${JSON.stringify(pointer, null, 2)}\n`, "utf8");
129
+ try {
130
+ upsertSessionIndex({
131
+ ...state,
132
+ sessionId,
133
+ projectRoot: paths.root,
134
+ commandCwd: state.commandCwd || paths.root,
135
+ projectSessionsDir: paths.sessionsDir,
136
+ sessionDir: globalDir,
137
+ status: state.status || "saved",
138
+ });
139
+ } catch {
140
+ // Migration should still leave readable pointers even if the global index is unavailable.
141
+ }
142
+ }
143
+ }
144
+
145
+ const hasNewDb = await fsp.stat(paths.sessionDbPath).then((stat) => stat.isFile()).catch(() => false);
146
+ const hasLegacyDb = await fsp.stat(paths.legacySessionDbPath).then((stat) => stat.isFile()).catch(() => false);
147
+ if (!hasNewDb && hasLegacyDb) {
148
+ await fsp.copyFile(paths.legacySessionDbPath, paths.sessionDbPath).catch(() => {});
149
+ }
150
+
151
+ return paths;
152
+ }
153
+
65
154
  export function defaultAgintiInstructions() {
66
155
  return [
67
156
  "# AGINTI.md",
@@ -202,6 +291,7 @@ export async function initProject(projectRoot = process.cwd()) {
202
291
  await ensureDir(paths.controlDir);
203
292
  await ensureDir(paths.notesDir);
204
293
  await ensureDir(paths.sessionsDir);
294
+ await fsp.mkdir(paths.globalSessionsDir, { recursive: true });
205
295
  await ensureFile(
206
296
  paths.agintiInstructionsPath,
207
297
  defaultAgintiInstructions()
@@ -217,7 +307,8 @@ export async function initProject(projectRoot = process.cwd()) {
217
307
  "- `.env` is ignored and can hold local provider keys.",
218
308
  "- `.env.example` documents accepted variable names.",
219
309
  "- `codebase-map.json` is a generated, ignored project-intelligence cache.",
220
- "- `.sessions/` at the project root stores CLI and web run history.",
310
+ "- `../.aginti-sessions/` stores project-local session pointers and the web UI database.",
311
+ "- `~/.agintiflow/sessions/<session-id>/` stores canonical session history and artifacts.",
221
312
  "",
222
313
  ].join("\n")
223
314
  );
@@ -256,11 +347,14 @@ export async function initProject(projectRoot = process.cwd()) {
256
347
  ".aginti/.env.*",
257
348
  "!.aginti/.env.example",
258
349
  ".aginti/codebase-map.json",
350
+ ".aginti-sessions/",
259
351
  ".sessions/",
260
352
  ]);
261
353
  if (gitignore.changed) updated.push(paths.gitignorePath);
262
354
  else skipped.push(paths.gitignorePath);
263
355
 
356
+ await ensureProjectSessionStorage(projectRoot);
357
+
264
358
  return {
265
359
  ok: true,
266
360
  projectRoot: paths.root,
@@ -391,37 +485,66 @@ export async function setProviderKey(projectRoot, provider, value) {
391
485
  }
392
486
 
393
487
  export async function listProjectSessions(projectRoot = process.cwd(), limit = 50) {
394
- const paths = projectPaths(projectRoot);
488
+ const paths = await ensureProjectSessionStorage(projectRoot);
489
+ const indexed = (() => {
490
+ try {
491
+ return listSessionIndex({ projectRoot: paths.root, limit: Math.max(limit, 100) });
492
+ } catch {
493
+ return [];
494
+ }
495
+ })();
496
+ const byId = new Map();
497
+ for (const session of indexed) {
498
+ if (session.sessionId) byId.set(session.sessionId, session);
499
+ }
500
+
395
501
  const entries = await fsp.readdir(paths.sessionsDir, { withFileTypes: true }).catch(() => []);
396
- const sessions = [];
397
502
  for (const entry of entries) {
398
- if (!entry.isDirectory()) continue;
399
- const statePath = path.join(paths.sessionsDir, entry.name, "state.json");
503
+ if (!entry.isDirectory() || !isSafeSessionId(entry.name)) continue;
504
+ const pointerPath = path.join(paths.sessionsDir, entry.name, "session.json");
505
+ const pointer = await fsp.readFile(pointerPath, "utf8").then(JSON.parse).catch(() => ({}));
506
+ const statePath = path.join(pointer.sessionDir || path.join(paths.globalSessionsDir, entry.name), "state.json");
507
+ const state = await fsp.readFile(statePath, "utf8").then(JSON.parse).catch(() => null);
508
+ const sessionId = state?.sessionId || pointer.sessionId || entry.name;
509
+ if (!sessionId) continue;
510
+ const record = {
511
+ ...byId.get(sessionId),
512
+ sessionId,
513
+ projectRoot: paths.root,
514
+ commandCwd: state?.commandCwd || pointer.commandCwd || paths.root,
515
+ sessionDir: pointer.sessionDir || path.join(paths.globalSessionsDir, sessionId),
516
+ provider: state?.provider || pointer.provider || byId.get(sessionId)?.provider || "",
517
+ model: state?.model || pointer.model || byId.get(sessionId)?.model || "",
518
+ goal: state?.goal || pointer.goal || byId.get(sessionId)?.goal || "",
519
+ title: state?.title || pointer.title || byId.get(sessionId)?.title || "",
520
+ createdAt: state?.createdAt || pointer.createdAt || byId.get(sessionId)?.createdAt || "",
521
+ updatedAt: state?.updatedAt || pointer.updatedAt || byId.get(sessionId)?.updatedAt || state?.createdAt || "",
522
+ stepsCompleted: state?.stepsCompleted || 0,
523
+ };
524
+ byId.set(sessionId, record);
400
525
  try {
401
- const state = JSON.parse(await fsp.readFile(statePath, "utf8"));
402
- sessions.push({
403
- sessionId: state.sessionId || entry.name,
404
- provider: state.provider || "",
405
- model: state.model || "",
406
- goal: state.goal || "",
407
- createdAt: state.createdAt || "",
408
- updatedAt: state.updatedAt || state.createdAt || "",
409
- stepsCompleted: state.stepsCompleted || 0,
526
+ upsertSessionIndex({
527
+ ...record,
528
+ projectSessionsDir: paths.sessionsDir,
529
+ status: record.status || "saved",
410
530
  });
411
531
  } catch {
412
- // Ignore malformed or unrelated session folders.
532
+ // Project pointer scanning should not fail when the optional global index is unavailable.
413
533
  }
414
534
  }
535
+
536
+ const sessions = [...byId.values()];
415
537
  return sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))).slice(0, limit);
416
538
  }
417
539
 
418
540
  export async function showProjectSession(projectRoot, sessionId) {
419
- const paths = projectPaths(projectRoot);
541
+ const paths = await ensureProjectSessionStorage(projectRoot);
420
542
  const safeId = String(sessionId || "");
421
- if (!/^[A-Za-z0-9._:-]+$/.test(safeId) || safeId.includes("..")) {
543
+ if (!isSafeSessionId(safeId)) {
422
544
  throw new Error("Invalid session id.");
423
545
  }
424
- const sessionDir = path.join(paths.sessionsDir, safeId);
546
+ const pointer = await fsp.readFile(path.join(paths.sessionsDir, safeId, "session.json"), "utf8").then(JSON.parse).catch(() => ({}));
547
+ const sessionDir = pointer.sessionDir || path.join(paths.globalSessionsDir, safeId);
425
548
  const state = JSON.parse(await fsp.readFile(path.join(sessionDir, "state.json"), "utf8"));
426
549
  let events = [];
427
550
  try {
@@ -438,6 +561,8 @@ export async function showProjectSession(projectRoot, sessionId) {
438
561
  provider: state.provider || "",
439
562
  model: state.model || "",
440
563
  goal: state.goal || "",
564
+ title: state.title || pointer.title || "",
565
+ commandCwd: state.commandCwd || pointer.commandCwd || paths.root,
441
566
  createdAt: state.createdAt || "",
442
567
  updatedAt: state.updatedAt || "",
443
568
  chat: state.chat || [],
@@ -445,6 +570,59 @@ export async function showProjectSession(projectRoot, sessionId) {
445
570
  };
446
571
  }
447
572
 
573
+ export async function renameProjectSession(projectRoot, sessionId, title) {
574
+ const paths = await ensureProjectSessionStorage(projectRoot);
575
+ const safeId = String(sessionId || "");
576
+ if (!isSafeSessionId(safeId)) throw new Error("Invalid session id.");
577
+ const cleanTitle = String(title || "").replace(/\s+/g, " ").trim().slice(0, 90);
578
+ if (!cleanTitle) throw new Error("Title is required.");
579
+ const pointerPath = path.join(paths.sessionsDir, safeId, "session.json");
580
+ const pointer = await fsp.readFile(pointerPath, "utf8").then(JSON.parse).catch(() => ({}));
581
+ const sessionDir = pointer.sessionDir || path.join(paths.globalSessionsDir, safeId);
582
+ const statePath = path.join(sessionDir, "state.json");
583
+ const state = await fsp.readFile(statePath, "utf8").then(JSON.parse).catch(() => ({}));
584
+ const updatedAt = new Date().toISOString();
585
+ const nextState = {
586
+ ...state,
587
+ sessionId: state.sessionId || safeId,
588
+ title: cleanTitle,
589
+ updatedAt,
590
+ };
591
+ await fsp.mkdir(sessionDir, { recursive: true });
592
+ await fsp.writeFile(statePath, `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
593
+ const nextPointer = {
594
+ ...pointer,
595
+ sessionId: safeId,
596
+ projectRoot: paths.root,
597
+ commandCwd: nextState.commandCwd || pointer.commandCwd || paths.root,
598
+ sessionDir,
599
+ artifactsDir: path.join(sessionDir, "artifacts"),
600
+ title: cleanTitle,
601
+ updatedAt,
602
+ goal: nextState.goal || pointer.goal || "",
603
+ provider: nextState.provider || pointer.provider || "",
604
+ model: nextState.model || pointer.model || "",
605
+ };
606
+ await fsp.mkdir(path.dirname(pointerPath), { recursive: true });
607
+ await fsp.writeFile(pointerPath, `${JSON.stringify(nextPointer, null, 2)}\n`, "utf8");
608
+ try {
609
+ renameSessionIndex(safeId, cleanTitle);
610
+ upsertSessionIndex({
611
+ ...nextState,
612
+ sessionId: safeId,
613
+ title: cleanTitle,
614
+ projectRoot: paths.root,
615
+ commandCwd: nextState.commandCwd || pointer.commandCwd || paths.root,
616
+ projectSessionsDir: paths.sessionsDir,
617
+ sessionDir,
618
+ status: nextState.status || "saved",
619
+ });
620
+ } catch {
621
+ // The title is still persisted in state.json and the project pointer.
622
+ }
623
+ return { ok: true, sessionId: safeId, title: cleanTitle, sessionDir };
624
+ }
625
+
448
626
  export async function npmLatestVersion(packageName = "@lazyingart/agintiflow") {
449
627
  try {
450
628
  const { stdout } = await execFileAsync("npm", ["view", packageName, "version", "--json"], {
@@ -492,6 +670,8 @@ export async function doctorReport(projectRoot, packageVersion, config) {
492
670
  controlDir: paths.controlDir,
493
671
  sessionsDir: paths.sessionsDir,
494
672
  sessionDbPath: paths.sessionDbPath,
673
+ globalSessionsDir: paths.globalSessionsDir,
674
+ globalSessionIndexPath: paths.globalSessionIndexPath,
495
675
  localEnvPresent: keyStatus.localEnv,
496
676
  },
497
677
  keys: {
@@ -0,0 +1,135 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { loadDatabaseSync } from "./sqlite.js";
5
+
6
+ export const PROJECT_SESSIONS_DIR_NAME = ".aginti-sessions";
7
+ export const LEGACY_PROJECT_SESSIONS_DIR_NAME = ".sessions";
8
+
9
+ export function agintiflowHome() {
10
+ return path.resolve(process.env.AGINTIFLOW_HOME || path.join(os.homedir(), ".agintiflow"));
11
+ }
12
+
13
+ export function globalSessionPaths(sessionId = "") {
14
+ const home = agintiflowHome();
15
+ const sessionsDir = path.join(home, "sessions");
16
+ return {
17
+ home,
18
+ sessionsDir,
19
+ indexDbPath: path.join(sessionsDir, "index.sqlite"),
20
+ sessionDir: sessionId ? path.join(sessionsDir, sessionId) : "",
21
+ };
22
+ }
23
+
24
+ export function isSafeSessionId(sessionId) {
25
+ const text = String(sessionId || "");
26
+ return /^[A-Za-z0-9._:-]+$/.test(text) && !text.includes("..");
27
+ }
28
+
29
+ function ensureIndexDb() {
30
+ const paths = globalSessionPaths();
31
+ fs.mkdirSync(paths.sessionsDir, { recursive: true });
32
+ const DatabaseSync = loadDatabaseSync();
33
+ const db = new DatabaseSync(paths.indexDbPath);
34
+ db.exec(`
35
+ CREATE TABLE IF NOT EXISTS sessions (
36
+ session_id TEXT PRIMARY KEY,
37
+ project_root TEXT NOT NULL DEFAULT '',
38
+ command_cwd TEXT NOT NULL DEFAULT '',
39
+ project_sessions_dir TEXT NOT NULL DEFAULT '',
40
+ session_dir TEXT NOT NULL DEFAULT '',
41
+ provider TEXT NOT NULL DEFAULT '',
42
+ model TEXT NOT NULL DEFAULT '',
43
+ goal TEXT NOT NULL DEFAULT '',
44
+ title TEXT NOT NULL DEFAULT '',
45
+ status TEXT NOT NULL DEFAULT '',
46
+ created_at TEXT NOT NULL DEFAULT '',
47
+ updated_at TEXT NOT NULL DEFAULT '',
48
+ ended_at TEXT,
49
+ result TEXT NOT NULL DEFAULT '',
50
+ error TEXT NOT NULL DEFAULT ''
51
+ );
52
+ `);
53
+ const existingColumns = db.prepare("PRAGMA table_info(sessions)").all();
54
+ if (!existingColumns.some((column) => column.name === "command_cwd")) {
55
+ db.exec("ALTER TABLE sessions ADD COLUMN command_cwd TEXT NOT NULL DEFAULT ''");
56
+ }
57
+ return db;
58
+ }
59
+
60
+ export function upsertSessionIndex(record = {}) {
61
+ const sessionId = String(record.sessionId || record.session_id || "").trim();
62
+ if (!isSafeSessionId(sessionId)) return false;
63
+ const now = new Date().toISOString();
64
+ const db = ensureIndexDb();
65
+ 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
+ db.prepare(
69
+ `INSERT INTO sessions (
70
+ session_id, project_root, command_cwd, project_sessions_dir, session_dir,
71
+ provider, model, goal, title, status, created_at, updated_at, ended_at, result, error
72
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
73
+ ON CONFLICT(session_id) DO UPDATE SET
74
+ project_root = CASE WHEN excluded.project_root != '' THEN excluded.project_root ELSE sessions.project_root END,
75
+ command_cwd = CASE WHEN excluded.command_cwd != '' THEN excluded.command_cwd ELSE sessions.command_cwd END,
76
+ project_sessions_dir = CASE WHEN excluded.project_sessions_dir != '' THEN excluded.project_sessions_dir ELSE sessions.project_sessions_dir END,
77
+ session_dir = CASE WHEN excluded.session_dir != '' THEN excluded.session_dir ELSE sessions.session_dir END,
78
+ provider = CASE WHEN excluded.provider != '' THEN excluded.provider ELSE sessions.provider END,
79
+ model = CASE WHEN excluded.model != '' THEN excluded.model ELSE sessions.model END,
80
+ goal = CASE WHEN excluded.goal != '' THEN excluded.goal ELSE sessions.goal END,
81
+ title = CASE WHEN excluded.title != '' THEN excluded.title ELSE sessions.title END,
82
+ status = CASE WHEN excluded.status != '' THEN excluded.status ELSE sessions.status END,
83
+ updated_at = excluded.updated_at,
84
+ ended_at = excluded.ended_at,
85
+ result = CASE WHEN excluded.result != '' THEN excluded.result ELSE sessions.result END,
86
+ error = CASE WHEN excluded.error != '' THEN excluded.error ELSE sessions.error END`
87
+ ).run(
88
+ 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 || "")
103
+ );
104
+ return true;
105
+ }
106
+
107
+ export function renameSessionIndex(sessionId, title) {
108
+ if (!isSafeSessionId(sessionId)) return false;
109
+ const db = ensureIndexDb();
110
+ const result = db
111
+ .prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE session_id = ?")
112
+ .run(String(title || "").trim(), new Date().toISOString(), sessionId);
113
+ return result.changes > 0;
114
+ }
115
+
116
+ export function deleteSessionIndex(sessionId) {
117
+ if (!isSafeSessionId(sessionId)) return false;
118
+ const db = ensureIndexDb();
119
+ const result = db.prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
120
+ return result.changes > 0;
121
+ }
122
+
123
+ export function listSessionIndex({ projectRoot = "", limit = 100 } = {}) {
124
+ const db = ensureIndexDb();
125
+ const maxRows = Math.min(Math.max(Number(limit) || 100, 1), 1000);
126
+ const columns = `session_id AS sessionId, project_root AS projectRoot, command_cwd AS commandCwd, project_sessions_dir AS projectSessionsDir,
127
+ session_dir AS sessionDir, provider, model, goal, title, status,
128
+ created_at AS createdAt, updated_at AS updatedAt, ended_at AS endedAt, result, error`;
129
+ if (projectRoot) {
130
+ return db
131
+ .prepare(`SELECT ${columns} FROM sessions WHERE project_root = ? ORDER BY updated_at DESC LIMIT ?`)
132
+ .all(path.resolve(projectRoot), maxRows);
133
+ }
134
+ return db.prepare(`SELECT ${columns} FROM sessions ORDER BY updated_at DESC LIMIT ?`).all(maxRows);
135
+ }
@@ -1,22 +1,51 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
+ import { deleteSessionIndex, globalSessionPaths, isSafeSessionId, upsertSessionIndex } from "./session-index.js";
4
5
 
5
6
  export class SessionStore {
6
- constructor(baseDir, sessionId) {
7
- this.baseDir = baseDir;
7
+ constructor(baseDir, sessionId, options = {}) {
8
+ this.baseDir = path.resolve(baseDir);
8
9
  this.sessionId = sessionId;
9
- this.sessionDir = path.join(baseDir, sessionId);
10
+ const globalPaths = globalSessionPaths(sessionId);
11
+ this.projectRoot = options.projectRoot ? path.resolve(options.projectRoot) : "";
12
+ this.projectSessionsDir = options.projectSessionsDir ? path.resolve(options.projectSessionsDir) : "";
13
+ this.legacySessionDir = options.legacySessionDir ? path.resolve(options.legacySessionDir) : "";
14
+ this.sessionDir = path.resolve(options.sessionDir || (baseDir ? path.join(this.baseDir, sessionId) : globalPaths.sessionDir));
10
15
  this.artifactsDir = path.join(this.sessionDir, "artifacts");
11
16
  this.statePath = path.join(this.sessionDir, "state.json");
12
17
  this.planPath = path.join(this.sessionDir, "plan.md");
13
18
  this.eventsPath = path.join(this.sessionDir, "events.jsonl");
14
19
  this.inboxPath = path.join(this.sessionDir, "inbox.jsonl");
15
20
  this.storageStatePath = path.join(this.sessionDir, "storage-state.json");
21
+ this.pointerDir = this.projectSessionsDir && isSafeSessionId(sessionId) ? path.join(this.projectSessionsDir, sessionId) : "";
22
+ this.pointerPath = this.pointerDir ? path.join(this.pointerDir, "session.json") : "";
16
23
  }
17
24
 
18
25
  async ensure() {
19
26
  await fs.mkdir(this.artifactsDir, { recursive: true });
27
+ await this.writePointer().catch(() => {});
28
+ }
29
+
30
+ async writePointer(state = {}) {
31
+ if (!this.pointerPath) return;
32
+ await fs.mkdir(this.pointerDir, { recursive: true });
33
+ const existing = await fs.readFile(this.pointerPath, "utf8").then(JSON.parse).catch(() => ({}));
34
+ const now = new Date().toISOString();
35
+ const payload = {
36
+ sessionId: this.sessionId,
37
+ projectRoot: this.projectRoot,
38
+ commandCwd: state.commandCwd || existing.commandCwd || this.projectRoot,
39
+ sessionDir: this.sessionDir,
40
+ artifactsDir: this.artifactsDir,
41
+ createdAt: state.createdAt || state.startedAt || existing.createdAt || now,
42
+ updatedAt: state.updatedAt || existing.updatedAt || now,
43
+ title: state.title || existing.title || "",
44
+ goal: state.goal || existing.goal || "",
45
+ provider: state.provider || existing.provider || "",
46
+ model: state.model || existing.model || "",
47
+ };
48
+ await fs.writeFile(this.pointerPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
20
49
  }
21
50
 
22
51
  async loadState() {
@@ -24,6 +53,14 @@ export class SessionStore {
24
53
  const raw = await fs.readFile(this.statePath, "utf8");
25
54
  return JSON.parse(raw);
26
55
  } catch {
56
+ if (this.legacySessionDir) {
57
+ try {
58
+ const raw = await fs.readFile(path.join(this.legacySessionDir, "state.json"), "utf8");
59
+ return JSON.parse(raw);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
27
64
  return null;
28
65
  }
29
66
  }
@@ -31,6 +68,19 @@ export class SessionStore {
31
68
  async saveState(state) {
32
69
  await this.ensure();
33
70
  await fs.writeFile(this.statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
71
+ await this.writePointer(state).catch(() => {});
72
+ try {
73
+ upsertSessionIndex({
74
+ ...state,
75
+ sessionId: this.sessionId,
76
+ projectRoot: this.projectRoot,
77
+ projectSessionsDir: this.projectSessionsDir,
78
+ sessionDir: this.sessionDir,
79
+ status: state.status || "saved",
80
+ });
81
+ } catch {
82
+ // Session state remains durable even if the optional global index is unavailable.
83
+ }
34
84
  }
35
85
 
36
86
  async savePlan(planText) {
@@ -66,6 +116,18 @@ export class SessionStore {
66
116
  .filter(Boolean)
67
117
  .map((line) => JSON.parse(line));
68
118
  } catch {
119
+ if (this.legacySessionDir) {
120
+ try {
121
+ const raw = await fs.readFile(path.join(this.legacySessionDir, "events.jsonl"), "utf8");
122
+ return raw
123
+ .split("\n")
124
+ .map((line) => line.trim())
125
+ .filter(Boolean)
126
+ .map((line) => JSON.parse(line));
127
+ } catch {
128
+ return [];
129
+ }
130
+ }
69
131
  return [];
70
132
  }
71
133
  }
@@ -131,5 +193,7 @@ export class SessionStore {
131
193
 
132
194
  async remove() {
133
195
  await fs.rm(this.sessionDir, { recursive: true, force: true });
196
+ if (this.pointerDir) await fs.rm(this.pointerDir, { recursive: true, force: true }).catch(() => {});
197
+ deleteSessionIndex(this.sessionId);
134
198
  }
135
199
  }
@@ -102,14 +102,28 @@ function scoreSkill(skill, text, taskProfile) {
102
102
  if (skill.triggers.includes(taskProfile)) score += 6;
103
103
  for (const trigger of skill.triggers) {
104
104
  const needle = trigger.toLowerCase();
105
- if (needle && text.includes(needle)) score += Math.max(2, Math.min(6, Math.ceil(needle.length / 6)));
105
+ if (needle && textHasTrigger(text, needle)) score += Math.max(2, Math.min(6, Math.ceil(needle.length / 6)));
106
106
  }
107
107
  for (const token of skill.description.toLowerCase().split(/[^a-z0-9+#.-]+/).filter((item) => item.length > 3)) {
108
- if (text.includes(token)) score += 0.25;
108
+ if (textHasTrigger(text, token)) score += 0.25;
109
109
  }
110
110
  return score;
111
111
  }
112
112
 
113
+ function escapeRegExp(value) {
114
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
115
+ }
116
+
117
+ function textHasTrigger(text, needle) {
118
+ if (!needle) return false;
119
+ if (/^[a-z0-9]+(?:[\s_-]+[a-z0-9]+)*$/.test(needle)) {
120
+ const parts = needle.split(/[\s_-]+/).filter(Boolean).map(escapeRegExp);
121
+ if (parts.length === 0) return false;
122
+ return new RegExp(`(^|[^a-z0-9])${parts.join("[^a-z0-9]+")}([^a-z0-9]|$)`).test(text);
123
+ }
124
+ return text.includes(needle);
125
+ }
126
+
113
127
  export function selectSkillsForGoal(goal = "", { taskProfile = "auto", limit = 6, includeBody = true } = {}) {
114
128
  const text = `${goal} ${taskProfile}`.toLowerCase();
115
129
  const skills = listSkills({ includeBody });
package/src/sqlite.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ let cachedDatabaseSync = null;
5
+
6
+ export function loadDatabaseSync() {
7
+ if (cachedDatabaseSync) return cachedDatabaseSync;
8
+ const originalEmitWarning = process.emitWarning;
9
+ process.emitWarning = function filteredSqliteWarning(warning, ...args) {
10
+ if (String(warning || "").includes("SQLite is an experimental feature")) return;
11
+ return originalEmitWarning.call(process, warning, ...args);
12
+ };
13
+ try {
14
+ cachedDatabaseSync = require("node:sqlite").DatabaseSync;
15
+ return cachedDatabaseSync;
16
+ } finally {
17
+ process.emitWarning = originalEmitWarning;
18
+ }
19
+ }
@@ -48,7 +48,14 @@ function isBlockedPath(relativePath) {
48
48
  const segments = relativePath.split(path.sep).filter(Boolean);
49
49
  const lowerBase = (segments.at(-1) || "").toLowerCase();
50
50
  const lowerPath = segments.join("/").toLowerCase();
51
- if (segments.includes(".git") || segments.includes("node_modules") || segments.includes(".sessions")) return true;
51
+ if (
52
+ segments.includes(".git") ||
53
+ segments.includes("node_modules") ||
54
+ segments.includes(".sessions") ||
55
+ segments.includes(".aginti-sessions")
56
+ ) {
57
+ return true;
58
+ }
52
59
  if (lowerBase === ".env" || lowerBase.startsWith(".env.")) return true;
53
60
  if (lowerBase === ".npmrc" || lowerBase === ".pypirc") return true;
54
61
  return /(^|\/)(secrets?|tokens?|passwords?|private[-_]?keys?|credentials?)(\/|\.|$)/i.test(lowerPath);