@lazyingart/agintiflow 0.20.46 → 0.20.47
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/docs/housekeeping.md +39 -0
- package/package.json +1 -1
- package/scripts/smoke-inbox.js +27 -0
- package/src/agent-runner.js +8 -0
- package/src/cli.js +36 -1
- package/src/housekeeping.js +337 -0
- package/src/interactive-cli.js +1 -0
- package/src/project.js +5 -0
- package/src/session-store.js +11 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# AgInTiFlow Housekeeping
|
|
2
|
+
|
|
3
|
+
AgInTiFlow keeps full private session history in `~/.agintiflow/sessions/<session-id>/`.
|
|
4
|
+
That history can include user prompts, model text, tool arguments, file diffs, and artifacts, so it is local-only and should not be committed.
|
|
5
|
+
|
|
6
|
+
Housekeeping is a lighter background layer for reusable learning:
|
|
7
|
+
|
|
8
|
+
- It listens to session events without blocking the main agent loop.
|
|
9
|
+
- It writes sanitized cross-session records to `~/.agintiflow/housekeeping/events.jsonl`.
|
|
10
|
+
- It maintains aggregate model, tool, and skill usage in `~/.agintiflow/housekeeping/capabilities.json`.
|
|
11
|
+
- It redacts common token/key/password patterns and replaces local project/home paths with placeholders.
|
|
12
|
+
- It stores previews, hashes, counts, model/tool names, selected skill ids, and touched skill files rather than a full second transcript.
|
|
13
|
+
|
|
14
|
+
Use:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
aginti housekeeping
|
|
18
|
+
aginti housekeeping --json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Disable local housekeeping for a run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
AGINTIFLOW_HOUSEKEEPING=0 aginti
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Project session pointers live in `.aginti-sessions/`, and old `.sessions/` folders may still exist after migration.
|
|
28
|
+
AgInTiFlow adds both to `.gitignore` during `aginti init` and when session storage is prepared, because these folders can contain private local metadata.
|
|
29
|
+
|
|
30
|
+
## Sharing Without A Center Server
|
|
31
|
+
|
|
32
|
+
Without a central server, the safe sharing path is:
|
|
33
|
+
|
|
34
|
+
1. Keep raw sessions and artifacts local.
|
|
35
|
+
2. Aggregate local learning into sanitized housekeeping capability data.
|
|
36
|
+
3. Promote reviewed reusable knowledge into Markdown skills, task profiles, command policy, or docs.
|
|
37
|
+
4. Ship those reviewed files through the npm package.
|
|
38
|
+
|
|
39
|
+
This avoids uploading private transcripts while still letting users receive better skills and tools when they upgrade `@lazyingart/agintiflow`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.47",
|
|
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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
sessionStoreOptions,
|
|
11
11
|
} from "../src/project.js";
|
|
12
12
|
import { SessionStore } from "../src/session-store.js";
|
|
13
|
+
import { flushHousekeeping, readHousekeepingSummary } from "../src/housekeeping.js";
|
|
13
14
|
|
|
14
15
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-inbox-"));
|
|
15
16
|
process.env.AGINTIFLOW_HOME = path.join(tempRoot, ".agintiflow-home");
|
|
@@ -53,6 +54,8 @@ try {
|
|
|
53
54
|
);
|
|
54
55
|
await fs.writeFile(path.join(legacyDir, "events.jsonl"), "", "utf8");
|
|
55
56
|
const paths = await ensureProjectSessionStorage(legacyProject);
|
|
57
|
+
const gitignore = await fs.readFile(path.join(legacyProject, ".gitignore"), "utf8");
|
|
58
|
+
assert(gitignore.includes(".aginti-sessions/") && gitignore.includes(".sessions/"), "session folders were not protected by .gitignore");
|
|
56
59
|
const migrated = await listProjectSessions(legacyProject, 10);
|
|
57
60
|
assert(migrated.some((session) => session.sessionId === legacySession), "legacy session was not discoverable after migration");
|
|
58
61
|
assert(
|
|
@@ -125,6 +128,28 @@ try {
|
|
|
125
128
|
"empty session project pointer was not removed"
|
|
126
129
|
);
|
|
127
130
|
|
|
131
|
+
const housekeepingStore = new SessionStore(paths.globalSessionsDir, "housekeeping-smoke", sessionStoreOptions(legacyProject, "housekeeping-smoke"));
|
|
132
|
+
await housekeepingStore.appendEvent("skills.selected", {
|
|
133
|
+
taskProfile: "website",
|
|
134
|
+
skills: ["website-app", "code-review"],
|
|
135
|
+
goal: "build a small website with token=secret-value",
|
|
136
|
+
});
|
|
137
|
+
await housekeepingStore.appendEvent("model.responded", {
|
|
138
|
+
step: 1,
|
|
139
|
+
content: "Use the local project at /tmp/private-project and avoid api_key=abc123456789.",
|
|
140
|
+
toolCalls: [{ id: "call-secret", name: "run_command", arguments: "{\"command\":\"echo hi\"}" }],
|
|
141
|
+
});
|
|
142
|
+
await housekeepingStore.appendEvent("tool.started", {
|
|
143
|
+
toolName: "run_command",
|
|
144
|
+
args: { command: "echo token=secret-value" },
|
|
145
|
+
});
|
|
146
|
+
await flushHousekeeping();
|
|
147
|
+
const housekeeping = await readHousekeepingSummary();
|
|
148
|
+
assert(housekeeping.capabilities?.totals?.skillSelections >= 1, "housekeeping did not aggregate selected skills");
|
|
149
|
+
assert(housekeeping.capabilities?.tools?.run_command?.count >= 1, "housekeeping did not aggregate tool usage");
|
|
150
|
+
const housekeepingEvents = await fs.readFile(housekeeping.paths.eventsPath, "utf8");
|
|
151
|
+
assert(!housekeepingEvents.includes("secret-value") && !housekeepingEvents.includes("abc123456789"), "housekeeping leaked raw secret text");
|
|
152
|
+
|
|
128
153
|
console.log(
|
|
129
154
|
JSON.stringify(
|
|
130
155
|
{
|
|
@@ -139,6 +164,8 @@ try {
|
|
|
139
164
|
"all-sessions-list",
|
|
140
165
|
"empty-session-detection",
|
|
141
166
|
"empty-session-removal",
|
|
167
|
+
"session-gitignore-protection",
|
|
168
|
+
"housekeeping-redacted-learning-log",
|
|
142
169
|
],
|
|
143
170
|
},
|
|
144
171
|
null,
|
package/src/agent-runner.js
CHANGED
|
@@ -26,6 +26,7 @@ import { formatSkillsForPrompt, selectSkillsForGoal } from "./skill-library.js";
|
|
|
26
26
|
import { hostShellOption, platformInfo, platformLabel } from "./platform.js";
|
|
27
27
|
import { captureTmuxPane, listTmuxSessions, sendTmuxKeys, startTmuxSession } from "./tmux-tools.js";
|
|
28
28
|
import { languageInstruction } from "./i18n.js";
|
|
29
|
+
import { flushHousekeeping } from "./housekeeping.js";
|
|
29
30
|
|
|
30
31
|
const exec = promisify(execCallback);
|
|
31
32
|
const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
|
|
@@ -1256,6 +1257,7 @@ export async function runAgent(config) {
|
|
|
1256
1257
|
const sessionId = config.resume || config.sessionId || `web-agent-${crypto.randomUUID()}`;
|
|
1257
1258
|
const store = new SessionStore(config.sessionsDir, sessionId, {
|
|
1258
1259
|
projectRoot: config.baseDir,
|
|
1260
|
+
commandCwd: config.commandCwd,
|
|
1259
1261
|
projectSessionsDir: config.projectSessionsDir,
|
|
1260
1262
|
});
|
|
1261
1263
|
const client = createClient(config);
|
|
@@ -1283,6 +1285,11 @@ export async function runAgent(config) {
|
|
|
1283
1285
|
routeReason: config.routeReason,
|
|
1284
1286
|
goal: config.goal,
|
|
1285
1287
|
});
|
|
1288
|
+
await store.appendEvent("skills.selected", {
|
|
1289
|
+
taskProfile: config.taskProfile,
|
|
1290
|
+
skills: state.meta.selectedSkills || [],
|
|
1291
|
+
goal: config.goal,
|
|
1292
|
+
});
|
|
1286
1293
|
await store.saveState(state);
|
|
1287
1294
|
} else {
|
|
1288
1295
|
await store.appendEvent("session.resumed", { sessionId });
|
|
@@ -1650,5 +1657,6 @@ export async function runAgent(config) {
|
|
|
1650
1657
|
};
|
|
1651
1658
|
} finally {
|
|
1652
1659
|
await closeBrowser(browserState, store);
|
|
1660
|
+
await flushHousekeeping();
|
|
1653
1661
|
}
|
|
1654
1662
|
}
|
package/src/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ import { normalizeAuthProvider, promptHidden, runAuthWizard, shouldPromptForDeep
|
|
|
32
32
|
import { listSkills, selectSkillsForGoal } from "./skill-library.js";
|
|
33
33
|
import { languageLabel, resolveLanguage } from "./i18n.js";
|
|
34
34
|
import { maybeAutoUpdate } from "./auto-update.js";
|
|
35
|
+
import { readHousekeepingSummary } from "./housekeeping.js";
|
|
35
36
|
import fs from "node:fs/promises";
|
|
36
37
|
import path from "node:path";
|
|
37
38
|
import { fileURLToPath } from "node:url";
|
|
@@ -381,7 +382,7 @@ export function parseArgs(argv) {
|
|
|
381
382
|
|
|
382
383
|
function printUsage() {
|
|
383
384
|
console.log(
|
|
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"'
|
|
385
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti housekeeping [--json] 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"'
|
|
385
386
|
);
|
|
386
387
|
console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
|
|
387
388
|
}
|
|
@@ -499,6 +500,35 @@ function printSkills(query = "") {
|
|
|
499
500
|
}
|
|
500
501
|
}
|
|
501
502
|
|
|
503
|
+
async function printHousekeeping(argv = []) {
|
|
504
|
+
const summary = await readHousekeepingSummary();
|
|
505
|
+
if (argv.includes("--json")) {
|
|
506
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const totals = summary.capabilities?.totals || {};
|
|
510
|
+
console.log("AgInTiFlow housekeeping");
|
|
511
|
+
console.log(`root=${summary.paths.root}`);
|
|
512
|
+
console.log(`events=${summary.paths.eventsPath}`);
|
|
513
|
+
console.log(`capabilities=${summary.paths.capabilitiesPath}`);
|
|
514
|
+
console.log(
|
|
515
|
+
`totals events=${totals.events || 0} modelRequests=${totals.modelRequests || 0} toolEvents=${totals.toolEvents || 0} skillSelections=${totals.skillSelections || 0}`
|
|
516
|
+
);
|
|
517
|
+
const tools = Object.entries(summary.capabilities?.tools || {})
|
|
518
|
+
.sort((a, b) => (b[1].count || 0) - (a[1].count || 0))
|
|
519
|
+
.slice(0, 8)
|
|
520
|
+
.map(([name, item]) => `${name}:${item.count || 0}`)
|
|
521
|
+
.join(" ");
|
|
522
|
+
const skills = Object.entries(summary.capabilities?.skills || {})
|
|
523
|
+
.sort((a, b) => (b[1].count || 0) - (a[1].count || 0))
|
|
524
|
+
.slice(0, 8)
|
|
525
|
+
.map(([name, item]) => `${name}:${item.count || 0}`)
|
|
526
|
+
.join(" ");
|
|
527
|
+
if (tools) console.log(`topTools ${tools}`);
|
|
528
|
+
if (skills) console.log(`topSkills ${skills}`);
|
|
529
|
+
console.log("Set AGINTIFLOW_HOUSEKEEPING=0 to disable local sanitized housekeeping logs.");
|
|
530
|
+
}
|
|
531
|
+
|
|
502
532
|
function printInitResult(result) {
|
|
503
533
|
console.log(`AgInTiFlow project initialized: ${result.projectRoot}`);
|
|
504
534
|
console.log(`instructions=${result.instructionsPath}`);
|
|
@@ -1056,6 +1086,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1056
1086
|
return;
|
|
1057
1087
|
}
|
|
1058
1088
|
|
|
1089
|
+
if (argv[0] === "housekeeping" || argv[0] === "housekeeper") {
|
|
1090
|
+
await printHousekeeping(argv.slice(1));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1059
1094
|
if (argv[0] === "keys/status") {
|
|
1060
1095
|
await handleKeyCommand(["status"]);
|
|
1061
1096
|
return;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { agintiflowHome } from "./session-index.js";
|
|
6
|
+
import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
7
|
+
|
|
8
|
+
const MAX_PREVIEW_CHARS = 360;
|
|
9
|
+
const MAX_EVENTS_FILE_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
let queue = Promise.resolve();
|
|
11
|
+
|
|
12
|
+
function enabled() {
|
|
13
|
+
return String(process.env.AGINTIFLOW_HOUSEKEEPING || "1").toLowerCase() !== "0";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function housekeepingPaths(home = agintiflowHome()) {
|
|
17
|
+
const root = path.join(home, "housekeeping");
|
|
18
|
+
return {
|
|
19
|
+
root,
|
|
20
|
+
eventsPath: path.join(root, "events.jsonl"),
|
|
21
|
+
capabilitiesPath: path.join(root, "capabilities.json"),
|
|
22
|
+
readmePath: path.join(root, "README.md"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hashText(value = "") {
|
|
27
|
+
return crypto.createHash("sha256").update(String(value || "")).digest("hex").slice(0, 16);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function redactContextText(value = "", context = {}) {
|
|
31
|
+
let text = redactSensitiveText(value);
|
|
32
|
+
const replacements = [
|
|
33
|
+
[context.projectRoot, "$PROJECT"],
|
|
34
|
+
[context.commandCwd, "$CWD"],
|
|
35
|
+
[os.homedir(), "~"],
|
|
36
|
+
].filter(([needle]) => needle && typeof needle === "string");
|
|
37
|
+
for (const [needle, replacement] of replacements) {
|
|
38
|
+
text = text.split(needle).join(replacement);
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function previewText(value = "", context = {}, limit = MAX_PREVIEW_CHARS) {
|
|
44
|
+
const text = redactContextText(String(value ?? ""), context).replace(/\s+/g, " ").trim();
|
|
45
|
+
return text.length <= limit ? text : `${text.slice(0, Math.max(limit - 1, 1))}...`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitizePath(value = "", context = {}) {
|
|
49
|
+
const text = redactContextText(String(value || ""), context);
|
|
50
|
+
if (!text) return "";
|
|
51
|
+
if (text.startsWith("$PROJECT/") || text.startsWith("$CWD/") || text.startsWith("~/")) return text;
|
|
52
|
+
if (path.isAbsolute(text)) return path.basename(text);
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeArray(value) {
|
|
57
|
+
return Array.isArray(value) ? value.filter(Boolean) : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeProjectName(projectRoot = "") {
|
|
61
|
+
return projectRoot ? path.basename(projectRoot) : "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sanitizeToolCalls(toolCalls = [], context = {}) {
|
|
65
|
+
return safeArray(toolCalls).slice(0, 20).map((call) => {
|
|
66
|
+
const name = call.name || call.function?.name || "";
|
|
67
|
+
const args = call.arguments || call.function?.arguments || "";
|
|
68
|
+
return {
|
|
69
|
+
id: call.id ? hashText(call.id) : "",
|
|
70
|
+
name,
|
|
71
|
+
argumentsPreview: previewText(args, context, 260),
|
|
72
|
+
argumentsHash: args ? hashText(args) : "",
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sanitizeEvent(type, data = {}, context = {}) {
|
|
78
|
+
const redacted = redactValue(data || {});
|
|
79
|
+
const base = {
|
|
80
|
+
type,
|
|
81
|
+
sessionId: context.sessionId || "",
|
|
82
|
+
projectHash: context.projectRoot ? hashText(path.resolve(context.projectRoot)) : "",
|
|
83
|
+
projectName: safeProjectName(context.projectRoot),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (type === "session.created" || type === "session.resumed" || type === "session.finished" || type === "session.stopped") {
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
provider: redacted.provider || "",
|
|
90
|
+
model: redacted.model || "",
|
|
91
|
+
routingMode: redacted.routingMode || "",
|
|
92
|
+
routeReason: redacted.routeReason || "",
|
|
93
|
+
reason: redacted.reason || "",
|
|
94
|
+
mode: redacted.mode || "",
|
|
95
|
+
goalPreview: previewText(redacted.goal || "", context),
|
|
96
|
+
goalHash: redacted.goal ? hashText(redacted.goal) : "",
|
|
97
|
+
resultPreview: previewText(redacted.result || "", context),
|
|
98
|
+
resultHash: redacted.result ? hashText(redacted.result) : "",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (type === "skills.selected") {
|
|
103
|
+
return {
|
|
104
|
+
...base,
|
|
105
|
+
taskProfile: redacted.taskProfile || "",
|
|
106
|
+
skills: safeArray(redacted.skills).map(String).slice(0, 30),
|
|
107
|
+
goalHash: redacted.goal ? hashText(redacted.goal) : "",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (type === "model.requested") {
|
|
112
|
+
return {
|
|
113
|
+
...base,
|
|
114
|
+
step: redacted.step || 0,
|
|
115
|
+
provider: redacted.provider || "",
|
|
116
|
+
model: redacted.model || "",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (type === "model.responded") {
|
|
121
|
+
const content = String(redacted.content || "");
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
step: redacted.step || 0,
|
|
125
|
+
contentPreview: previewText(content, context),
|
|
126
|
+
contentHash: content ? hashText(content) : "",
|
|
127
|
+
contentChars: content.length,
|
|
128
|
+
toolCalls: sanitizeToolCalls(redacted.toolCalls, context),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (type.startsWith("tool.")) {
|
|
133
|
+
return {
|
|
134
|
+
...base,
|
|
135
|
+
toolName: redacted.toolName || "",
|
|
136
|
+
ok: redacted.ok,
|
|
137
|
+
blocked: Boolean(redacted.blocked),
|
|
138
|
+
category: redacted.commandPolicy?.category || redacted.category || "",
|
|
139
|
+
commandPreview: previewText(redacted.args?.command || redacted.command || "", context, 260),
|
|
140
|
+
path: sanitizePath(redacted.path || redacted.args?.path || "", context),
|
|
141
|
+
stdoutPreview: previewText(redacted.stdout || "", context, 220),
|
|
142
|
+
stderrPreview: previewText(redacted.stderr || redacted.error || redacted.reason || "", context, 220),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (type === "file.changed") {
|
|
147
|
+
const diff = String(redacted.diff || "");
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
toolName: redacted.toolName || "",
|
|
151
|
+
path: sanitizePath(redacted.path || "", context),
|
|
152
|
+
beforeHash: redacted.beforeHash || "",
|
|
153
|
+
afterHash: redacted.afterHash || "",
|
|
154
|
+
diffPreview: previewText(diff, context, 260),
|
|
155
|
+
diffHash: diff ? hashText(diff) : "",
|
|
156
|
+
skillTouched: /^skills\/[^/]+\/SKILL\.md$/.test(String(redacted.path || "")),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (type === "plan.created") {
|
|
161
|
+
const plan = String(redacted.plan || "");
|
|
162
|
+
return {
|
|
163
|
+
...base,
|
|
164
|
+
planPreview: previewText(plan, context),
|
|
165
|
+
planHash: plan ? hashText(plan) : "",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (type === "parallel_scouts.completed") {
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
model: redacted.model || "",
|
|
173
|
+
requested: redacted.requested || 0,
|
|
174
|
+
completed: redacted.completed || 0,
|
|
175
|
+
scoutNames: safeArray(redacted.scouts).map((scout) => scout.name || "").filter(Boolean).slice(0, 20),
|
|
176
|
+
synthesisHash: redacted.synthesis ? hashText(redacted.synthesis) : "",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (type.startsWith("canvas.") || type === "image.generated") {
|
|
181
|
+
return {
|
|
182
|
+
...base,
|
|
183
|
+
kind: redacted.kind || "",
|
|
184
|
+
title: previewText(redacted.title || "", context, 160),
|
|
185
|
+
path: sanitizePath(redacted.path || redacted.outputPath || redacted.artifactPath || "", context),
|
|
186
|
+
notePreview: previewText(redacted.note || redacted.preview || "", context, 220),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...base,
|
|
192
|
+
dataPreview: previewText(JSON.stringify(redacted), context),
|
|
193
|
+
dataHash: hashText(JSON.stringify(redacted)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function emptyCapabilities() {
|
|
198
|
+
return {
|
|
199
|
+
version: 1,
|
|
200
|
+
updatedAt: "",
|
|
201
|
+
totals: {
|
|
202
|
+
events: 0,
|
|
203
|
+
sessions: 0,
|
|
204
|
+
modelRequests: 0,
|
|
205
|
+
modelResponses: 0,
|
|
206
|
+
toolEvents: 0,
|
|
207
|
+
skillSelections: 0,
|
|
208
|
+
skillFileChanges: 0,
|
|
209
|
+
},
|
|
210
|
+
models: {},
|
|
211
|
+
tools: {},
|
|
212
|
+
skills: {},
|
|
213
|
+
sessions: {},
|
|
214
|
+
projects: {},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function readCapabilities(paths) {
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(await fs.readFile(paths.capabilitiesPath, "utf8"));
|
|
221
|
+
} catch {
|
|
222
|
+
return emptyCapabilities();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function bumpCounter(map, key, patch = {}) {
|
|
227
|
+
if (!key) return;
|
|
228
|
+
const current = map[key] || { count: 0 };
|
|
229
|
+
map[key] = {
|
|
230
|
+
...current,
|
|
231
|
+
...patch,
|
|
232
|
+
count: (current.count || 0) + 1,
|
|
233
|
+
lastSeenAt: patch.lastSeenAt || new Date().toISOString(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function maybeRotateEvents(paths) {
|
|
238
|
+
const stat = await fs.stat(paths.eventsPath).catch(() => null);
|
|
239
|
+
if (!stat || stat.size < MAX_EVENTS_FILE_BYTES) return;
|
|
240
|
+
const rotated = path.join(paths.root, `events-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
|
|
241
|
+
await fs.rename(paths.eventsPath, rotated).catch(() => {});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function ensureReadme(paths) {
|
|
245
|
+
await fs.writeFile(
|
|
246
|
+
paths.readmePath,
|
|
247
|
+
[
|
|
248
|
+
"# AgInTiFlow Housekeeping",
|
|
249
|
+
"",
|
|
250
|
+
"This folder stores local, redacted learning logs derived from session events.",
|
|
251
|
+
"",
|
|
252
|
+
"- `events.jsonl` is a sanitized cross-session event feed.",
|
|
253
|
+
"- `capabilities.json` aggregates models, tools, selected skills, and touched skill files.",
|
|
254
|
+
"- Full session history remains in `~/.agintiflow/sessions/<session-id>/` and should not be committed.",
|
|
255
|
+
"",
|
|
256
|
+
"The housekeeping files are local by default. Share only reviewed, sanitized capability packs.",
|
|
257
|
+
"",
|
|
258
|
+
].join("\n"),
|
|
259
|
+
"utf8"
|
|
260
|
+
).catch(() => {});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function recordEvent(record) {
|
|
264
|
+
const paths = housekeepingPaths();
|
|
265
|
+
await fs.mkdir(paths.root, { recursive: true });
|
|
266
|
+
await ensureReadme(paths);
|
|
267
|
+
await maybeRotateEvents(paths);
|
|
268
|
+
const timestamp = record.timestamp || new Date().toISOString();
|
|
269
|
+
await fs.appendFile(paths.eventsPath, `${JSON.stringify({ timestamp, ...record })}\n`, "utf8");
|
|
270
|
+
|
|
271
|
+
const capabilities = await readCapabilities(paths);
|
|
272
|
+
capabilities.updatedAt = timestamp;
|
|
273
|
+
capabilities.totals.events = (capabilities.totals.events || 0) + 1;
|
|
274
|
+
bumpCounter(capabilities.projects, record.projectHash || "unknown", {
|
|
275
|
+
name: record.projectName || "",
|
|
276
|
+
lastSeenAt: timestamp,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
capabilities.sessions ||= {};
|
|
280
|
+
if (record.sessionId) {
|
|
281
|
+
capabilities.sessions[record.sessionId] = {
|
|
282
|
+
projectHash: record.projectHash || "",
|
|
283
|
+
lastSeenAt: timestamp,
|
|
284
|
+
};
|
|
285
|
+
capabilities.totals.sessions = Object.keys(capabilities.sessions).length;
|
|
286
|
+
}
|
|
287
|
+
if (record.type === "model.requested") {
|
|
288
|
+
capabilities.totals.modelRequests = (capabilities.totals.modelRequests || 0) + 1;
|
|
289
|
+
bumpCounter(capabilities.models, `${record.provider}/${record.model}`, { lastSeenAt: timestamp });
|
|
290
|
+
}
|
|
291
|
+
if (record.type === "model.responded") {
|
|
292
|
+
capabilities.totals.modelResponses = (capabilities.totals.modelResponses || 0) + 1;
|
|
293
|
+
}
|
|
294
|
+
if (record.type?.startsWith("tool.")) {
|
|
295
|
+
capabilities.totals.toolEvents = (capabilities.totals.toolEvents || 0) + 1;
|
|
296
|
+
bumpCounter(capabilities.tools, record.toolName || "unknown", {
|
|
297
|
+
category: record.category || "",
|
|
298
|
+
lastSeenAt: timestamp,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (record.type === "skills.selected") {
|
|
302
|
+
capabilities.totals.skillSelections = (capabilities.totals.skillSelections || 0) + 1;
|
|
303
|
+
for (const skill of record.skills || []) {
|
|
304
|
+
bumpCounter(capabilities.skills, skill, { source: "selected", lastSeenAt: timestamp });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (record.type === "file.changed" && record.skillTouched) {
|
|
308
|
+
capabilities.totals.skillFileChanges = (capabilities.totals.skillFileChanges || 0) + 1;
|
|
309
|
+
const skillId = String(record.path || "").split("/")[1] || "unknown";
|
|
310
|
+
bumpCounter(capabilities.skills, skillId, { source: "skill-file-change", lastSeenAt: timestamp });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await fs.writeFile(paths.capabilitiesPath, `${JSON.stringify(capabilities, null, 2)}\n`, "utf8");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function enqueueHousekeepingEvent({ sessionId = "", projectRoot = "", commandCwd = "", event = {} } = {}) {
|
|
317
|
+
if (!enabled()) return;
|
|
318
|
+
const timestamp = event.timestamp || new Date().toISOString();
|
|
319
|
+
const context = { sessionId, projectRoot, commandCwd };
|
|
320
|
+
const record = {
|
|
321
|
+
timestamp,
|
|
322
|
+
...sanitizeEvent(event.type || "event", event.data || {}, context),
|
|
323
|
+
};
|
|
324
|
+
queue = queue.then(() => recordEvent(record)).catch(() => {});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function flushHousekeeping() {
|
|
328
|
+
await queue.catch(() => {});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function readHousekeepingSummary() {
|
|
332
|
+
const paths = housekeepingPaths();
|
|
333
|
+
return {
|
|
334
|
+
paths,
|
|
335
|
+
capabilities: await readCapabilities(paths),
|
|
336
|
+
};
|
|
337
|
+
}
|
package/src/interactive-cli.js
CHANGED
|
@@ -3016,6 +3016,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
3016
3016
|
|
|
3017
3017
|
const store = new SessionStore(config.sessionsDir, state.sessionId, {
|
|
3018
3018
|
projectRoot: config.baseDir,
|
|
3019
|
+
commandCwd: config.commandCwd,
|
|
3019
3020
|
projectSessionsDir: config.projectSessionsDir,
|
|
3020
3021
|
});
|
|
3021
3022
|
const liveInput = new LiveRunInput({ state, store, controller });
|
package/src/project.js
CHANGED
|
@@ -82,6 +82,7 @@ export function sessionStoreOptions(projectRoot = process.cwd(), sessionId = "")
|
|
|
82
82
|
const paths = projectPaths(projectRoot);
|
|
83
83
|
return {
|
|
84
84
|
projectRoot: paths.root,
|
|
85
|
+
commandCwd: paths.root,
|
|
85
86
|
projectSessionsDir: paths.sessionsDir,
|
|
86
87
|
legacySessionDir: sessionId ? path.join(paths.legacySessionsDir, sessionId) : "",
|
|
87
88
|
};
|
|
@@ -91,6 +92,10 @@ export async function ensureProjectSessionStorage(projectRoot = process.cwd()) {
|
|
|
91
92
|
const paths = projectPaths(projectRoot);
|
|
92
93
|
await fsp.mkdir(paths.sessionsDir, { recursive: true });
|
|
93
94
|
await fsp.mkdir(paths.globalSessionsDir, { recursive: true });
|
|
95
|
+
await ensureLine(paths.gitignorePath, [
|
|
96
|
+
".aginti-sessions/",
|
|
97
|
+
".sessions/",
|
|
98
|
+
]).catch(() => {});
|
|
94
99
|
|
|
95
100
|
const legacyEntries = await fsp.readdir(paths.legacySessionsDir, { withFileTypes: true }).catch(() => []);
|
|
96
101
|
if (legacyEntries.length > 0) {
|
package/src/session-store.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { deleteSessionIndex, globalSessionPaths, isSafeSessionId, upsertSessionIndex } from "./session-index.js";
|
|
5
|
+
import { enqueueHousekeepingEvent } from "./housekeeping.js";
|
|
5
6
|
|
|
6
7
|
export class SessionStore {
|
|
7
8
|
constructor(baseDir, sessionId, options = {}) {
|
|
@@ -9,6 +10,7 @@ export class SessionStore {
|
|
|
9
10
|
this.sessionId = sessionId;
|
|
10
11
|
const globalPaths = globalSessionPaths(sessionId);
|
|
11
12
|
this.projectRoot = options.projectRoot ? path.resolve(options.projectRoot) : "";
|
|
13
|
+
this.commandCwd = options.commandCwd ? path.resolve(options.commandCwd) : this.projectRoot;
|
|
12
14
|
this.projectSessionsDir = options.projectSessionsDir ? path.resolve(options.projectSessionsDir) : "";
|
|
13
15
|
this.legacySessionDir = options.legacySessionDir ? path.resolve(options.legacySessionDir) : "";
|
|
14
16
|
this.sessionDir = path.resolve(options.sessionDir || (baseDir ? path.join(this.baseDir, sessionId) : globalPaths.sessionDir));
|
|
@@ -99,12 +101,19 @@ export class SessionStore {
|
|
|
99
101
|
|
|
100
102
|
async appendEvent(type, data = {}) {
|
|
101
103
|
await this.ensure();
|
|
102
|
-
const
|
|
104
|
+
const event = {
|
|
103
105
|
timestamp: new Date().toISOString(),
|
|
104
106
|
type,
|
|
105
107
|
data,
|
|
106
|
-
}
|
|
108
|
+
};
|
|
109
|
+
const line = JSON.stringify(event);
|
|
107
110
|
await fs.appendFile(this.eventsPath, `${line}\n`, "utf8");
|
|
111
|
+
enqueueHousekeepingEvent({
|
|
112
|
+
sessionId: this.sessionId,
|
|
113
|
+
projectRoot: this.projectRoot,
|
|
114
|
+
commandCwd: this.commandCwd,
|
|
115
|
+
event,
|
|
116
|
+
});
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
async loadEvents() {
|