@technomoron/agent-run 0.99.42 → 0.99.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/CHANGES +14 -0
- package/README.md +23 -1
- package/dist/brain/server.js +1 -1
- package/dist/cli.js +20 -0
- package/dist/commands.js +18 -5
- package/dist/process.js +3 -3
- package/dist/runtime/brain-preflight.js +40 -0
- package/dist/runtime/clean-sessions.js +230 -0
- package/dist/utils.js +6 -0
- package/package.json +2 -2
package/CHANGES
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
CHANGES
|
|
2
2
|
=======
|
|
3
3
|
|
|
4
|
+
Version 0.99.44 (2026-10-14)
|
|
5
|
+
----------------------------
|
|
6
|
+
* Remove local agent sessions last modified more than seven days ago before
|
|
7
|
+
startup, including Codex SQLite history, using native deletion where needed.
|
|
8
|
+
* Add --clean to delete all saved sessions for selected agents while preserving
|
|
9
|
+
authentication, settings, skills and brain knowledge.
|
|
10
|
+
* Check the configured brain MCP connection before launching an agent, and
|
|
11
|
+
explain socket ownership and permission failures.
|
|
12
|
+
|
|
13
|
+
Version 0.99.43 (2026-09-25)
|
|
14
|
+
----------------------------
|
|
15
|
+
* Report startup errors without a stack trace and include file ownership and
|
|
16
|
+
permissions guidance when access is denied.
|
|
17
|
+
|
|
4
18
|
Version 0.99.42 (2026-09-25)
|
|
5
19
|
----------------------------
|
|
6
20
|
* Apply repository AI-file checks only to project scopes and actual repositories.
|
package/README.md
CHANGED
|
@@ -281,7 +281,7 @@ Config root:
|
|
|
281
281
|
|
|
282
282
|
## Commands
|
|
283
283
|
|
|
284
|
-
Global
|
|
284
|
+
Global flags:
|
|
285
285
|
|
|
286
286
|
- `-h`, `--help`: show wrapper help for `agent-run` and the built-in commands
|
|
287
287
|
- `-v`, `--verbose`: print path resolution, file creation, include expansion,
|
|
@@ -290,6 +290,26 @@ Global flag:
|
|
|
290
290
|
`agent-run codex --help`, `agent-run claude --help`, `agent-run gemini --help`,
|
|
291
291
|
and `agent-run grok --help` pass `--help` through to the underlying tool.
|
|
292
292
|
|
|
293
|
+
Agent launches purge local sessions last modified more than seven days ago for all
|
|
294
|
+
four agents. This covers native user homes and the profiles under the selected
|
|
295
|
+
configuration root. Sessions exactly seven days old are retained. Authentication,
|
|
296
|
+
settings, skills and brain knowledge are preserved.
|
|
297
|
+
|
|
298
|
+
Delete all saved sessions for selected agents without launching an agent:
|
|
299
|
+
|
|
300
|
+
```sh
|
|
301
|
+
agent-run --clean all
|
|
302
|
+
agent-run --clean codex,claude
|
|
303
|
+
agent-run --clean grok gemini
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`--configdir PATH` selects the configuration tree to scan. The native user homes
|
|
307
|
+
are also included. Codex and Grok deletion requires an installed CLI with its
|
|
308
|
+
native deletion command; Codex SQLite history is included. Failed or unsupported
|
|
309
|
+
deletions are reported and retained; explicit cleanup exits with status 1 if any
|
|
310
|
+
operation fails. A current session identified in the environment is skipped.
|
|
311
|
+
Symlinks and paths owned by a different user are not cleaned.
|
|
312
|
+
|
|
293
313
|
For any native agent command, use `--generate` to generate profile files
|
|
294
314
|
without launching the underlying tool.
|
|
295
315
|
|
|
@@ -1067,6 +1087,8 @@ The supplied service uses `serve --pull`. On startup this pulls the configuratio
|
|
|
1067
1087
|
repository with `--ff-only --no-rebase` when it is clean and has an upstream.
|
|
1068
1088
|
Dirty checkouts and missing upstreams are skipped. Divergence or network failures
|
|
1069
1089
|
are reported in the service journal and the service continues with local files.
|
|
1090
|
+
Before launching any configured agent, agent-run initializes its enabled brain MCP connection and calls `brain_status`. If this fails, launch stops with the underlying error. A disabled brain MCP connection also blocks launch when brain is enabled. Run the agent and brain service as the same user; private socket ownership is enforced. `--none` remains an explicit configuration bypass.
|
|
1091
|
+
|
|
1070
1092
|
Startup never commits, pushes, stashes, or rebases local work. Omit `--pull` to
|
|
1071
1093
|
disable this behavior for a manually launched service.
|
|
1072
1094
|
|
package/dist/brain/server.js
CHANGED
|
@@ -116,7 +116,7 @@ function createBrainServer(store) {
|
|
|
116
116
|
function requirePrivateSocketDirectory(socketPath) {
|
|
117
117
|
const stat = fs.lstatSync(path.dirname(socketPath));
|
|
118
118
|
if (!stat.isDirectory() || stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0)
|
|
119
|
-
throw new Error(
|
|
119
|
+
throw new Error(`The socket directory ${path.dirname(socketPath)} must be owned by the current user with mode 0700 (current uid ${process.getuid()}, owner uid ${stat.uid}, mode ${(stat.mode & 0o777).toString(8)}). Run agent-run and agent-brain as the same user.`);
|
|
120
120
|
}
|
|
121
121
|
async function serveApiCore(configRoot, socketPath) {
|
|
122
122
|
if (process.platform !== 'win32') {
|
package/dist/cli.js
CHANGED
|
@@ -12,11 +12,26 @@ function parseInvocation(invokedTool, argv) {
|
|
|
12
12
|
let command = normalizeCommandName(invokedTool);
|
|
13
13
|
const options = extractGlobalOptions(argv);
|
|
14
14
|
applyGlobalOptions(options);
|
|
15
|
+
if (command === null && (options.args[0] === '--clean' || options.args[0]?.startsWith('--clean='))) {
|
|
16
|
+
return parseCleanCommand(options.args);
|
|
17
|
+
}
|
|
15
18
|
if (command === null) {
|
|
16
19
|
command = extractCommand(options.args);
|
|
17
20
|
}
|
|
18
21
|
return parseCommand(command, options.args);
|
|
19
22
|
}
|
|
23
|
+
function parseCleanCommand(args) {
|
|
24
|
+
const flag = args[0];
|
|
25
|
+
const values = [
|
|
26
|
+
...(flag.includes('=') ? [flag.slice('--clean='.length)] : []),
|
|
27
|
+
...args.slice(1)
|
|
28
|
+
].flatMap((value) => value.split(','));
|
|
29
|
+
if (!values.length || values.some((value) => value !== 'all' && !types_1.AGENT_IDS.includes(value))
|
|
30
|
+
|| (values.includes('all') && values.length !== 1)) {
|
|
31
|
+
(0, utils_1.fail)('--clean requires all or a list of codex, claude, gemini, grok');
|
|
32
|
+
}
|
|
33
|
+
return { command: 'clean', agents: values[0] === 'all' ? [...types_1.AGENT_IDS] : [...new Set(values)] };
|
|
34
|
+
}
|
|
20
35
|
function applyGlobalOptions(options) {
|
|
21
36
|
if (options.verbose) {
|
|
22
37
|
process.env[constants_1.VERBOSE_ENV] = '1';
|
|
@@ -52,6 +67,8 @@ function parseCommand(command, args) {
|
|
|
52
67
|
case 'create':
|
|
53
68
|
case 'project':
|
|
54
69
|
return { command, args };
|
|
70
|
+
case 'clean':
|
|
71
|
+
(0, utils_1.fail)('Use agent-run --clean all|codex,claude,gemini,grok');
|
|
55
72
|
case 'compress':
|
|
56
73
|
return parseCompressCommand(args);
|
|
57
74
|
case 'check':
|
|
@@ -156,6 +173,8 @@ function parseRunCommand(command, inputArgs) {
|
|
|
156
173
|
const args = [];
|
|
157
174
|
let passthrough = false;
|
|
158
175
|
for (const arg of inputArgs) {
|
|
176
|
+
if (!passthrough && (arg === '--clean' || arg.startsWith('--clean=')))
|
|
177
|
+
(0, utils_1.fail)('Use --clean on its own: agent-run --clean all|codex,claude,gemini,grok');
|
|
159
178
|
if (passthrough) {
|
|
160
179
|
args.push(arg);
|
|
161
180
|
continue;
|
|
@@ -380,6 +399,7 @@ function renderHelp(topic) {
|
|
|
380
399
|
' Run gemini with generated private config',
|
|
381
400
|
' grok [--none] [--create] [--local] [--show] [--generate] [--yolo|--sandboxed] [args...]',
|
|
382
401
|
' Run grok with generated private config',
|
|
402
|
+
' --clean all|AGENTS Delete saved sessions for selected agents',
|
|
383
403
|
' compress [--agent NAME] [--scope SCOPE] [--dry-run] Consolidate brain knowledge',
|
|
384
404
|
' setup [org/repo] Install the default tree and selected profile',
|
|
385
405
|
' check [--all] [path] Validate generated profile output',
|
package/dist/commands.js
CHANGED
|
@@ -13,14 +13,25 @@ const process_1 = require("./process");
|
|
|
13
13
|
const project_1 = require("./project");
|
|
14
14
|
const renderer_1 = require("./renderer");
|
|
15
15
|
const spawn_agent_1 = require("./runtime/spawn-agent");
|
|
16
|
+
const brain_preflight_1 = require("./runtime/brain-preflight");
|
|
17
|
+
const clean_sessions_1 = require("./runtime/clean-sessions");
|
|
16
18
|
const utils_1 = require("./utils");
|
|
17
19
|
function main(invokedTool, argv) {
|
|
18
|
-
|
|
20
|
+
try {
|
|
21
|
+
dispatch((0, cli_1.parseInvocation)(invokedTool, argv));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
process.stderr.write(`agent-run: ${(0, utils_1.formatError)(error)}\n`);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
19
27
|
}
|
|
20
28
|
function dispatch(command) {
|
|
21
29
|
switch (command.command) {
|
|
30
|
+
case 'clean':
|
|
31
|
+
(0, clean_sessions_1.reportCleanup)((0, clean_sessions_1.cleanSessions)((0, project_1.defaultConfigRoot)(process.cwd()), command.agents, true), true);
|
|
32
|
+
return;
|
|
22
33
|
case 'compress':
|
|
23
|
-
void runCompress(command).catch((error) => { process.stderr.write(`agent-run: ${
|
|
34
|
+
void runCompress(command).catch((error) => { process.stderr.write(`agent-run: ${(0, utils_1.formatError)(error)}\n`); process.exitCode = 1; });
|
|
24
35
|
return;
|
|
25
36
|
case 'mcp':
|
|
26
37
|
case 'create':
|
|
@@ -51,7 +62,7 @@ function dispatch(command) {
|
|
|
51
62
|
runMigrateConfig(command);
|
|
52
63
|
return;
|
|
53
64
|
default:
|
|
54
|
-
runTool(command);
|
|
65
|
+
void runTool(command).catch((error) => { process.stderr.write(`agent-run: ${(0, utils_1.formatError)(error)}\n`); process.exitCode = 1; });
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
68
|
async function runCompress(command) {
|
|
@@ -90,11 +101,11 @@ async function runCompress(command) {
|
|
|
90
101
|
if (process.platform === 'win32')
|
|
91
102
|
prompt = prompt.replace(/\r?\n/g, ' ');
|
|
92
103
|
// Use the normal adapters, configuration and permission modes. The prompt is one argv value.
|
|
93
|
-
runTool({ command: agent, args: agent === 'gemini' ? ['-i', prompt] : [prompt], wrapperArgs: {
|
|
104
|
+
await runTool({ command: agent, args: agent === 'gemini' ? ['-i', prompt] : [prompt], wrapperArgs: {
|
|
94
105
|
none: false, create: false, local: false, show: false, generate: false, sandboxMode: null, codexNetwork: false
|
|
95
106
|
} });
|
|
96
107
|
}
|
|
97
|
-
function runTool(parsed) {
|
|
108
|
+
async function runTool(parsed) {
|
|
98
109
|
const { args, command, wrapperArgs } = parsed;
|
|
99
110
|
const adapter = (0, registry_1.getAgentAdapter)(command);
|
|
100
111
|
const projectRoot = (0, project_1.findProjectRoot)(process.cwd());
|
|
@@ -138,11 +149,13 @@ function runTool(parsed) {
|
|
|
138
149
|
}
|
|
139
150
|
checkLocalAiFiles(command, projectRoot, agentDir, preview.context, wrapperArgs.local);
|
|
140
151
|
const realBinary = (0, process_1.findRealBinary)(command);
|
|
152
|
+
await (0, brain_preflight_1.requireBrain)(preview.context);
|
|
141
153
|
const rendered = (0, renderer_1.syncRenderedProfile)(preview);
|
|
142
154
|
const runtime = rendered.runtimes[command];
|
|
143
155
|
if (!runtime) {
|
|
144
156
|
(0, utils_1.fail)(`no generated ${command} runtime found for profile ${rendered.profile}`);
|
|
145
157
|
}
|
|
158
|
+
(0, clean_sessions_1.reportCleanup)((0, clean_sessions_1.cleanSessions)(configRoot));
|
|
146
159
|
(0, spawn_agent_1.spawnAgent)(adapter.spawn(runtime, { binary: realBinary, passthroughArgs: args, wrapperArgs }));
|
|
147
160
|
}
|
|
148
161
|
function runStatus() {
|
package/dist/process.js
CHANGED
|
@@ -9,8 +9,8 @@ const path = require("path");
|
|
|
9
9
|
const constants_1 = require("./constants");
|
|
10
10
|
const utils_1 = require("./utils");
|
|
11
11
|
const crossSpawn = require('cross-spawn');
|
|
12
|
-
function findRealBinary(tool) {
|
|
13
|
-
const pathDirs =
|
|
12
|
+
function findRealBinary(tool, searchPath = process.env.PATH ?? '') {
|
|
13
|
+
const pathDirs = searchPath.split(path.delimiter).filter(Boolean);
|
|
14
14
|
const currentScriptArg = process.argv[1];
|
|
15
15
|
const currentScript = currentScriptArg ? fs.realpathSync(currentScriptArg) : '';
|
|
16
16
|
const wrappers = wrapperCandidates(tool);
|
|
@@ -31,7 +31,7 @@ function findRealBinary(tool) {
|
|
|
31
31
|
return candidate;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
throw new Error(`no ${tool} binary found in PATH`);
|
|
35
35
|
}
|
|
36
36
|
function wrapperCandidates(tool) {
|
|
37
37
|
const names = [
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireBrain = requireBrain;
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
6
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
7
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
8
|
+
const config_1 = require("../brain/config");
|
|
9
|
+
const environment_1 = require("./environment");
|
|
10
|
+
async function requireBrain(context) {
|
|
11
|
+
const server = context.mcpServers['agent-brain'];
|
|
12
|
+
if (!server?.enabled) {
|
|
13
|
+
if ((0, config_1.readBrainConfig)(context.configRoot)?.enabled)
|
|
14
|
+
throw new Error('Cannot start agent: agent-brain is enabled but its MCP connection is disabled or missing.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const client = new index_js_1.Client({ name: 'agent-run-preflight', version: '1.0.0' });
|
|
18
|
+
let diagnostics = '';
|
|
19
|
+
const transport = server.transport === 'stdio'
|
|
20
|
+
? new stdio_js_1.StdioClientTransport({ command: server.command ?? '', args: server.args, cwd: server.cwd ?? context.projectRoot,
|
|
21
|
+
env: Object.fromEntries(Object.entries({ ...(0, environment_1.buildAgentEnvironment)(context), ...server.env }).filter((entry) => entry[1] !== undefined)), stderr: 'pipe' })
|
|
22
|
+
: server.transport === 'http'
|
|
23
|
+
? new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(server.url ?? ''), { requestInit: { headers: server.headers } })
|
|
24
|
+
: new sse_js_1.SSEClientTransport(new URL(server.url ?? ''), { requestInit: { headers: server.headers } });
|
|
25
|
+
if (transport instanceof stdio_js_1.StdioClientTransport)
|
|
26
|
+
transport.stderr?.on('data', (chunk) => { diagnostics = (diagnostics + chunk.toString()).slice(-8192); });
|
|
27
|
+
try {
|
|
28
|
+
await client.connect(transport, { timeout: 10000 });
|
|
29
|
+
const status = await client.callTool({ name: 'brain_status', arguments: {} }, undefined, { timeout: 10000 });
|
|
30
|
+
if (status.isError)
|
|
31
|
+
throw new Error(JSON.stringify(status.content));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new Error(`Cannot start agent: agent-brain MCP check failed: ${error instanceof Error ? error.message : String(error)}${diagnostics.trim() ? `\n${diagnostics.trim()}` : ''}`);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await client.close();
|
|
38
|
+
await transport.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cleanSessions = cleanSessions;
|
|
4
|
+
exports.reportCleanup = reportCleanup;
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const node_sqlite_1 = require("node:sqlite");
|
|
9
|
+
const cross_spawn_1 = require("cross-spawn");
|
|
10
|
+
const process_1 = require("../process");
|
|
11
|
+
const types_1 = require("../agents/types");
|
|
12
|
+
const config_tree_1 = require("../config-tree");
|
|
13
|
+
const manifest_1 = require("../manifest");
|
|
14
|
+
const templates_1 = require("../templates");
|
|
15
|
+
const project_1 = require("../project");
|
|
16
|
+
const WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
+
// Do not follow symlinks, including symlinked parent directories or another user's home.
|
|
19
|
+
function owned(file) {
|
|
20
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
21
|
+
if (!stat || stat.isSymbolicLink())
|
|
22
|
+
return undefined;
|
|
23
|
+
if (process.getuid && stat.uid !== process.getuid())
|
|
24
|
+
throw new Error(`Session path belongs to uid ${stat.uid}, current uid is ${process.getuid()}: ${file}`);
|
|
25
|
+
const parent = path.dirname(file);
|
|
26
|
+
if (parent !== file) {
|
|
27
|
+
const parentStat = fs.lstatSync(parent);
|
|
28
|
+
if (parentStat.isSymbolicLink())
|
|
29
|
+
return undefined;
|
|
30
|
+
// Shared ancestors such as /tmp need not belong to the user.
|
|
31
|
+
for (let dir = path.dirname(parent); path.dirname(dir) !== dir; dir = path.dirname(dir)) {
|
|
32
|
+
if (fs.lstatSync(dir).isSymbolicLink())
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return stat;
|
|
37
|
+
}
|
|
38
|
+
function entries(dir) {
|
|
39
|
+
return owned(dir)?.isDirectory() ? fs.readdirSync(dir, { withFileTypes: true }) : [];
|
|
40
|
+
}
|
|
41
|
+
function files(dir) {
|
|
42
|
+
return entries(dir).flatMap((entry) => {
|
|
43
|
+
const file = path.join(dir, entry.name);
|
|
44
|
+
return entry.isDirectory() ? files(file) : entry.isFile() && owned(file)?.isFile() ? [file] : [];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function modified(paths) {
|
|
48
|
+
return Math.max(...paths.map((file) => owned(file)?.mtimeMs ?? Infinity));
|
|
49
|
+
}
|
|
50
|
+
function homes(configRoot, agent) {
|
|
51
|
+
const native = process.env[agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : agent === 'gemini' ? 'GEMINI_CLI_HOME' : agent === 'grok' ? 'GROK_HOME' : 'CODEX_HOME'];
|
|
52
|
+
const result = [path.join(os.homedir(), `.${agent}`)];
|
|
53
|
+
if (native)
|
|
54
|
+
result.push(agent === 'gemini' ? path.join(native, '.gemini') : native);
|
|
55
|
+
if (agent === 'codex')
|
|
56
|
+
result.push(path.join(configRoot, 'runtime', 'codex'));
|
|
57
|
+
for (const profile of (0, config_tree_1.findProfileDirs)(configRoot)) {
|
|
58
|
+
const live = path.join(profile, 'live');
|
|
59
|
+
if (agent === 'codex') {
|
|
60
|
+
const profileName = path.relative(configRoot, profile).split(path.sep).join('/');
|
|
61
|
+
const manifest = (0, manifest_1.normalizeManifest)((0, manifest_1.loadManifest)(configRoot, profile, profileName), profileName);
|
|
62
|
+
const context = (0, templates_1.buildRenderContext)((0, project_1.projectRootForProfile)(profileName, configRoot), profile, configRoot, manifest, (0, templates_1.createNunjucksEnv)(configRoot));
|
|
63
|
+
result.push(context.paths.codexHomeDir);
|
|
64
|
+
}
|
|
65
|
+
if (agent === 'gemini')
|
|
66
|
+
result.push(path.join(live, 'gemini', 'home', '.gemini'));
|
|
67
|
+
if (agent === 'grok')
|
|
68
|
+
result.push(path.join(live, 'grok'));
|
|
69
|
+
}
|
|
70
|
+
return [...new Set(result.map((home) => {
|
|
71
|
+
const absolute = path.resolve(home);
|
|
72
|
+
const stat = fs.lstatSync(absolute, { throwIfNoEntry: false });
|
|
73
|
+
// Resolve system aliases such as macOS /var, but never follow a home symlink.
|
|
74
|
+
return stat && !stat.isSymbolicLink() ? fs.realpathSync(absolute) : absolute;
|
|
75
|
+
}))];
|
|
76
|
+
}
|
|
77
|
+
function fileSessions(home, agent) {
|
|
78
|
+
if (agent === 'grok') {
|
|
79
|
+
return entries(path.join(home, 'sessions')).filter((entry) => entry.isDirectory()).flatMap((group) => entries(path.join(home, 'sessions', group.name)).filter((entry) => entry.isDirectory() && UUID.test(entry.name)).map((entry) => {
|
|
80
|
+
const dir = path.join(home, 'sessions', group.name, entry.name);
|
|
81
|
+
return { id: entry.name, files: [dir], modified: modified([dir, ...files(dir)]) };
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
if (agent === 'claude') {
|
|
85
|
+
return entries(path.join(home, 'projects')).filter((entry) => entry.isDirectory()).flatMap((group) => {
|
|
86
|
+
const dir = path.join(home, 'projects', group.name);
|
|
87
|
+
return entries(dir).filter((entry) => entry.isFile() && UUID.test(path.basename(entry.name, '.jsonl')) && entry.name.endsWith('.jsonl')).map((entry) => {
|
|
88
|
+
const id = path.basename(entry.name, '.jsonl');
|
|
89
|
+
const transcript = path.join(dir, entry.name);
|
|
90
|
+
const companions = [path.join(dir, id), path.join(home, 'file-history', id)].filter((file) => owned(file)?.isDirectory());
|
|
91
|
+
return { id, files: [transcript, ...companions], modified: modified([transcript, ...companions.flatMap(files)]) };
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (agent === 'gemini') {
|
|
96
|
+
return entries(path.join(home, 'tmp')).filter((entry) => entry.isDirectory()).flatMap((group) => {
|
|
97
|
+
const dir = path.join(home, 'tmp', group.name, 'chats');
|
|
98
|
+
return entries(dir).filter((entry) => entry.isFile() && entry.name.endsWith('.json')).map((entry) => {
|
|
99
|
+
const file = path.join(dir, entry.name);
|
|
100
|
+
return { id: entry.name, files: [file], modified: modified([file]) };
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const sessions = new Map();
|
|
105
|
+
for (const dir of ['sessions', 'archived_sessions']) {
|
|
106
|
+
for (const file of files(path.join(home, dir)).filter((file) => file.endsWith('.jsonl'))) {
|
|
107
|
+
const id = path.basename(file, '.jsonl').slice(-36);
|
|
108
|
+
if (!UUID.test(id))
|
|
109
|
+
continue;
|
|
110
|
+
const previous = sessions.get(id);
|
|
111
|
+
sessions.set(id, { id, files: [...previous?.files ?? [], file], modified: Math.max(previous?.modified ?? 0, modified([file])) });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Recent Codex keeps paginated history in SQLite; rollout files alone are incomplete.
|
|
115
|
+
for (const entry of entries(home).filter((entry) => entry.isFile() && /^state_\d+\.sqlite$/.test(entry.name))) {
|
|
116
|
+
const db = new node_sqlite_1.DatabaseSync(path.join(home, entry.name), { readOnly: true });
|
|
117
|
+
try {
|
|
118
|
+
for (const row of db.prepare('SELECT id, updated_at FROM threads').all()) {
|
|
119
|
+
if (typeof row.id !== 'string' || !UUID.test(row.id))
|
|
120
|
+
continue;
|
|
121
|
+
const previous = sessions.get(row.id);
|
|
122
|
+
sessions.set(row.id, { id: row.id, files: previous?.files ?? [], modified: Math.max(previous?.modified ?? 0, Number(row.updated_at) * 1000) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
db.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...sessions.values()];
|
|
130
|
+
}
|
|
131
|
+
function pruneIndexes(home, agent, ids) {
|
|
132
|
+
const jsonl = agent === 'claude' ? ['history.jsonl'] : agent === 'codex' ? ['history.jsonl', 'session_index.jsonl'] : [];
|
|
133
|
+
for (const name of jsonl) {
|
|
134
|
+
const file = path.join(home, name);
|
|
135
|
+
if (!owned(file)?.isFile())
|
|
136
|
+
continue;
|
|
137
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
138
|
+
const lines = original.split(/(?<=\n)/).filter((line) => {
|
|
139
|
+
try {
|
|
140
|
+
const row = JSON.parse(line);
|
|
141
|
+
return !ids.has(row.sessionId ?? row.session_id ?? row.id);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (lines.join('') !== original)
|
|
148
|
+
fs.writeFileSync(file, lines.join(''));
|
|
149
|
+
}
|
|
150
|
+
if (agent === 'claude') {
|
|
151
|
+
for (const group of entries(path.join(home, 'projects')).filter((entry) => entry.isDirectory())) {
|
|
152
|
+
const file = path.join(home, 'projects', group.name, 'sessions-index.json');
|
|
153
|
+
if (!owned(file)?.isFile())
|
|
154
|
+
continue;
|
|
155
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
156
|
+
if (Array.isArray(data.entries)) {
|
|
157
|
+
data.entries = data.entries.filter((entry) => !ids.has(entry.sessionId ?? ''));
|
|
158
|
+
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function cleanSessions(configRoot, agents = types_1.AGENT_IDS, all = false, now = Date.now()) {
|
|
164
|
+
return agents.map((agent) => {
|
|
165
|
+
const result = { agent, removed: 0, errors: [] };
|
|
166
|
+
let directories;
|
|
167
|
+
try {
|
|
168
|
+
directories = homes(configRoot, agent);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
for (const home of directories) {
|
|
175
|
+
try {
|
|
176
|
+
if (!owned(home)?.isDirectory())
|
|
177
|
+
continue;
|
|
178
|
+
const removed = new Set();
|
|
179
|
+
for (const session of fileSessions(home, agent)) {
|
|
180
|
+
if (!all && session.modified >= now - WEEK)
|
|
181
|
+
continue;
|
|
182
|
+
if ([process.env.CODEX_THREAD_ID, process.env.CODEX_SESSION_ID, process.env.CLAUDE_SESSION_ID, process.env.GROK_SESSION_ID].includes(session.id)) {
|
|
183
|
+
if (all)
|
|
184
|
+
result.errors.push(`Skipped current session ${session.id}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
if (agent === 'codex' || agent === 'grok') {
|
|
189
|
+
// Let the native CLI remove history and update its own databases/indexes.
|
|
190
|
+
const binary = (0, process_1.findRealBinary)(agent, process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '');
|
|
191
|
+
const response = (0, cross_spawn_1.sync)(binary, agent === 'codex' ? ['delete', '--force', session.id] : ['sessions', 'delete', session.id], {
|
|
192
|
+
env: { ...process.env, PATH: process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH, [agent === 'codex' ? 'CODEX_HOME' : 'GROK_HOME']: home }, shell: false, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024
|
|
193
|
+
});
|
|
194
|
+
if (response.error || response.status !== 0)
|
|
195
|
+
throw new Error(response.error?.message ?? (response.stderr || response.stdout || `delete exited ${response.status}`).trim());
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
for (const file of session.files)
|
|
199
|
+
if (owned(file))
|
|
200
|
+
fs.rmSync(file, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
if (session.files.some((file) => fs.existsSync(file)))
|
|
203
|
+
throw new Error('Native deletion left session files in place');
|
|
204
|
+
removed.add(session.id);
|
|
205
|
+
result.removed++;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
result.errors.push(`${home}: ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (removed.size)
|
|
212
|
+
pruneIndexes(home, agent, removed);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
result.errors.push(`${home}: ${error instanceof Error ? error.message : String(error)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function reportCleanup(results, explicit = false) {
|
|
222
|
+
for (const result of results) {
|
|
223
|
+
if (explicit || result.removed)
|
|
224
|
+
process.stdout.write(`${result.agent}: removed ${result.removed} session(s)\n`);
|
|
225
|
+
for (const error of result.errors)
|
|
226
|
+
process.stderr.write(`agent-run: ${result.agent} session cleanup: ${error}\n`);
|
|
227
|
+
}
|
|
228
|
+
if (explicit && results.some((result) => result.errors.length))
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
}
|
package/dist/utils.js
CHANGED
|
@@ -35,6 +35,12 @@ function localDateString() {
|
|
|
35
35
|
return `${year}-${month}-${day}`;
|
|
36
36
|
}
|
|
37
37
|
function formatError(error) {
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
const { code, path: filePath } = error;
|
|
40
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
41
|
+
return `${error.message}\nCheck file ownership and permissions${filePath ? ` for ${filePath}` : ''} and its parent directories. Run agent-run as your normal user, not with sudo.`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
38
44
|
return error instanceof Error ? error.message : String(error);
|
|
39
45
|
}
|
|
40
46
|
function verbose(message) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/agent-run",
|
|
3
|
-
"version": "0.99.
|
|
3
|
+
"version": "0.99.44",
|
|
4
4
|
"packageManager": "pnpm@10.34.5",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-run": "dist/agent-run.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"cleanbuild": "run-s clean:dist build",
|
|
37
37
|
"prepack": "run-s build",
|
|
38
38
|
"test": "run-s build test:codex-profiles test:integration test:release",
|
|
39
|
-
"test:codex-profiles": "node --test scripts/test-codex-profiles.cjs",
|
|
39
|
+
"test:codex-profiles": "node --test scripts/test-codex-profiles.cjs scripts/test-session-cleanup.cjs",
|
|
40
40
|
"test:brain": "node --test scripts/test-brain.cjs",
|
|
41
41
|
"test:integration": "bash ./scripts/test.sh",
|
|
42
42
|
"test:release": "bash ./scripts/test-release.sh",
|