@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.
- package/README.md +9 -4
- package/docs/agent-runtime-pipe.md +5 -4
- package/docs/cli-i18n.md +1 -1
- package/docs/project-agent-lessons.md +1 -1
- package/docs/real-deepseek-capabilities.md +1 -1
- package/docs/self-development-supervision.md +1 -2
- package/docs/skills-and-tools.md +1 -1
- package/docs/supervised-capability-curriculum.md +111 -0
- package/package.json +9 -3
- package/public/app.js +4 -0
- package/references/backend-frontend-artifact-tunnel.md +3 -3
- package/references/general-task-capability-taxonomy.md +109 -0
- package/references/supervision-student-recursive-training.md +116 -0
- package/scripts/smoke-auto-update.js +96 -0
- package/scripts/smoke-auxiliary-tools.js +1 -0
- package/scripts/smoke-canvas-artifacts.js +67 -0
- package/scripts/smoke-capabilities.js +51 -2
- package/scripts/smoke-cli-chat.js +2 -0
- package/scripts/smoke-coding-tools.js +41 -0
- package/scripts/smoke-inbox.js +40 -1
- package/scripts/smoke-skills.js +53 -1
- package/scripts/smoke-toolchain-docker.js +1 -0
- package/scripts/smoke-web-api.js +6 -1
- package/skills/android/SKILL.md +2 -1
- package/skills/code-review/SKILL.md +24 -0
- package/skills/data-analysis/SKILL.md +29 -0
- package/skills/database/SKILL.md +27 -0
- package/skills/devops-deployment/SKILL.md +29 -0
- package/skills/docs-knowledge/SKILL.md +28 -0
- package/skills/dotnet-csharp/SKILL.md +24 -0
- package/skills/education-tutorial/SKILL.md +26 -0
- package/skills/go/SKILL.md +23 -0
- package/skills/ios-swift/SKILL.md +26 -0
- package/skills/java-jvm/SKILL.md +25 -0
- package/skills/php/SKILL.md +24 -0
- package/skills/presentation-slides/SKILL.md +26 -0
- package/skills/qa-testing/SKILL.md +27 -0
- package/skills/ruby/SKILL.md +24 -0
- package/skills/rust/SKILL.md +23 -0
- package/skills/security-review/SKILL.md +28 -0
- package/skills/supervision-student/SKILL.md +67 -0
- package/skills/writing-editing/SKILL.md +28 -0
- package/src/agent-runner.js +49 -7
- package/src/artifact-tunnel.js +75 -4
- package/src/auto-update.js +389 -0
- package/src/capabilities.js +6 -1
- package/src/cli.js +129 -14
- package/src/config.js +6 -2
- package/src/engineering-guidance.js +12 -2
- package/src/interactive-cli.js +42 -5
- package/src/model-client.js +2 -2
- package/src/model-routing.js +3 -1
- package/src/parallel-scouts.js +17 -1
- package/src/project.js +200 -20
- package/src/session-index.js +135 -0
- package/src/session-store.js +67 -3
- package/src/skill-library.js +16 -2
- package/src/sqlite.js +19 -0
- package/src/static-preview-server.js +8 -1
- package/src/task-profiles.js +318 -5
- package/src/web-db.js +58 -6
- package/src/workspace-tools.js +1 -0
- 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,
|
|
60
|
-
|
|
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
|
-
"-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
//
|
|
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 =
|
|
541
|
+
const paths = await ensureProjectSessionStorage(projectRoot);
|
|
420
542
|
const safeId = String(sessionId || "");
|
|
421
|
-
if (
|
|
543
|
+
if (!isSafeSessionId(safeId)) {
|
|
422
544
|
throw new Error("Invalid session id.");
|
|
423
545
|
}
|
|
424
|
-
const
|
|
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
|
+
}
|
package/src/session-store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/skill-library.js
CHANGED
|
@@ -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
|
|
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
|
|
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 (
|
|
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);
|