@adrive/cli 1.1.1 → 1.1.2
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 +1 -1
- package/dist/commands/hook.js +19 -12
- package/dist/commands/init.js +15 -3
- package/dist/commands/resume.js +5 -2
- package/dist/lib/auto-context.js +9 -7
- package/dist/lib/config.js +9 -0
- package/dist/lib/detect-agents.js +4 -1
- package/dist/lib/harness-wiring.js +34 -10
- package/dist/lib/sync.js +13 -7
- package/dist/lib/transcripts/codex.js +4 -3
- package/dist/lib/transcripts/opencode.js +6 -2
- package/package.json +3 -8
package/README.md
CHANGED
package/dist/commands/hook.js
CHANGED
|
@@ -7,16 +7,19 @@ import { HARNESSES } from '../lib/sync.js';
|
|
|
7
7
|
import { recordSessionCwd } from '../lib/session-cwd.js';
|
|
8
8
|
import { asObj, asStr, truncate } from '../lib/transcripts/util.js';
|
|
9
9
|
// Entry point for every harness lifecycle hook. Hooks run inline with the
|
|
10
|
-
// agent (Codex
|
|
10
|
+
// agent (Codex clamps hook timeouts to 3s), so this answers fast, hands the real
|
|
11
11
|
// work to a detached `adrive sync`, never throws, and always exits 0. stdout
|
|
12
12
|
// is reserved for the harness's JSON protocol.
|
|
13
13
|
const STDIN_TIMEOUT_MS = 1000;
|
|
14
14
|
const CONTEXT_TIMEOUT_MS = 1500;
|
|
15
15
|
const MAX_CONTEXT_SESSIONS = 3;
|
|
16
|
+
const CONTEXT_FETCH = 20;
|
|
16
17
|
const NOT_CONNECTED = 'AgentDrive is installed but not connected: run `adrive auth login`.';
|
|
17
18
|
const START_EVENTS = new Set(['SessionStart', 'sessionStart', 'session.created']);
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// OpenCode has no end event and its idle often lands within the debounce of
|
|
20
|
+
// session.created, so a debounced idle would leave a one-shot session empty.
|
|
21
|
+
const FORCE_EVENTS = new Set(['SessionEnd', 'sessionEnd', 'PreCompact', 'preCompact', 'PreCompress', 'session.idle']);
|
|
22
|
+
const TURN_EVENTS = new Set(['Stop', 'stop', 'AfterAgent']);
|
|
20
23
|
export function classifyEvent(name) {
|
|
21
24
|
if (START_EVENTS.has(name))
|
|
22
25
|
return 'start';
|
|
@@ -42,11 +45,14 @@ function age(ms) {
|
|
|
42
45
|
}
|
|
43
46
|
async function recentSessions(cfg, harness, cwd, sessionId, now) {
|
|
44
47
|
const client = new CliApiClient({ baseUrl: cfg.api_url, apiKey: cfg.api_key, agentPlatform: harness, agentRole: cfg.agent_role });
|
|
45
|
-
const runs = await client.listRuns({ project: scopeProjectKey(projectKeyFor(cwd), !!cfg.preferences?.private_workspace), limit:
|
|
48
|
+
const runs = await client.listRuns({ project: scopeProjectKey(projectKeyFor(cwd), !!cfg.preferences?.private_workspace), limit: CONTEXT_FETCH, timeoutMs: CONTEXT_TIMEOUT_MS }).catch(() => []);
|
|
46
49
|
const current = sessionId ? sessionRunId(harness, sessionId) : undefined;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const others = runs.filter((r) => r.run_id !== current);
|
|
51
|
+
// The newest session of every other harness first, so a burst of short runs in one harness cannot hide the rest.
|
|
52
|
+
const firsts = others.filter((r, i) => r.agent_platform !== harness && others.findIndex((o) => o.agent_platform === r.agent_platform) === i);
|
|
53
|
+
const picked = [...new Set([...firsts, ...others])].slice(0, MAX_CONTEXT_SESSIONS);
|
|
54
|
+
const lines = others
|
|
55
|
+
.filter((r) => picked.includes(r))
|
|
50
56
|
.map((r) => {
|
|
51
57
|
const title = truncate(asStr(r.metadata?.title) || r.task, 120);
|
|
52
58
|
return `- ${r.agent_platform} · ${age(now - sessionActivityAt(r))} · "${title}" (id ${r.run_id})`;
|
|
@@ -140,13 +146,14 @@ function readStdin(timeoutMs) {
|
|
|
140
146
|
});
|
|
141
147
|
}
|
|
142
148
|
export async function cmdHook(harness) {
|
|
149
|
+
let out = '';
|
|
143
150
|
try {
|
|
144
|
-
|
|
145
|
-
if (out)
|
|
146
|
-
process.stdout.write(`${out}\n`);
|
|
151
|
+
out = await runHook(harness, await readStdin(STDIN_TIMEOUT_MS));
|
|
147
152
|
}
|
|
148
153
|
catch {
|
|
149
|
-
// runHook already swallows; this guards stdin
|
|
154
|
+
// runHook already swallows; this guards stdin edge cases.
|
|
150
155
|
}
|
|
151
|
-
|
|
156
|
+
// Exit explicitly: an aborted context fetch keeps its socket connecting for
|
|
157
|
+
// ~10s, and harnesses wait for the hook process to exit.
|
|
158
|
+
process.stdout.write(out ? `${out}\n` : '', () => process.exit(0));
|
|
152
159
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ensureCaptureConsent } from '../lib/consent.js';
|
|
|
9
9
|
import { pollAuth } from '../lib/poll-auth.js';
|
|
10
10
|
import { detectInstalledAgents, wireAgent } from '../lib/detect-agents.js';
|
|
11
11
|
import { writeAllInstructions } from '../lib/auto-context.js';
|
|
12
|
-
import { CLAUDE_MANUAL_STEPS, CODEX_TRUST_NOTE, HARNESS_LABELS, HOOK_HARNESSES, harnessInstalled, installClaudePlugin, wireHarness, } from '../lib/harness-wiring.js';
|
|
12
|
+
import { CLAUDE_MANUAL_STEPS, CODEX_TRUST_NOTE, HARNESS_LABELS, HOOK_HARNESSES, harnessInstalled, installClaudePlugin, removeLegacyClaudeSkill, wireHarness, } from '../lib/harness-wiring.js';
|
|
13
13
|
async function runPrivacyOptIn(existing, yes) {
|
|
14
14
|
const start = { ...DEFAULT_PREFERENCES, ...(existing ?? {}) };
|
|
15
15
|
process.stdout.write(`\n${kleur.bold('Your AI memory')}\n`);
|
|
@@ -125,7 +125,19 @@ export async function cmdInit(opts = {}) {
|
|
|
125
125
|
await writeConfig(config);
|
|
126
126
|
process.stdout.write(`\n${kleur.bold('Step 2.')} Connect agent harnesses.\n`);
|
|
127
127
|
const summary = [];
|
|
128
|
-
|
|
128
|
+
const claude = await wireClaudeCode(yes, home);
|
|
129
|
+
summary.push(['Claude Code', claude]);
|
|
130
|
+
// Until the plugin is in, the old skill is the only AgentDrive integration Claude Code has.
|
|
131
|
+
if (claude.startsWith('wired')) {
|
|
132
|
+
try {
|
|
133
|
+
const legacy = await removeLegacyClaudeSkill(home);
|
|
134
|
+
if (legacy)
|
|
135
|
+
process.stdout.write(` ${kleur.green('-')} ${legacy} (old CLI copy; the Claude Code plugin ships the skill now)\n`);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
process.stdout.write(` ${kleur.yellow('!')} could not remove the old AgentDrive skill: ${errorMessage(err)}\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
129
141
|
let codexWired = false;
|
|
130
142
|
for (const h of HOOK_HARNESSES) {
|
|
131
143
|
const label = HARNESS_LABELS[h];
|
|
@@ -143,7 +155,7 @@ export async function cmdInit(opts = {}) {
|
|
|
143
155
|
process.stdout.write(` ${kleur.green('+')} ${p}\n`);
|
|
144
156
|
for (const w of wiring.warnings)
|
|
145
157
|
process.stdout.write(` ${kleur.yellow('!')} ${label}: ${w}\n`);
|
|
146
|
-
summary.push([label, wiring.changed.length ? 'wired' : 'wired (already up to date)']);
|
|
158
|
+
summary.push([label, wiring.warnings.length ? 'manual step needed (see above)' : wiring.changed.length ? 'wired' : 'wired (already up to date)']);
|
|
147
159
|
if (h === 'codex')
|
|
148
160
|
codexWired = true;
|
|
149
161
|
}
|
package/dist/commands/resume.js
CHANGED
|
@@ -36,6 +36,9 @@ export async function cmdResume(id, opts = {}) {
|
|
|
36
36
|
throw new Error(`Session ${run.run_id} has no uploaded transcript.`);
|
|
37
37
|
}
|
|
38
38
|
const file = await client.readFileByPath(run.workspace_id, transcriptPath);
|
|
39
|
-
const
|
|
40
|
-
|
|
39
|
+
const encrypted = isEncrypted(file.content);
|
|
40
|
+
const markdown = encrypted ? await decryptContentAsync(cfg.api_key, file.content) : file.content;
|
|
41
|
+
// drive_read cannot decrypt, so a private session's uploads are not offered.
|
|
42
|
+
const uploads = encrypted ? undefined : { workspaceId: run.workspace_id, fileIds: run.artifacts_produced };
|
|
43
|
+
process.stdout.write(`${buildHandoff(markdown, opts.maxChars, uploads)}\n`);
|
|
41
44
|
}
|
package/dist/lib/auto-context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { codexHome, opencodeConfigDir } from './config.js';
|
|
4
5
|
import { readTextIfExists, writeFileIfChanged } from './write-file.js';
|
|
5
6
|
// Small instructions block written into each agent host's persistent
|
|
6
7
|
// preamble (AGENTS.md, GEMINI.md, cursor rules). Tells the agent when to
|
|
@@ -29,10 +30,11 @@ Privacy: the user controls capture and encryption with \`adrive privacy\`.
|
|
|
29
30
|
With "Private workspace" on, content is end-to-end encrypted on their
|
|
30
31
|
device; the AgentDrive server stores only ciphertext.
|
|
31
32
|
${END}`;
|
|
32
|
-
export function getInstructionsTargets(home = homedir()) {
|
|
33
|
-
const
|
|
33
|
+
export function getInstructionsTargets(home = homedir(), env = process.env) {
|
|
34
|
+
const codex = codexHome(env, home);
|
|
35
|
+
const opencode = opencodeConfigDir(env, home);
|
|
34
36
|
return [
|
|
35
|
-
{ root:
|
|
37
|
+
{ root: codex, path: join(codex, 'AGENTS.md'), format: 'markdown' },
|
|
36
38
|
{ root: opencode, path: join(opencode, 'AGENTS.md'), format: 'markdown' },
|
|
37
39
|
{ root: join(home, '.gemini'), path: join(home, '.gemini', 'GEMINI.md'), format: 'markdown' },
|
|
38
40
|
{ root: join(home, '.cursor'), path: join(home, '.cursor', 'rules', 'agentdrive.mdc'), format: 'cursor-mdc' },
|
|
@@ -67,16 +69,16 @@ export async function writeInstructions(target) {
|
|
|
67
69
|
}
|
|
68
70
|
return { path: target.path, action: await writeFileIfChanged(target.path, next, { backup: true }) };
|
|
69
71
|
}
|
|
70
|
-
export async function writeAllInstructions(home = homedir()) {
|
|
72
|
+
export async function writeAllInstructions(home = homedir(), env = process.env) {
|
|
71
73
|
const out = [];
|
|
72
|
-
for (const t of getInstructionsTargets(home)) {
|
|
74
|
+
for (const t of getInstructionsTargets(home, env)) {
|
|
73
75
|
if (await dirExists(t.root))
|
|
74
76
|
out.push(await writeInstructions(t));
|
|
75
77
|
}
|
|
76
78
|
return out;
|
|
77
79
|
}
|
|
78
|
-
export async function removeAllInstructions(home = homedir()) {
|
|
79
|
-
const targets = getInstructionsTargets(home);
|
|
80
|
+
export async function removeAllInstructions(home = homedir(), env = process.env) {
|
|
81
|
+
const targets = getInstructionsTargets(home, env);
|
|
80
82
|
for (const t of targets) {
|
|
81
83
|
try {
|
|
82
84
|
const current = await fs.readFile(t.path, 'utf8');
|
package/dist/lib/config.js
CHANGED
|
@@ -13,6 +13,15 @@ export function configDir(env = process.env, home = homedir()) {
|
|
|
13
13
|
const base = xdg && xdg.length > 0 ? xdg : join(home, '.config');
|
|
14
14
|
return join(base, 'agentdrive');
|
|
15
15
|
}
|
|
16
|
+
export function codexHome(env = process.env, home = homedir()) {
|
|
17
|
+
return env.CODEX_HOME || join(home, '.codex');
|
|
18
|
+
}
|
|
19
|
+
export function opencodeConfigDir(env = process.env, home = homedir()) {
|
|
20
|
+
return join(env.XDG_CONFIG_HOME || join(home, '.config'), 'opencode');
|
|
21
|
+
}
|
|
22
|
+
export function opencodeDataDir(env = process.env, home = homedir()) {
|
|
23
|
+
return join(env.XDG_DATA_HOME || join(home, '.local', 'share'), 'opencode');
|
|
24
|
+
}
|
|
16
25
|
export function configPath(env = process.env, home = homedir()) {
|
|
17
26
|
return join(configDir(env, home), 'config.json');
|
|
18
27
|
}
|
|
@@ -96,7 +96,10 @@ export function mergeOpencodeJson(existing) {
|
|
|
96
96
|
return base;
|
|
97
97
|
}
|
|
98
98
|
const CODEX_BLOCK = '[mcp_servers.agentdrive]\ncommand = "npx"\nargs = ["-y", "@adrive/mcp-server"]\n\n' +
|
|
99
|
-
'[mcp_servers.agentdrive.env]\nAGENTDRIVE_AGENT_PLATFORM = "codex"\n'
|
|
99
|
+
'[mcp_servers.agentdrive.env]\nAGENTDRIVE_AGENT_PLATFORM = "codex"\n' +
|
|
100
|
+
// codex exec runs with approval policy "never", which refuses MCP calls unless the tool is pre-approved; only the read-only ones are.
|
|
101
|
+
'\n[mcp_servers.agentdrive.tools.drive_sessions]\napproval_mode = "approve"\n' +
|
|
102
|
+
'\n[mcp_servers.agentdrive.tools.drive_resume]\napproval_mode = "approve"\n';
|
|
100
103
|
// Line-based so every table we don't own keeps its exact text. Drops our
|
|
101
104
|
// [mcp_servers.agentdrive*] tables plus the [[mcp_servers]] name="agentdrive"
|
|
102
105
|
// array entry older CLIs wrote, which would make the file invalid TOML next
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { codexHome, opencodeConfigDir } from './config.js';
|
|
4
5
|
import { mergeCodexToml, mergeMcpJson, mergeOpencodeJson } from './detect-agents.js';
|
|
5
6
|
import { HARNESS_COMMANDS, commandFileName, renderCommand } from './harness-commands.js';
|
|
6
7
|
import { SKILL_MARKDOWN } from './skill.js';
|
|
@@ -45,7 +46,8 @@ export function mergeGroupedHooks(existing, groups) {
|
|
|
45
46
|
return { ...existing, hooks };
|
|
46
47
|
}
|
|
47
48
|
export function codexHookGroups() {
|
|
48
|
-
|
|
49
|
+
// Codex clamps hook timeouts above 3s and reports it as an error item on every run.
|
|
50
|
+
const group = { hooks: [{ type: 'command', command: hookCommand('codex'), timeout: 3 }] };
|
|
49
51
|
return { SessionStart: group, Stop: group, SessionEnd: group, PreCompact: group };
|
|
50
52
|
}
|
|
51
53
|
export function geminiHookGroups() {
|
|
@@ -99,12 +101,12 @@ export const AgentDrive = async ({ directory }: { directory: string }) => ({
|
|
|
99
101
|
|
|
100
102
|
export default { id: 'agentdrive', server: AgentDrive };
|
|
101
103
|
`;
|
|
102
|
-
export function harnessRoot(harness, home) {
|
|
104
|
+
export function harnessRoot(harness, home, env = process.env) {
|
|
103
105
|
switch (harness) {
|
|
104
106
|
case 'codex':
|
|
105
|
-
return
|
|
107
|
+
return codexHome(env, home);
|
|
106
108
|
case 'opencode':
|
|
107
|
-
return
|
|
109
|
+
return opencodeConfigDir(env, home);
|
|
108
110
|
case 'gemini-cli':
|
|
109
111
|
return join(home, '.gemini');
|
|
110
112
|
case 'cursor':
|
|
@@ -117,16 +119,16 @@ const COMMAND_DIRS = {
|
|
|
117
119
|
'gemini-cli': join('commands', 'agentdrive'),
|
|
118
120
|
cursor: 'commands',
|
|
119
121
|
};
|
|
120
|
-
export async function harnessInstalled(harness, home) {
|
|
122
|
+
export async function harnessInstalled(harness, home, env = process.env) {
|
|
121
123
|
try {
|
|
122
|
-
return (await fs.stat(harnessRoot(harness, home))).isDirectory();
|
|
124
|
+
return (await fs.stat(harnessRoot(harness, home, env))).isDirectory();
|
|
123
125
|
}
|
|
124
126
|
catch {
|
|
125
127
|
return false;
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
|
-
export async function wireHarness(harness, home) {
|
|
129
|
-
const root = harnessRoot(harness, home);
|
|
130
|
+
export async function wireHarness(harness, home, env = process.env) {
|
|
131
|
+
const root = harnessRoot(harness, home, env);
|
|
130
132
|
const result = { changed: [], warnings: [] };
|
|
131
133
|
const track = async (path, write) => {
|
|
132
134
|
if ((await write) !== 'unchanged')
|
|
@@ -142,8 +144,13 @@ export async function wireHarness(harness, home) {
|
|
|
142
144
|
}
|
|
143
145
|
case 'opencode': {
|
|
144
146
|
const json = join(root, 'opencode.json');
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
const jsonc = join(root, 'opencode.jsonc');
|
|
148
|
+
if ((await readTextIfExists(json)) === null && (await readTextIfExists(jsonc)) !== null) {
|
|
149
|
+
// A fresh OpenCode writes plain JSON into opencode.jsonc; only real JSONC would lose comments on a merge.
|
|
150
|
+
if (await readJsonObject(jsonc).then(() => true, () => false))
|
|
151
|
+
await mergeJson(jsonc, mergeOpencodeJson);
|
|
152
|
+
else
|
|
153
|
+
result.warnings.push('opencode.jsonc has comments; add the agentdrive MCP entry to it by hand (they would be lost if merged)');
|
|
147
154
|
}
|
|
148
155
|
else {
|
|
149
156
|
await mergeJson(json, mergeOpencodeJson);
|
|
@@ -170,6 +177,23 @@ export async function wireHarness(harness, home) {
|
|
|
170
177
|
}
|
|
171
178
|
return result;
|
|
172
179
|
}
|
|
180
|
+
// CLI 1.0.0 wrote this skill; the Claude Code plugin now ships it, and OpenCode
|
|
181
|
+
// also loads ~/.claude/skills, so the copy shows up as a duplicate skill there.
|
|
182
|
+
export async function removeLegacyClaudeSkill(home) {
|
|
183
|
+
const dir = join(home, '.claude', 'skills', 'agentdrive');
|
|
184
|
+
const skill = join(dir, 'SKILL.md');
|
|
185
|
+
// A linked folder is shared with other agents or a dev checkout; deleting through it would reach outside ~/.claude.
|
|
186
|
+
if (!(await fs.lstat(dir).then((st) => st.isDirectory(), () => false)))
|
|
187
|
+
return null;
|
|
188
|
+
const text = await readTextIfExists(skill);
|
|
189
|
+
const frontmatter = text && /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1];
|
|
190
|
+
if (!frontmatter || !/^name:[ \t]*agentdrive[ \t]*\r?$/m.test(frontmatter))
|
|
191
|
+
return null;
|
|
192
|
+
await fs.rm(skill);
|
|
193
|
+
// Anything else the user put there stays.
|
|
194
|
+
await fs.rmdir(dir).catch(() => undefined);
|
|
195
|
+
return skill;
|
|
196
|
+
}
|
|
173
197
|
// This repo is private; agentdrive-mcp carries a public copy of .claude-plugin/ and plugins/.
|
|
174
198
|
export const CLAUDE_MARKETPLACE = 'Rakesh1002/agentdrive-mcp';
|
|
175
199
|
export const CLAUDE_MANUAL_STEPS = [
|
package/dist/lib/sync.js
CHANGED
|
@@ -147,7 +147,7 @@ function makeLogger(quiet, dir) {
|
|
|
147
147
|
function defaultClient(cfg, harness) {
|
|
148
148
|
return new CliApiClient({ baseUrl: cfg.api_url, apiKey: cfg.api_key, agentPlatform: harness, agentRole: cfg.agent_role });
|
|
149
149
|
}
|
|
150
|
-
// shortcut: no trailing-edge debounce;
|
|
150
|
+
// shortcut: no trailing-edge debounce; a debounced last turn with no end event after it (a killed agent) syncs on the next sweep from any harness. Add a delayed re-sync if that lag matters.
|
|
151
151
|
export async function runSync(opts = {}) {
|
|
152
152
|
const result = { synced: [], unchanged: 0, debounced: 0, failed: 0, perHarness: {} };
|
|
153
153
|
const dir = opts.configDir ?? defaultConfigDir();
|
|
@@ -200,7 +200,8 @@ export async function runSync(opts = {}) {
|
|
|
200
200
|
const entry = await locate(sources[opts.harness], opts, home, dir);
|
|
201
201
|
if (entry)
|
|
202
202
|
candidates.push({ harness: opts.harness, entry, explicit: true });
|
|
203
|
-
|
|
203
|
+
// A starting session usually has no transcript yet.
|
|
204
|
+
else if (!opts.start)
|
|
204
205
|
await log(`${opts.harness} session ${opts.session ?? opts.transcript} not found`);
|
|
205
206
|
}
|
|
206
207
|
const sweep = !named || !!opts.all || !!opts.start || !!opts.force || !manifest.last_sweep_at || now - manifest.last_sweep_at > DEBOUNCE_MS;
|
|
@@ -214,7 +215,7 @@ export async function runSync(opts = {}) {
|
|
|
214
215
|
for (const harness of opts.harness && !named ? [opts.harness] : HARNESSES) {
|
|
215
216
|
let entries;
|
|
216
217
|
try {
|
|
217
|
-
entries = await sources[harness].enumerate({ home, since, configDir: dir, cwd: opts.cwd ?? process.cwd() });
|
|
218
|
+
entries = await sources[harness].enumerate({ home, since, configDir: dir, cwd: opts.cwd ?? process.cwd(), env: opts.env });
|
|
218
219
|
}
|
|
219
220
|
catch (err) {
|
|
220
221
|
await log(`${harness} enumerate failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -229,7 +230,7 @@ export async function runSync(opts = {}) {
|
|
|
229
230
|
found.push({ harness, entry, explicit: false });
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
|
-
// Sessions
|
|
233
|
+
// Sessions whose end event never came (a killed agent) never
|
|
233
234
|
// change again, so a stale in_progress entry is revisited to finalize it.
|
|
234
235
|
for (const [runId, rec] of Object.entries(manifest.sessions)) {
|
|
235
236
|
if (rec.status !== 'in_progress' || !rec.harness || !rec.path || now - rec.synced_at < DEBOUNCE_MS)
|
|
@@ -301,11 +302,12 @@ async function locate(source, opts, home, dir) {
|
|
|
301
302
|
const sessionId = opts.session ?? basename(opts.transcript).replace(/\.jsonl?(\.zst)?$/, '');
|
|
302
303
|
return { sessionId, path: opts.transcript, mtime, cwd: opts.cwd };
|
|
303
304
|
}
|
|
304
|
-
const entries = await source.enumerate({ home, configDir: dir, cwd: opts.cwd });
|
|
305
|
+
const entries = await source.enumerate({ home, configDir: dir, cwd: opts.cwd, env: opts.env });
|
|
305
306
|
const hit = entries.find((e) => e.sessionId === opts.session);
|
|
306
307
|
return hit ? { ...hit, cwd: hit.cwd ?? opts.cwd } : null;
|
|
307
308
|
}
|
|
308
|
-
|
|
309
|
+
// A hard gate: project .adriveignore files are untrusted and could negate DEFAULT_DENY.
|
|
310
|
+
const denyRules = compileRules(DEFAULT_DENY);
|
|
309
311
|
async function syncSession(ctx, c, result) {
|
|
310
312
|
const { opts, manifest, now } = ctx;
|
|
311
313
|
if (!opts.force && now - (manifest.sessions[sessionRunId(c.harness, c.entry.sessionId)]?.synced_at ?? 0) < DEBOUNCE_MS) {
|
|
@@ -364,9 +366,13 @@ async function syncSession(ctx, c, result) {
|
|
|
364
366
|
const realCwd = cwd ? await realpathSafe(cwd) : null;
|
|
365
367
|
for (const abs of absPaths) {
|
|
366
368
|
const real = await realpathSafe(abs);
|
|
367
|
-
if (!real
|
|
369
|
+
if (!real)
|
|
368
370
|
continue;
|
|
369
371
|
const inCwd = !!realCwd && isInside(realCwd, real);
|
|
372
|
+
// Outside home only the session's own project is in scope, and a filesystem-root cwd is no project.
|
|
373
|
+
const root = isInside(ctx.realHome, real) ? ctx.realHome : inCwd && dirname(realCwd) !== realCwd ? realCwd : null;
|
|
374
|
+
if (!root || shouldIgnore(root, real, denyRules))
|
|
375
|
+
continue;
|
|
370
376
|
if (inCwd) {
|
|
371
377
|
let rules = ctx.cwdRules.get(realCwd);
|
|
372
378
|
if (!rules)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { codexHome } from '../config.js';
|
|
3
4
|
import { DocBuilder, asObj, asStr, jsonLines, mtimeOf, patchPaths, readText, toolSummary } from './util.js';
|
|
4
|
-
// Codex CLI: ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl, with
|
|
5
|
+
// Codex CLI: $CODEX_HOME (default ~/.codex)/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl, with
|
|
5
6
|
// older rollouts zstd-compressed to .jsonl.zst. Lines are
|
|
6
7
|
// `{ timestamp, type, payload }`: session_meta (id, cwd), response_item
|
|
7
8
|
// (message | function_call | custom_tool_call), and event_msg
|
|
@@ -46,9 +47,9 @@ function parseArgs(raw) {
|
|
|
46
47
|
}
|
|
47
48
|
export const codexSource = {
|
|
48
49
|
platform: 'codex',
|
|
49
|
-
async enumerate({ home, since }) {
|
|
50
|
+
async enumerate({ home, since, env }) {
|
|
50
51
|
const byId = new Map();
|
|
51
|
-
for (const path of await walkRollouts(join(
|
|
52
|
+
for (const path of await walkRollouts(join(codexHome(env, home), 'sessions'))) {
|
|
52
53
|
const mtime = await mtimeOf(path);
|
|
53
54
|
if (!mtime || (since && mtime <= since))
|
|
54
55
|
continue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { opencodeDataDir } from '../config.js';
|
|
3
|
+
import { DocBuilder, asObj, asStr, mtimeOf, patchPaths, toolSummary } from './util.js';
|
|
3
4
|
const EXEC_TIMEOUT_MS = 15_000;
|
|
4
5
|
const defaultExec = (args, cwd) => new Promise((resolve, reject) => {
|
|
5
6
|
execFile('opencode', [...args, '--pure'], { cwd, timeout: EXEC_TIMEOUT_MS, maxBuffer: 256 * 1024 * 1024, encoding: 'utf8' }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
@@ -32,7 +33,10 @@ export function createOpencodeSource(exec = defaultExec) {
|
|
|
32
33
|
// shortcut: `opencode session list` only lists the project of `cwd`, so a
|
|
33
34
|
// sweep sees the sessions of the directory the hook fired in; reading
|
|
34
35
|
// OpenCode's storage directly would cover all projects.
|
|
35
|
-
async enumerate({ since, cwd }) {
|
|
36
|
+
async enumerate({ home, since, cwd, env }) {
|
|
37
|
+
// Without a data dir OpenCode was never used, and the CLI would cost up to 15s and create its config dir.
|
|
38
|
+
if (!(await mtimeOf(opencodeDataDir(env, home))))
|
|
39
|
+
return [];
|
|
36
40
|
let list;
|
|
37
41
|
try {
|
|
38
42
|
list = parseJson(await exec(['session', 'list', '--format', 'json'], cwd));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrive/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "AgentDrive CLI — cross-agent artifact workspace with provenance and portable storage across Claude Code, Codex, Cursor, and LangGraph.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -14,13 +14,8 @@
|
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"homepage": "https://agentdrive.sh",
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/Rakesh1002/agentdrive.git",
|
|
20
|
-
"directory": "packages/cli"
|
|
21
|
-
},
|
|
22
17
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/Rakesh1002/agentdrive/issues"
|
|
18
|
+
"url": "https://github.com/Rakesh1002/agentdrive-mcp/issues"
|
|
24
19
|
},
|
|
25
20
|
"keywords": [
|
|
26
21
|
"agentdrive",
|
|
@@ -43,7 +38,7 @@
|
|
|
43
38
|
"kleur": "^4.1.5",
|
|
44
39
|
"open": "^10.1.0",
|
|
45
40
|
"prompts": "^2.4.2",
|
|
46
|
-
"@adrive/mcp-server": "^1.1.
|
|
41
|
+
"@adrive/mcp-server": "^1.1.1"
|
|
47
42
|
},
|
|
48
43
|
"devDependencies": {
|
|
49
44
|
"@types/node": "^22.0.0",
|