@lazyingart/agintiflow 0.20.43 → 0.20.44
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 +4 -0
- package/package.json +1 -1
- package/scripts/smoke-inbox.js +51 -1
- package/src/cli.js +220 -2
- package/src/project.js +113 -0
package/README.md
CHANGED
|
@@ -138,6 +138,8 @@ aginti update
|
|
|
138
138
|
aginti sessions list
|
|
139
139
|
aginti sessions show <session-id>
|
|
140
140
|
aginti sessions rename <session-id> "friendly title"
|
|
141
|
+
aginti --remove-empty-sessions
|
|
142
|
+
aginti --remove-sessions
|
|
141
143
|
aginti storage migrate
|
|
142
144
|
aginti resume
|
|
143
145
|
aginti resume --all-sessions
|
|
@@ -151,6 +153,8 @@ aginti "set up this project and run the tests"
|
|
|
151
153
|
|
|
152
154
|
Bare `aginti resume` lists sessions for the current cwd by default. Use `--all-sessions` to browse the global session index; in the interactive selector, type a number to resume, `q` to quit, `/text` to filter the visible list, or `/` to clear the filter.
|
|
153
155
|
|
|
156
|
+
Session cleanup is cwd-scoped by default. `aginti --remove-empty-sessions` shows only empty sessions and preselects them; `aginti --remove-sessions` shows all cwd sessions with nothing preselected. The cleanup selector uses Space to select, Up/Down to move, Tab to switch to OK/Cancel, and a second Yes/Cancel confirmation before deleting the project pointer and central `~/.agintiflow/sessions/<session-id>` data.
|
|
157
|
+
|
|
154
158
|
Run from a source checkout:
|
|
155
159
|
|
|
156
160
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.44",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a web-first coding agent and CLI with DeepSeek routing, sandboxed tools, model providers, canvas artifacts, and optional wrappers.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/scripts/smoke-inbox.js
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ensureProjectSessionStorage,
|
|
7
|
+
listProjectSessionRemovalCandidates,
|
|
8
|
+
listProjectSessions,
|
|
9
|
+
removeProjectSessions,
|
|
10
|
+
sessionStoreOptions,
|
|
11
|
+
} from "../src/project.js";
|
|
6
12
|
import { SessionStore } from "../src/session-store.js";
|
|
7
13
|
|
|
8
14
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-inbox-"));
|
|
@@ -77,6 +83,48 @@ try {
|
|
|
77
83
|
const allSessions = await listProjectSessions(legacyProject, { limit: 10, allSessions: true });
|
|
78
84
|
assert(allSessions.some((session) => session.sessionId === "other-cwd-smoke"), "--all-sessions mode did not include a different cwd session");
|
|
79
85
|
|
|
86
|
+
const emptyStore = new SessionStore(paths.globalSessionsDir, "empty-session-smoke", sessionStoreOptions(legacyProject, "empty-session-smoke"));
|
|
87
|
+
await emptyStore.saveState({
|
|
88
|
+
sessionId: "empty-session-smoke",
|
|
89
|
+
createdAt: "2026-01-01T00:04:00.000Z",
|
|
90
|
+
updatedAt: "2026-01-01T00:04:00.000Z",
|
|
91
|
+
provider: "mock",
|
|
92
|
+
model: "mock-agent",
|
|
93
|
+
projectRoot: legacyProject,
|
|
94
|
+
commandCwd: legacyProject,
|
|
95
|
+
chat: [],
|
|
96
|
+
stepsCompleted: 0,
|
|
97
|
+
});
|
|
98
|
+
const nonEmptyStore = new SessionStore(paths.globalSessionsDir, "nonempty-session-smoke", sessionStoreOptions(legacyProject, "nonempty-session-smoke"));
|
|
99
|
+
await nonEmptyStore.saveState({
|
|
100
|
+
sessionId: "nonempty-session-smoke",
|
|
101
|
+
createdAt: "2026-01-01T00:05:00.000Z",
|
|
102
|
+
updatedAt: "2026-01-01T00:05:00.000Z",
|
|
103
|
+
provider: "mock",
|
|
104
|
+
model: "mock-agent",
|
|
105
|
+
goal: "keep this non-empty session",
|
|
106
|
+
projectRoot: legacyProject,
|
|
107
|
+
commandCwd: legacyProject,
|
|
108
|
+
chat: [{ role: "user", content: "hello" }],
|
|
109
|
+
stepsCompleted: 1,
|
|
110
|
+
});
|
|
111
|
+
const removalCandidates = await listProjectSessionRemovalCandidates(legacyProject, { limit: 20, commandCwd: legacyProject });
|
|
112
|
+
assert(removalCandidates.find((session) => session.sessionId === "empty-session-smoke")?.isEmpty, "empty session was not detected");
|
|
113
|
+
assert(removalCandidates.find((session) => session.sessionId === "nonempty-session-smoke")?.isEmpty === false, "non-empty session was classified as empty");
|
|
114
|
+
const emptyOnly = await listProjectSessionRemovalCandidates(legacyProject, { limit: 20, commandCwd: legacyProject, emptyOnly: true });
|
|
115
|
+
assert(emptyOnly.some((session) => session.sessionId === "empty-session-smoke"), "empty-only removal list omitted the empty session");
|
|
116
|
+
assert(!emptyOnly.some((session) => session.sessionId === "nonempty-session-smoke"), "empty-only removal list included a non-empty session");
|
|
117
|
+
const removed = await removeProjectSessions(legacyProject, ["empty-session-smoke"]);
|
|
118
|
+
assert(removed.removed.length === 1, "empty session removal did not report one removed session");
|
|
119
|
+
assert(
|
|
120
|
+
!(await fs.stat(path.join(paths.globalSessionsDir, "empty-session-smoke", "state.json")).then((stat) => stat.isFile()).catch(() => false)),
|
|
121
|
+
"empty session global state was not removed"
|
|
122
|
+
);
|
|
123
|
+
assert(
|
|
124
|
+
!(await fs.stat(path.join(paths.sessionsDir, "empty-session-smoke", "session.json")).then((stat) => stat.isFile()).catch(() => false)),
|
|
125
|
+
"empty session project pointer was not removed"
|
|
126
|
+
);
|
|
127
|
+
|
|
80
128
|
console.log(
|
|
81
129
|
JSON.stringify(
|
|
82
130
|
{
|
|
@@ -89,6 +137,8 @@ try {
|
|
|
89
137
|
"global-session-store",
|
|
90
138
|
"cwd-session-filter",
|
|
91
139
|
"all-sessions-list",
|
|
140
|
+
"empty-session-detection",
|
|
141
|
+
"empty-session-removal",
|
|
92
142
|
],
|
|
93
143
|
},
|
|
94
144
|
null,
|
package/src/cli.js
CHANGED
|
@@ -17,8 +17,10 @@ import {
|
|
|
17
17
|
doctorReport,
|
|
18
18
|
ensureProjectSessionStorage,
|
|
19
19
|
initProject,
|
|
20
|
+
listProjectSessionRemovalCandidates,
|
|
20
21
|
listProjectSessions,
|
|
21
22
|
renameProjectSession,
|
|
23
|
+
removeProjectSessions,
|
|
22
24
|
providerKeyStatus,
|
|
23
25
|
setProviderKey,
|
|
24
26
|
showProjectSession,
|
|
@@ -34,6 +36,7 @@ import fs from "node:fs/promises";
|
|
|
34
36
|
import path from "node:path";
|
|
35
37
|
import { fileURLToPath } from "node:url";
|
|
36
38
|
import readline from "node:readline/promises";
|
|
39
|
+
import * as readlineRaw from "node:readline";
|
|
37
40
|
|
|
38
41
|
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
39
42
|
const packageJson = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf8"));
|
|
@@ -378,7 +381,7 @@ export function parseArgs(argv) {
|
|
|
378
381
|
|
|
379
382
|
function printUsage() {
|
|
380
383
|
console.log(
|
|
381
|
-
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
|
|
384
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
|
|
382
385
|
);
|
|
383
386
|
console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
|
|
384
387
|
}
|
|
@@ -600,9 +603,214 @@ function printAuthWizardResult(result) {
|
|
|
600
603
|
}
|
|
601
604
|
}
|
|
602
605
|
|
|
606
|
+
const removeSessionAnsi = {
|
|
607
|
+
reset: "\x1b[0m",
|
|
608
|
+
bold: "\x1b[1m",
|
|
609
|
+
dim: "\x1b[2m",
|
|
610
|
+
inverse: "\x1b[7m",
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
function removeSessionColor(text, ...codes) {
|
|
614
|
+
return `${codes.join("")}${text}${removeSessionAnsi.reset}`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function stripAnsi(text) {
|
|
618
|
+
return String(text || "").replace(/\x1b\[[0-9;]*m/g, "");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function ellipsize(text, width) {
|
|
622
|
+
const value = String(text || "").replace(/\s+/g, " ").trim();
|
|
623
|
+
if (value.length <= width) return value.padEnd(width, " ");
|
|
624
|
+
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function buttonLabel(label, focused, disabled = false) {
|
|
628
|
+
const text = ` ${label} `;
|
|
629
|
+
if (disabled) return removeSessionColor(text, removeSessionAnsi.dim);
|
|
630
|
+
return focused ? removeSessionColor(text, removeSessionAnsi.inverse, removeSessionAnsi.bold) : text;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function renderSessionRemovalWizard(state) {
|
|
634
|
+
const width = Math.min(Math.max(process.stdout.columns || 100, 80), 128);
|
|
635
|
+
const rows = process.stdout.rows || 28;
|
|
636
|
+
const bodyWidth = width - 4;
|
|
637
|
+
const visibleCount = Math.max(5, Math.min(state.items.length || 1, rows - 11));
|
|
638
|
+
if (state.cursor < state.scroll) state.scroll = state.cursor;
|
|
639
|
+
if (state.cursor >= state.scroll + visibleCount) state.scroll = state.cursor - visibleCount + 1;
|
|
640
|
+
const shown = state.items.slice(state.scroll, state.scroll + visibleCount);
|
|
641
|
+
const selectedCount = state.selected.size;
|
|
642
|
+
const border = "─".repeat(width - 2);
|
|
643
|
+
const line = (content = "") => {
|
|
644
|
+
const value = String(content || "");
|
|
645
|
+
const clipped = stripAnsi(value).length > bodyWidth ? ellipsize(stripAnsi(value), bodyWidth) : value;
|
|
646
|
+
return `│ ${clipped}${" ".repeat(Math.max(0, bodyWidth - stripAnsi(clipped).length))} │`;
|
|
647
|
+
};
|
|
648
|
+
const listRows = shown.map((session, offset) => {
|
|
649
|
+
const index = state.scroll + offset;
|
|
650
|
+
const checked = state.selected.has(session.sessionId) ? "[x]" : "[ ]";
|
|
651
|
+
const cursor = index === state.cursor ? ">" : " ";
|
|
652
|
+
const badge = session.isEmpty ? "empty" : "work";
|
|
653
|
+
const title = session.title || session.goal || "(no title)";
|
|
654
|
+
const meta = `${session.provider || "unknown"}/${session.model || "unknown"} chat=${session.chatCount} steps=${session.stepsCompleted} files=${session.artifactFileCount}`;
|
|
655
|
+
const row = `${cursor} ${checked} ${badge.padEnd(5)} ${session.sessionId} ${meta} ${title}`;
|
|
656
|
+
const clipped = ellipsize(row, bodyWidth);
|
|
657
|
+
return state.focus === "list" && index === state.cursor ? line(removeSessionColor(clipped, removeSessionAnsi.inverse)) : line(clipped);
|
|
658
|
+
});
|
|
659
|
+
const footer =
|
|
660
|
+
state.phase === "confirm"
|
|
661
|
+
? `Confirm deletion: ${buttonLabel("Yes, delete", state.confirmFocus === "yes")} ${buttonLabel("Cancel", state.confirmFocus === "cancel")}`
|
|
662
|
+
: `Actions: ${buttonLabel(`OK delete ${selectedCount}`, state.focus === "ok", selectedCount === 0)} ${buttonLabel("Cancel", state.focus === "cancel")}`;
|
|
663
|
+
const guidance =
|
|
664
|
+
state.phase === "confirm"
|
|
665
|
+
? "Left/Right switches choice. Enter confirms. Esc/q cancels."
|
|
666
|
+
: "Space toggles. Up/Down moves. Tab changes focus. Enter opens confirm. Esc/q cancels.";
|
|
667
|
+
const lines = [
|
|
668
|
+
`╭${border}╮`,
|
|
669
|
+
line(state.title),
|
|
670
|
+
line(state.subtitle),
|
|
671
|
+
line(`Showing ${state.items.length === 0 ? 0 : state.scroll + 1}-${Math.min(state.items.length, state.scroll + visibleCount)} of ${state.items.length}; selected ${selectedCount}`),
|
|
672
|
+
`├${border}┤`,
|
|
673
|
+
...listRows,
|
|
674
|
+
`├${border}┤`,
|
|
675
|
+
line(footer),
|
|
676
|
+
line(state.message || guidance),
|
|
677
|
+
`╰${border}╯`,
|
|
678
|
+
];
|
|
679
|
+
process.stdout.write(`\x1b[H\x1b[2J${lines.join("\n")}`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function promptRemoveSessions(candidates, { defaultSelectedIds = [], title = "Remove sessions", subtitle = "" } = {}) {
|
|
683
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
684
|
+
console.log("Interactive terminal required; no sessions removed.");
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const state = {
|
|
688
|
+
items: candidates,
|
|
689
|
+
selected: new Set(defaultSelectedIds),
|
|
690
|
+
cursor: 0,
|
|
691
|
+
scroll: 0,
|
|
692
|
+
focus: "list",
|
|
693
|
+
phase: "select",
|
|
694
|
+
confirmFocus: "cancel",
|
|
695
|
+
message: "",
|
|
696
|
+
title,
|
|
697
|
+
subtitle,
|
|
698
|
+
};
|
|
699
|
+
return await new Promise((resolve) => {
|
|
700
|
+
const input = process.stdin;
|
|
701
|
+
const output = process.stdout;
|
|
702
|
+
const cleanup = (value) => {
|
|
703
|
+
input.off("keypress", onKeypress);
|
|
704
|
+
if (input.isTTY) input.setRawMode(false);
|
|
705
|
+
input.pause();
|
|
706
|
+
output.write("\x1b[?25h\x1b[?1049l");
|
|
707
|
+
resolve(value);
|
|
708
|
+
};
|
|
709
|
+
const moveCursor = (delta) => {
|
|
710
|
+
state.focus = "list";
|
|
711
|
+
state.cursor = Math.min(Math.max(state.cursor + delta, 0), Math.max(0, state.items.length - 1));
|
|
712
|
+
state.message = "";
|
|
713
|
+
};
|
|
714
|
+
const toggleCurrent = () => {
|
|
715
|
+
const session = state.items[state.cursor];
|
|
716
|
+
if (!session) return;
|
|
717
|
+
if (state.selected.has(session.sessionId)) state.selected.delete(session.sessionId);
|
|
718
|
+
else state.selected.add(session.sessionId);
|
|
719
|
+
state.message = `${state.selected.size} session(s) selected.`;
|
|
720
|
+
};
|
|
721
|
+
const openConfirm = () => {
|
|
722
|
+
if (state.selected.size === 0) {
|
|
723
|
+
state.message = "Select at least one session before confirming.";
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
state.phase = "confirm";
|
|
727
|
+
state.confirmFocus = "cancel";
|
|
728
|
+
state.message = "Second confirmation required before deleting session data.";
|
|
729
|
+
};
|
|
730
|
+
function onKeypress(char, key = {}) {
|
|
731
|
+
if (key.ctrl && key.name === "c") return cleanup(null);
|
|
732
|
+
const name = key.name || char;
|
|
733
|
+
if (name === "escape" || name === "q") return cleanup(null);
|
|
734
|
+
if (state.phase === "confirm") {
|
|
735
|
+
if (name === "left" || name === "right" || name === "tab") state.confirmFocus = state.confirmFocus === "yes" ? "cancel" : "yes";
|
|
736
|
+
else if (name === "return" || name === "enter") return cleanup(state.confirmFocus === "yes" ? [...state.selected] : null);
|
|
737
|
+
renderSessionRemovalWizard(state);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (name === "up") moveCursor(-1);
|
|
741
|
+
else if (name === "down") moveCursor(1);
|
|
742
|
+
else if (name === "pageup") moveCursor(-8);
|
|
743
|
+
else if (name === "pagedown") moveCursor(8);
|
|
744
|
+
else if (name === "space") toggleCurrent();
|
|
745
|
+
else if (name === "tab") state.focus = state.focus === "list" ? "ok" : state.focus === "ok" ? "cancel" : "list";
|
|
746
|
+
else if (name === "left" || name === "right") state.focus = state.focus === "cancel" ? "ok" : "cancel";
|
|
747
|
+
else if (name === "return" || name === "enter") {
|
|
748
|
+
if (state.focus === "cancel") return cleanup(null);
|
|
749
|
+
openConfirm();
|
|
750
|
+
}
|
|
751
|
+
renderSessionRemovalWizard(state);
|
|
752
|
+
}
|
|
753
|
+
readlineRaw.emitKeypressEvents(input);
|
|
754
|
+
input.setRawMode(true);
|
|
755
|
+
input.resume();
|
|
756
|
+
output.write("\x1b[?1049h\x1b[?25l");
|
|
757
|
+
input.on("keypress", onKeypress);
|
|
758
|
+
renderSessionRemovalWizard(state);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function printRemovalPreview(candidates) {
|
|
763
|
+
for (const session of candidates) {
|
|
764
|
+
const title = session.title || session.goal || "(no title)";
|
|
765
|
+
const status = session.isEmpty ? "empty" : "work";
|
|
766
|
+
console.log(`${status.padEnd(5)} ${session.sessionId} ${session.updatedAt || ""} ${title.slice(0, 90)}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function handleRemoveSessionsCommand({ emptyOnly = false } = {}) {
|
|
771
|
+
const candidates = await listProjectSessionRemovalCandidates(process.cwd(), {
|
|
772
|
+
limit: 1000,
|
|
773
|
+
emptyOnly,
|
|
774
|
+
});
|
|
775
|
+
if (candidates.length === 0) {
|
|
776
|
+
console.log(emptyOnly ? "No empty sessions found for this cwd." : "No sessions found for this cwd.");
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
780
|
+
printRemovalPreview(candidates);
|
|
781
|
+
console.log("No sessions removed because this command needs an interactive terminal.");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const defaultSelectedIds = emptyOnly ? candidates.map((session) => session.sessionId) : [];
|
|
785
|
+
const selected = await promptRemoveSessions(candidates, {
|
|
786
|
+
defaultSelectedIds,
|
|
787
|
+
title: emptyOnly ? "Remove empty AgInTiFlow sessions in this cwd" : "Remove AgInTiFlow sessions in this cwd",
|
|
788
|
+
subtitle: emptyOnly
|
|
789
|
+
? "Only empty sessions are shown and selected by default."
|
|
790
|
+
: "All cwd sessions are shown; nothing is selected by default.",
|
|
791
|
+
});
|
|
792
|
+
if (!selected || selected.length === 0) {
|
|
793
|
+
console.log("No sessions removed.");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const allowedIds = new Set(candidates.map((session) => session.sessionId));
|
|
797
|
+
const safeSelected = selected.filter((sessionId) => allowedIds.has(sessionId));
|
|
798
|
+
const result = await removeProjectSessions(process.cwd(), safeSelected);
|
|
799
|
+
console.log(`Removed ${result.removed.length} session(s) from this cwd:`);
|
|
800
|
+
for (const item of result.removed) console.log(`- ${item.sessionId}`);
|
|
801
|
+
}
|
|
802
|
+
|
|
603
803
|
async function handleSessionsCommand(argv) {
|
|
604
804
|
const normalizedArgv = argv[0] === "--all-sessions" ? ["list", ...argv] : argv;
|
|
605
805
|
const [verb = "list", sessionId = "", ...rest] = normalizedArgv;
|
|
806
|
+
if (verb === "remove-empty" || verb === "delete-empty") {
|
|
807
|
+
await handleRemoveSessionsCommand({ emptyOnly: true });
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (verb === "remove" || verb === "delete") {
|
|
811
|
+
await handleRemoveSessionsCommand({ emptyOnly: false });
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
606
814
|
if (verb === "list") {
|
|
607
815
|
const allSessions = normalizedArgv.includes("--all-sessions");
|
|
608
816
|
const sessions = await listProjectSessions(process.cwd(), { limit: 80, allSessions });
|
|
@@ -638,7 +846,7 @@ async function handleSessionsCommand(argv) {
|
|
|
638
846
|
return;
|
|
639
847
|
}
|
|
640
848
|
|
|
641
|
-
console.error('Usage: aginti sessions list OR aginti sessions show <session-id> OR aginti sessions rename <session-id> "title"');
|
|
849
|
+
console.error('Usage: aginti sessions list OR aginti sessions show <session-id> OR aginti sessions rename <session-id> "title" OR aginti sessions remove-empty OR aginti sessions remove');
|
|
642
850
|
process.exit(1);
|
|
643
851
|
}
|
|
644
852
|
|
|
@@ -788,6 +996,16 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
788
996
|
});
|
|
789
997
|
if (autoUpdateResult.restarted) process.exit(autoUpdateResult.exitCode ?? 0);
|
|
790
998
|
|
|
999
|
+
if (argv.includes("--remove-empty-sessions") || argv[0] === "remove-empty-sessions") {
|
|
1000
|
+
await handleRemoveSessionsCommand({ emptyOnly: true });
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (argv.includes("--remove-sessions") || argv[0] === "remove-sessions") {
|
|
1005
|
+
await handleRemoveSessionsCommand({ emptyOnly: false });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
791
1009
|
if (argv[0] === "init") {
|
|
792
1010
|
printInitResult(await initProject(process.cwd()));
|
|
793
1011
|
return;
|
package/src/project.js
CHANGED
|
@@ -9,6 +9,7 @@ import { platformInfo, platformLabel, platformSetupHints } from "./platform.js";
|
|
|
9
9
|
import {
|
|
10
10
|
LEGACY_PROJECT_SESSIONS_DIR_NAME,
|
|
11
11
|
PROJECT_SESSIONS_DIR_NAME,
|
|
12
|
+
deleteSessionIndex,
|
|
12
13
|
globalSessionPaths,
|
|
13
14
|
isSafeSessionId,
|
|
14
15
|
listSessionIndex,
|
|
@@ -561,6 +562,118 @@ export async function listProjectSessions(projectRoot = process.cwd(), limitOrOp
|
|
|
561
562
|
return sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))).slice(0, options.limit);
|
|
562
563
|
}
|
|
563
564
|
|
|
565
|
+
async function readJsonFile(filePath, fallback = {}) {
|
|
566
|
+
try {
|
|
567
|
+
return JSON.parse(await fsp.readFile(filePath, "utf8"));
|
|
568
|
+
} catch {
|
|
569
|
+
return fallback;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function readJsonLines(filePath) {
|
|
574
|
+
try {
|
|
575
|
+
const raw = await fsp.readFile(filePath, "utf8");
|
|
576
|
+
return raw
|
|
577
|
+
.split("\n")
|
|
578
|
+
.map((line) => line.trim())
|
|
579
|
+
.filter(Boolean)
|
|
580
|
+
.map((line) => JSON.parse(line));
|
|
581
|
+
} catch {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function countFilesRecursive(dirPath, limit = 200) {
|
|
587
|
+
let count = 0;
|
|
588
|
+
async function walk(current) {
|
|
589
|
+
if (count >= limit) return;
|
|
590
|
+
const entries = await fsp.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
591
|
+
for (const entry of entries) {
|
|
592
|
+
if (count >= limit) return;
|
|
593
|
+
const child = path.join(current, entry.name);
|
|
594
|
+
if (entry.isDirectory()) await walk(child);
|
|
595
|
+
else if (entry.isFile()) count += 1;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
await walk(dirPath);
|
|
599
|
+
return count;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function isMeaningfulSessionEvent(event = {}) {
|
|
603
|
+
const type = String(event.type || "");
|
|
604
|
+
if (!type) return false;
|
|
605
|
+
return /^(agent|assistant|browser|canvas|conversation|file|image|model|patch|plan|run|shell|tool|workspace|write)[.:_-]/.test(type);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export async function listProjectSessionRemovalCandidates(projectRoot = process.cwd(), options = {}) {
|
|
609
|
+
const paths = await ensureProjectSessionStorage(projectRoot);
|
|
610
|
+
const sessions = await listProjectSessions(projectRoot, {
|
|
611
|
+
limit: options.limit || 1000,
|
|
612
|
+
commandCwd: options.commandCwd,
|
|
613
|
+
allSessions: options.allSessions,
|
|
614
|
+
});
|
|
615
|
+
const candidates = [];
|
|
616
|
+
for (const session of sessions) {
|
|
617
|
+
const safeId = String(session.sessionId || "");
|
|
618
|
+
if (!isSafeSessionId(safeId)) continue;
|
|
619
|
+
const pointerPath = path.join(paths.sessionsDir, safeId, "session.json");
|
|
620
|
+
const pointer = await readJsonFile(pointerPath, {});
|
|
621
|
+
const sessionDir = session.sessionDir || pointer.sessionDir || path.join(paths.globalSessionsDir, safeId);
|
|
622
|
+
const state = await readJsonFile(path.join(sessionDir, "state.json"), {});
|
|
623
|
+
const events = await readJsonLines(path.join(sessionDir, "events.jsonl"));
|
|
624
|
+
const chat = Array.isArray(state.chat) ? state.chat : [];
|
|
625
|
+
const goal = String(state.goal || pointer.goal || session.goal || "").trim();
|
|
626
|
+
const title = String(state.title || pointer.title || session.title || "").trim();
|
|
627
|
+
const stepsCompleted = Number(state.stepsCompleted || 0);
|
|
628
|
+
const artifactsDir = state.artifactsDir || pointer.artifactsDir || path.join(sessionDir, "artifacts");
|
|
629
|
+
const artifactFileCount = await countFilesRecursive(artifactsDir);
|
|
630
|
+
const meaningfulEventCount = events.filter(isMeaningfulSessionEvent).length;
|
|
631
|
+
const isEmpty =
|
|
632
|
+
!goal &&
|
|
633
|
+
!title &&
|
|
634
|
+
chat.length === 0 &&
|
|
635
|
+
stepsCompleted === 0 &&
|
|
636
|
+
artifactFileCount === 0 &&
|
|
637
|
+
meaningfulEventCount === 0;
|
|
638
|
+
candidates.push({
|
|
639
|
+
...session,
|
|
640
|
+
sessionId: safeId,
|
|
641
|
+
sessionDir,
|
|
642
|
+
pointerPath,
|
|
643
|
+
chatCount: chat.length,
|
|
644
|
+
eventCount: events.length,
|
|
645
|
+
meaningfulEventCount,
|
|
646
|
+
artifactFileCount,
|
|
647
|
+
stepsCompleted,
|
|
648
|
+
isEmpty,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return options.emptyOnly ? candidates.filter((session) => session.isEmpty) : candidates;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function removeProjectSessions(projectRoot = process.cwd(), sessionIds = []) {
|
|
655
|
+
const paths = await ensureProjectSessionStorage(projectRoot);
|
|
656
|
+
const removed = [];
|
|
657
|
+
for (const rawId of sessionIds) {
|
|
658
|
+
const safeId = String(rawId || "");
|
|
659
|
+
if (!isSafeSessionId(safeId)) throw new Error(`Invalid session id: ${rawId}`);
|
|
660
|
+
const pointerDir = path.join(paths.sessionsDir, safeId);
|
|
661
|
+
const pointer = await readJsonFile(path.join(pointerDir, "session.json"), {});
|
|
662
|
+
const sessionDir = pointer.sessionDir || path.join(paths.globalSessionsDir, safeId);
|
|
663
|
+
const legacyDir = path.join(paths.legacySessionsDir, safeId);
|
|
664
|
+
await fsp.rm(sessionDir, { recursive: true, force: true });
|
|
665
|
+
await fsp.rm(pointerDir, { recursive: true, force: true });
|
|
666
|
+
await fsp.rm(legacyDir, { recursive: true, force: true }).catch(() => {});
|
|
667
|
+
try {
|
|
668
|
+
deleteSessionIndex(safeId);
|
|
669
|
+
} catch {
|
|
670
|
+
// The on-disk session and pointer are already removed; stale index cleanup can be retried later.
|
|
671
|
+
}
|
|
672
|
+
removed.push({ sessionId: safeId, sessionDir, pointerDir });
|
|
673
|
+
}
|
|
674
|
+
return { ok: true, removed };
|
|
675
|
+
}
|
|
676
|
+
|
|
564
677
|
export async function showProjectSession(projectRoot, sessionId) {
|
|
565
678
|
const paths = await ensureProjectSessionStorage(projectRoot);
|
|
566
679
|
const safeId = String(sessionId || "");
|