@lazyingart/agintiflow 0.20.43 → 0.20.45
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 +8 -0
- package/package.json +1 -1
- package/scripts/smoke-cli-chat.js +6 -1
- package/scripts/smoke-inbox.js +51 -1
- package/skills/code-review/SKILL.md +8 -1
- package/src/cli.js +228 -2
- package/src/interactive-cli.js +38 -0
- package/src/project.js +113 -0
- package/src/task-profiles.js +14 -0
package/README.md
CHANGED
|
@@ -138,12 +138,16 @@ 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
|
|
144
146
|
aginti resume latest
|
|
145
147
|
aginti resume <session-id> "continue with a short follow-up"
|
|
146
148
|
aginti queue <session-id> "extra instruction for the running agent"
|
|
149
|
+
aginti chat
|
|
150
|
+
# then in chat: /review [focus]
|
|
147
151
|
aginti --profile code "write a small Python CLI app with tests"
|
|
148
152
|
aginti --latex "draw a figure, write a short LaTeX report, and compile the PDF"
|
|
149
153
|
aginti "set up this project and run the tests"
|
|
@@ -151,6 +155,10 @@ aginti "set up this project and run the tests"
|
|
|
151
155
|
|
|
152
156
|
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
157
|
|
|
158
|
+
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 or activate the focused button, Up/Down to move, Tab to switch to Delete/Cancel, and a second Delete/Cancel confirmation before deleting the project pointer and central `~/.agintiflow/sessions/<session-id>` data.
|
|
159
|
+
|
|
160
|
+
In interactive chat, `/review [focus]` starts a bounded repository review. It begins from git status/diff and project instructions, reads manifests/entry points/tests/changed files first, avoids generated or binary folders, limits discovery passes, and reports findings before any summary.
|
|
161
|
+
|
|
154
162
|
Run from a source checkout:
|
|
155
163
|
|
|
156
164
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.45",
|
|
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",
|
|
@@ -260,7 +260,7 @@ try {
|
|
|
260
260
|
}
|
|
261
261
|
const helpResult = await runChat("/help\n/exit\n");
|
|
262
262
|
const misspelledAuxiliary = "/auxil" + "liary";
|
|
263
|
-
if (!helpResult.stdout.includes("/auxiliary") || helpResult.stdout.includes(misspelledAuxiliary)) {
|
|
263
|
+
if (!helpResult.stdout.includes("/auxiliary") || !helpResult.stdout.includes("/review") || helpResult.stdout.includes(misspelledAuxiliary)) {
|
|
264
264
|
throw new Error("interactive help did not expose only the correctly spelled /auxiliary command");
|
|
265
265
|
}
|
|
266
266
|
const zhHelpResult = await runCli(["chat", "--language", "zh-Hans"], "/help\n/exit\n");
|
|
@@ -271,6 +271,10 @@ try {
|
|
|
271
271
|
if (!skillsResult.stdout.includes("website-app") || !skillsResult.stdout.includes("Website And App Builder")) {
|
|
272
272
|
throw new Error("interactive /skills did not show matching built-in skills");
|
|
273
273
|
}
|
|
274
|
+
const reviewResult = await runChat("/review changed files only\n/exit\n");
|
|
275
|
+
if (!reviewResult.stdout.includes("Review focus: changed files only") || !reviewResult.stdout.includes("Mock run complete")) {
|
|
276
|
+
throw new Error("interactive /review did not launch the bounded review workflow");
|
|
277
|
+
}
|
|
274
278
|
const abbreviatedSkillsResult = await runChat("/sk website\n/ex\n");
|
|
275
279
|
if (abbreviatedSkillsResult.stdout.includes("Unknown command") || !abbreviatedSkillsResult.stdout.includes("website-app")) {
|
|
276
280
|
throw new Error("interactive slash command prefix did not auto-select the first matching command");
|
|
@@ -347,6 +351,7 @@ try {
|
|
|
347
351
|
"instructions-command",
|
|
348
352
|
"auxiliary-command-spelling",
|
|
349
353
|
"skills-command",
|
|
354
|
+
"review-command",
|
|
350
355
|
"slash-prefix-autoselect",
|
|
351
356
|
"slash-prefix-canonical-history",
|
|
352
357
|
"instructions-chat-edit",
|
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,
|
|
@@ -20,5 +20,12 @@ tools:
|
|
|
20
20
|
|
|
21
21
|
Prioritize findings over summary. Inspect changed files, neighboring code, tests, and runtime assumptions. Report concrete risks with file paths, reproduction evidence, and suggested fixes.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Use a bounded review loop:
|
|
24
|
+
|
|
25
|
+
1. Start with git status/diff and project instructions or manifests.
|
|
26
|
+
2. Read high-signal files first: changed files, entry points, tests, package/build configs, and nearby code needed to prove a risk.
|
|
27
|
+
3. Avoid full-tree reads and generated/vendor/cache/binary folders such as `.git`, `node_modules`, `dist`, `build`, `target`, `coverage`, `.venv`, `__pycache__`, `.aginti-sessions`, `.sessions`, and artifacts.
|
|
28
|
+
4. Cap discovery at two passes unless a concrete finding needs one more neighboring file.
|
|
29
|
+
5. Run focused non-destructive checks when useful; do not install dependencies or run long broad suites for a review unless clearly justified.
|
|
24
30
|
|
|
31
|
+
If no findings are found, say that clearly and name residual risk or missing test coverage. Do not rewrite code during a review unless the user asks for fixes.
|
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,222 @@ 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
|
+
red: "\x1b[31m",
|
|
611
|
+
inverse: "\x1b[7m",
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
function removeSessionColor(text, ...codes) {
|
|
615
|
+
return `${codes.join("")}${text}${removeSessionAnsi.reset}`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function stripAnsi(text) {
|
|
619
|
+
return String(text || "").replace(/\x1b\[[0-9;]*m/g, "");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function ellipsize(text, width) {
|
|
623
|
+
const value = String(text || "").replace(/\s+/g, " ").trim();
|
|
624
|
+
if (value.length <= width) return value.padEnd(width, " ");
|
|
625
|
+
return `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function buttonLabel(label, focused, disabled = false, danger = false) {
|
|
629
|
+
const text = ` ${label} `;
|
|
630
|
+
if (disabled) return removeSessionColor(text, removeSessionAnsi.dim);
|
|
631
|
+
const codes = [];
|
|
632
|
+
if (danger) codes.push(removeSessionAnsi.red, removeSessionAnsi.bold);
|
|
633
|
+
if (focused) codes.push(removeSessionAnsi.inverse, removeSessionAnsi.bold);
|
|
634
|
+
return codes.length > 0 ? removeSessionColor(text, ...codes) : text;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function renderSessionRemovalWizard(state) {
|
|
638
|
+
const width = Math.min(Math.max(process.stdout.columns || 100, 80), 128);
|
|
639
|
+
const rows = process.stdout.rows || 28;
|
|
640
|
+
const bodyWidth = width - 4;
|
|
641
|
+
const visibleCount = Math.max(5, Math.min(state.items.length || 1, rows - 11));
|
|
642
|
+
if (state.cursor < state.scroll) state.scroll = state.cursor;
|
|
643
|
+
if (state.cursor >= state.scroll + visibleCount) state.scroll = state.cursor - visibleCount + 1;
|
|
644
|
+
const shown = state.items.slice(state.scroll, state.scroll + visibleCount);
|
|
645
|
+
const selectedCount = state.selected.size;
|
|
646
|
+
const border = "─".repeat(width - 2);
|
|
647
|
+
const line = (content = "") => {
|
|
648
|
+
const value = String(content || "");
|
|
649
|
+
const clipped = stripAnsi(value).length > bodyWidth ? ellipsize(stripAnsi(value), bodyWidth) : value;
|
|
650
|
+
return `│ ${clipped}${" ".repeat(Math.max(0, bodyWidth - stripAnsi(clipped).length))} │`;
|
|
651
|
+
};
|
|
652
|
+
const listRows = shown.map((session, offset) => {
|
|
653
|
+
const index = state.scroll + offset;
|
|
654
|
+
const checked = state.selected.has(session.sessionId) ? "[x]" : "[ ]";
|
|
655
|
+
const cursor = index === state.cursor ? ">" : " ";
|
|
656
|
+
const badge = session.isEmpty ? "empty" : "work";
|
|
657
|
+
const title = session.title || session.goal || "(no title)";
|
|
658
|
+
const meta = `${session.provider || "unknown"}/${session.model || "unknown"} chat=${session.chatCount} steps=${session.stepsCompleted} files=${session.artifactFileCount}`;
|
|
659
|
+
const row = `${cursor} ${checked} ${badge.padEnd(5)} ${session.sessionId} ${meta} ${title}`;
|
|
660
|
+
const clipped = ellipsize(row, bodyWidth);
|
|
661
|
+
return state.focus === "list" && index === state.cursor ? line(removeSessionColor(clipped, removeSessionAnsi.inverse)) : line(clipped);
|
|
662
|
+
});
|
|
663
|
+
const footer =
|
|
664
|
+
state.phase === "confirm"
|
|
665
|
+
? `Confirm deletion: ${buttonLabel("Delete", state.confirmFocus === "yes", false, true)} ${buttonLabel("Cancel", state.confirmFocus === "cancel")}`
|
|
666
|
+
: `Actions: ${buttonLabel(`Delete ${selectedCount}`, state.focus === "ok", selectedCount === 0, true)} ${buttonLabel("Cancel", state.focus === "cancel")}`;
|
|
667
|
+
const guidance =
|
|
668
|
+
state.phase === "confirm"
|
|
669
|
+
? "Left/Right switches choice. Enter/Space confirms. Esc/q cancels."
|
|
670
|
+
: "Space toggles or activates focused button. Up/Down moves. Tab changes focus. Esc/q cancels.";
|
|
671
|
+
const lines = [
|
|
672
|
+
`╭${border}╮`,
|
|
673
|
+
line(state.title),
|
|
674
|
+
line(state.subtitle),
|
|
675
|
+
line(`Showing ${state.items.length === 0 ? 0 : state.scroll + 1}-${Math.min(state.items.length, state.scroll + visibleCount)} of ${state.items.length}; selected ${selectedCount}`),
|
|
676
|
+
`├${border}┤`,
|
|
677
|
+
...listRows,
|
|
678
|
+
`├${border}┤`,
|
|
679
|
+
line(footer),
|
|
680
|
+
line(state.message || guidance),
|
|
681
|
+
`╰${border}╯`,
|
|
682
|
+
];
|
|
683
|
+
process.stdout.write(`\x1b[H\x1b[2J${lines.join("\n")}`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function promptRemoveSessions(candidates, { defaultSelectedIds = [], title = "Remove sessions", subtitle = "" } = {}) {
|
|
687
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
688
|
+
console.log("Interactive terminal required; no sessions removed.");
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const state = {
|
|
692
|
+
items: candidates,
|
|
693
|
+
selected: new Set(defaultSelectedIds),
|
|
694
|
+
cursor: 0,
|
|
695
|
+
scroll: 0,
|
|
696
|
+
focus: "list",
|
|
697
|
+
phase: "select",
|
|
698
|
+
confirmFocus: "cancel",
|
|
699
|
+
message: "",
|
|
700
|
+
title,
|
|
701
|
+
subtitle,
|
|
702
|
+
};
|
|
703
|
+
return await new Promise((resolve) => {
|
|
704
|
+
const input = process.stdin;
|
|
705
|
+
const output = process.stdout;
|
|
706
|
+
const cleanup = (value) => {
|
|
707
|
+
input.off("keypress", onKeypress);
|
|
708
|
+
if (input.isTTY) input.setRawMode(false);
|
|
709
|
+
input.pause();
|
|
710
|
+
output.write("\x1b[?25h\x1b[?1049l");
|
|
711
|
+
resolve(value);
|
|
712
|
+
};
|
|
713
|
+
const moveCursor = (delta) => {
|
|
714
|
+
state.focus = "list";
|
|
715
|
+
state.cursor = Math.min(Math.max(state.cursor + delta, 0), Math.max(0, state.items.length - 1));
|
|
716
|
+
state.message = "";
|
|
717
|
+
};
|
|
718
|
+
const toggleCurrent = () => {
|
|
719
|
+
const session = state.items[state.cursor];
|
|
720
|
+
if (!session) return;
|
|
721
|
+
if (state.selected.has(session.sessionId)) state.selected.delete(session.sessionId);
|
|
722
|
+
else state.selected.add(session.sessionId);
|
|
723
|
+
state.message = `${state.selected.size} session(s) selected.`;
|
|
724
|
+
};
|
|
725
|
+
const openConfirm = () => {
|
|
726
|
+
if (state.selected.size === 0) {
|
|
727
|
+
state.message = "Select at least one session before confirming.";
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
state.phase = "confirm";
|
|
731
|
+
state.confirmFocus = "cancel";
|
|
732
|
+
state.message = "Second confirmation required before deleting session data.";
|
|
733
|
+
};
|
|
734
|
+
function onKeypress(char, key = {}) {
|
|
735
|
+
if (key.ctrl && key.name === "c") return cleanup(null);
|
|
736
|
+
const name = key.name || char;
|
|
737
|
+
if (name === "escape" || name === "q") return cleanup(null);
|
|
738
|
+
if (state.phase === "confirm") {
|
|
739
|
+
if (name === "left" || name === "right" || name === "tab") state.confirmFocus = state.confirmFocus === "yes" ? "cancel" : "yes";
|
|
740
|
+
else if (name === "return" || name === "enter" || name === "space") return cleanup(state.confirmFocus === "yes" ? [...state.selected] : null);
|
|
741
|
+
renderSessionRemovalWizard(state);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (name === "up") moveCursor(-1);
|
|
745
|
+
else if (name === "down") moveCursor(1);
|
|
746
|
+
else if (name === "pageup") moveCursor(-8);
|
|
747
|
+
else if (name === "pagedown") moveCursor(8);
|
|
748
|
+
else if (name === "space") {
|
|
749
|
+
if (state.focus === "list") toggleCurrent();
|
|
750
|
+
else if (state.focus === "cancel") return cleanup(null);
|
|
751
|
+
else openConfirm();
|
|
752
|
+
}
|
|
753
|
+
else if (name === "tab") state.focus = state.focus === "list" ? "ok" : state.focus === "ok" ? "cancel" : "list";
|
|
754
|
+
else if (name === "left" || name === "right") state.focus = state.focus === "cancel" ? "ok" : "cancel";
|
|
755
|
+
else if (name === "return" || name === "enter") {
|
|
756
|
+
if (state.focus === "cancel") return cleanup(null);
|
|
757
|
+
openConfirm();
|
|
758
|
+
}
|
|
759
|
+
renderSessionRemovalWizard(state);
|
|
760
|
+
}
|
|
761
|
+
readlineRaw.emitKeypressEvents(input);
|
|
762
|
+
input.setRawMode(true);
|
|
763
|
+
input.resume();
|
|
764
|
+
output.write("\x1b[?1049h\x1b[?25l");
|
|
765
|
+
input.on("keypress", onKeypress);
|
|
766
|
+
renderSessionRemovalWizard(state);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function printRemovalPreview(candidates) {
|
|
771
|
+
for (const session of candidates) {
|
|
772
|
+
const title = session.title || session.goal || "(no title)";
|
|
773
|
+
const status = session.isEmpty ? "empty" : "work";
|
|
774
|
+
console.log(`${status.padEnd(5)} ${session.sessionId} ${session.updatedAt || ""} ${title.slice(0, 90)}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function handleRemoveSessionsCommand({ emptyOnly = false } = {}) {
|
|
779
|
+
const candidates = await listProjectSessionRemovalCandidates(process.cwd(), {
|
|
780
|
+
limit: 1000,
|
|
781
|
+
emptyOnly,
|
|
782
|
+
});
|
|
783
|
+
if (candidates.length === 0) {
|
|
784
|
+
console.log(emptyOnly ? "No empty sessions found for this cwd." : "No sessions found for this cwd.");
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
788
|
+
printRemovalPreview(candidates);
|
|
789
|
+
console.log("No sessions removed because this command needs an interactive terminal.");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const defaultSelectedIds = emptyOnly ? candidates.map((session) => session.sessionId) : [];
|
|
793
|
+
const selected = await promptRemoveSessions(candidates, {
|
|
794
|
+
defaultSelectedIds,
|
|
795
|
+
title: emptyOnly ? "Remove empty AgInTiFlow sessions in this cwd" : "Remove AgInTiFlow sessions in this cwd",
|
|
796
|
+
subtitle: emptyOnly
|
|
797
|
+
? "Only empty sessions are shown and selected by default."
|
|
798
|
+
: "All cwd sessions are shown; nothing is selected by default.",
|
|
799
|
+
});
|
|
800
|
+
if (!selected || selected.length === 0) {
|
|
801
|
+
console.log("No sessions removed.");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const allowedIds = new Set(candidates.map((session) => session.sessionId));
|
|
805
|
+
const safeSelected = selected.filter((sessionId) => allowedIds.has(sessionId));
|
|
806
|
+
const result = await removeProjectSessions(process.cwd(), safeSelected);
|
|
807
|
+
console.log(`Removed ${result.removed.length} session(s) from this cwd:`);
|
|
808
|
+
for (const item of result.removed) console.log(`- ${item.sessionId}`);
|
|
809
|
+
}
|
|
810
|
+
|
|
603
811
|
async function handleSessionsCommand(argv) {
|
|
604
812
|
const normalizedArgv = argv[0] === "--all-sessions" ? ["list", ...argv] : argv;
|
|
605
813
|
const [verb = "list", sessionId = "", ...rest] = normalizedArgv;
|
|
814
|
+
if (verb === "remove-empty" || verb === "delete-empty") {
|
|
815
|
+
await handleRemoveSessionsCommand({ emptyOnly: true });
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (verb === "remove" || verb === "delete") {
|
|
819
|
+
await handleRemoveSessionsCommand({ emptyOnly: false });
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
606
822
|
if (verb === "list") {
|
|
607
823
|
const allSessions = normalizedArgv.includes("--all-sessions");
|
|
608
824
|
const sessions = await listProjectSessions(process.cwd(), { limit: 80, allSessions });
|
|
@@ -638,7 +854,7 @@ async function handleSessionsCommand(argv) {
|
|
|
638
854
|
return;
|
|
639
855
|
}
|
|
640
856
|
|
|
641
|
-
console.error('Usage: aginti sessions list OR aginti sessions show <session-id> OR aginti sessions rename <session-id> "title"');
|
|
857
|
+
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
858
|
process.exit(1);
|
|
643
859
|
}
|
|
644
860
|
|
|
@@ -788,6 +1004,16 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
788
1004
|
});
|
|
789
1005
|
if (autoUpdateResult.restarted) process.exit(autoUpdateResult.exitCode ?? 0);
|
|
790
1006
|
|
|
1007
|
+
if (argv.includes("--remove-empty-sessions") || argv[0] === "remove-empty-sessions") {
|
|
1008
|
+
await handleRemoveSessionsCommand({ emptyOnly: true });
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (argv.includes("--remove-sessions") || argv[0] === "remove-sessions") {
|
|
1013
|
+
await handleRemoveSessionsCommand({ emptyOnly: false });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
791
1017
|
if (argv[0] === "init") {
|
|
792
1018
|
printInitResult(await initProject(process.cwd()));
|
|
793
1019
|
return;
|
package/src/interactive-cli.js
CHANGED
|
@@ -69,6 +69,7 @@ const SLASH_COMMANDS = [
|
|
|
69
69
|
"/new",
|
|
70
70
|
"/resume",
|
|
71
71
|
"/sessions",
|
|
72
|
+
"/review",
|
|
72
73
|
"/rename",
|
|
73
74
|
"/skills",
|
|
74
75
|
"/skill",
|
|
@@ -635,6 +636,7 @@ function printHelp() {
|
|
|
635
636
|
` ${command("/auxiliary [status|grsai|venice|model [provider/model]|on|off|image]", "Manage optional auxiliary skills, including image generation.", "helpAuxiliary")}`,
|
|
636
637
|
` ${command("/new", "Start a fresh session on the next message.", "helpNew")}`,
|
|
637
638
|
` ${command("/resume <session-id>", "Continue a saved session.", "helpResume")}`,
|
|
639
|
+
` ${command("/review [focus]", "Run a bounded repo/diff review with controlled context gathering.", "helpReview")}`,
|
|
638
640
|
` ${command("/rename [title|auto]", "Rename the current session.", "helpRename")}`,
|
|
639
641
|
` ${command("/sessions", "List recent sessions in this project.", "helpSessions")}`,
|
|
640
642
|
` ${command("/skills [query]", "List Markdown skills selected for a topic.", "helpSkills")}`,
|
|
@@ -2454,6 +2456,29 @@ async function promptAndSaveProviderKey(provider = "", state = null) {
|
|
|
2454
2456
|
applyAuthWizardResult(result, state);
|
|
2455
2457
|
}
|
|
2456
2458
|
|
|
2459
|
+
function buildReviewPrompt(focus = "") {
|
|
2460
|
+
const target = String(focus || "").trim();
|
|
2461
|
+
return [
|
|
2462
|
+
target ? `Review focus: ${target}` : "Review focus: current repository state, especially local changes if any.",
|
|
2463
|
+
"",
|
|
2464
|
+
"Run a bounded, evidence-based code review of this workspace. Default to read-only review; do not edit files unless the review focus explicitly asks for fixes.",
|
|
2465
|
+
"",
|
|
2466
|
+
"Review operating loop:",
|
|
2467
|
+
"1. Start with `git status --short`, `git diff --stat`, and project metadata. If git is unavailable, say so and continue from manifests.",
|
|
2468
|
+
"2. Read only the highest-signal context first: AGINTI.md/AGENTS.md/README, package/build manifests, entry points, tests, and files changed in git diff.",
|
|
2469
|
+
"3. Use `inspect_project`, `search_files`, and targeted `read_file`; avoid full-tree dumps. Prefer precise symbol/error searches over opening many files.",
|
|
2470
|
+
"4. Exclude generated, vendored, binary, cache, and large artifact paths: .git, node_modules, vendor, dist, build, out, target, coverage, .next, .turbo, .venv, __pycache__, .pytest_cache, .aginti-sessions, .sessions, artifacts, images/videos/PDFs unless directly relevant.",
|
|
2471
|
+
"5. Context budget: at most two discovery passes; at most 12 primary files read initially; expand only when a concrete risk requires neighboring code.",
|
|
2472
|
+
"6. Check likely validation commands from manifests, but run only focused non-destructive checks when useful. Do not install packages or run long broad suites unless clearly justified.",
|
|
2473
|
+
"7. Stop when you have enough evidence. Do not keep scanning just because more files exist.",
|
|
2474
|
+
"",
|
|
2475
|
+
"Final answer format:",
|
|
2476
|
+
"- Findings first, ordered by severity, with file/line references or exact evidence. Focus on bugs, regressions, security issues, data loss, broken UX/API behavior, and missing tests.",
|
|
2477
|
+
"- If no findings, state that clearly and list residual risks or unrun checks.",
|
|
2478
|
+
"- Then include a short `Files inspected` and `Checks run` section. Keep summary secondary and concise.",
|
|
2479
|
+
].join("\n");
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2457
2482
|
async function handleCommand(line, state, packageDir) {
|
|
2458
2483
|
const [rawCommand, ...rest] = line.slice(1).trim().split(/\s+/);
|
|
2459
2484
|
const command = resolveSlashCommand(rawCommand);
|
|
@@ -2605,6 +2630,19 @@ async function handleCommand(line, state, packageDir) {
|
|
|
2605
2630
|
}
|
|
2606
2631
|
return true;
|
|
2607
2632
|
}
|
|
2633
|
+
if (command === "review") {
|
|
2634
|
+
const previousProfile = state.taskProfile;
|
|
2635
|
+
const previousMaxSteps = state.maxSteps;
|
|
2636
|
+
try {
|
|
2637
|
+
state.taskProfile = "review";
|
|
2638
|
+
state.maxSteps = Math.max(state.maxSteps, 32);
|
|
2639
|
+
await runPrompt(buildReviewPrompt(value), state, packageDir);
|
|
2640
|
+
} finally {
|
|
2641
|
+
state.taskProfile = previousProfile;
|
|
2642
|
+
state.maxSteps = previousMaxSteps;
|
|
2643
|
+
}
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2608
2646
|
if (command === "rename") {
|
|
2609
2647
|
if (!state.sessionId) {
|
|
2610
2648
|
printAgentMessage("No active session to rename. Start or resume a session first.");
|
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 || "");
|
package/src/task-profiles.js
CHANGED
|
@@ -20,6 +20,13 @@ export const TASK_PROFILES = {
|
|
|
20
20
|
"Bias toward senior large-repo engineering while still answering ordinary side questions. Inspect_project first unless context is already known, read AGINTI/AGENTS/README/manifests, locate entry points and tests, make a small explicit change plan, patch in coherent batches, run the narrowest relevant checks first, escalate to broader checks when stable, and summarize files changed, checks, tradeoffs, and remaining risks.",
|
|
21
21
|
tools: ["inspect_project", "search_files", "read_file", "apply_patch", "shell", "sandbox", "canvas"],
|
|
22
22
|
},
|
|
23
|
+
review: {
|
|
24
|
+
id: "review",
|
|
25
|
+
label: "Code review",
|
|
26
|
+
prompt:
|
|
27
|
+
"Bias toward bounded code review rather than implementation. Start with git status/diff and project instructions, then inspect manifests, entry points, tests, changed files, and only the neighboring code needed to prove or disprove concrete risks. Avoid full-tree scans, generated/vendor/cache/binary folders, and infinite context gathering. Do not edit files unless explicitly asked for fixes. Findings must come first, ordered by severity with file/line evidence; if no findings are found, say so and name residual risks and checks not run.",
|
|
28
|
+
tools: ["inspect_project", "search_files", "read_file", "shell", "web_search"],
|
|
29
|
+
},
|
|
23
30
|
writing: {
|
|
24
31
|
id: "writing",
|
|
25
32
|
label: "Book/script writing",
|
|
@@ -331,6 +338,12 @@ const PROFILE_ALIASES = {
|
|
|
331
338
|
etl: "data",
|
|
332
339
|
dataframe: "data",
|
|
333
340
|
qa: "qa",
|
|
341
|
+
review: "review",
|
|
342
|
+
reviews: "review",
|
|
343
|
+
"code-review": "review",
|
|
344
|
+
"code-audit": "review",
|
|
345
|
+
codereview: "review",
|
|
346
|
+
codeaudit: "review",
|
|
334
347
|
test: "qa",
|
|
335
348
|
testing: "qa",
|
|
336
349
|
ci: "qa",
|
|
@@ -412,6 +425,7 @@ export function getTaskProfile(value = "auto") {
|
|
|
412
425
|
export function defaultMaxStepsForProfile(value = "auto") {
|
|
413
426
|
const profile = normalizeTaskProfile(value);
|
|
414
427
|
if (profile === "code") return 36;
|
|
428
|
+
if (profile === "review") return 32;
|
|
415
429
|
if (profile === "large-codebase") return 36;
|
|
416
430
|
if (profile === "qa") return 40;
|
|
417
431
|
if (profile === "app") return 40;
|