@adrive/cli 1.1.1 → 1.1.3

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 CHANGED
@@ -80,4 +80,4 @@ Merges preserve everything else in each file, the first change to an existing fi
80
80
 
81
81
  ## License
82
82
 
83
- Apache-2.0. See the repo's [LICENSING.md](https://github.com/Rakesh1002/agentdrive/blob/main/LICENSING.md) for the full package-by-package breakdown.
83
+ Apache-2.0.
@@ -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 gives SessionEnd 1s), so this answers fast, hands the real
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
- const FORCE_EVENTS = new Set(['SessionEnd', 'sessionEnd', 'PreCompact', 'preCompact', 'PreCompress']);
19
- const TURN_EVENTS = new Set(['Stop', 'stop', 'AfterAgent', 'session.idle']);
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: 5, timeoutMs: CONTEXT_TIMEOUT_MS }).catch(() => []);
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 lines = runs
48
- .filter((r) => r.run_id !== current)
49
- .slice(0, MAX_CONTEXT_SESSIONS)
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
- const out = await runHook(harness, await readStdin(STDIN_TIMEOUT_MS));
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/stdout edge cases.
154
+ // runHook already swallows; this guards stdin edge cases.
150
155
  }
151
- process.exitCode = 0;
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
  }
@@ -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
- summary.push(['Claude Code', await wireClaudeCode(yes, home)]);
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
  }
@@ -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 markdown = isEncrypted(file.content) ? await decryptContentAsync(cfg.api_key, file.content) : file.content;
40
- process.stdout.write(`${buildHandoff(markdown, opts.maxChars)}\n`);
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
  }
@@ -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 opencode = join(home, '.config', 'opencode');
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: join(home, '.codex'), path: join(home, '.codex', 'AGENTS.md'), format: 'markdown' },
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');
@@ -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
  }
@@ -6,11 +6,13 @@ export function getAgentTargets(opts = {}) {
6
6
  const env = opts.env ?? process.env;
7
7
  const home = opts.home ?? homedir();
8
8
  const os = opts.osPlatform ?? platform();
9
+ // Linux apps (Electron, Zed) resolve their config under XDG_CONFIG_HOME; Zed on macOS always uses ~/.config.
10
+ const xdgConfig = env.XDG_CONFIG_HOME || join(home, '.config');
9
11
  const claudeDesktop = os === 'darwin'
10
12
  ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
11
13
  : os === 'win32'
12
14
  ? join(env.APPDATA ?? join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
13
- : join(home, '.config', 'Claude', 'claude_desktop_config.json');
15
+ : join(xdgConfig, 'Claude', 'claude_desktop_config.json');
14
16
  return [
15
17
  {
16
18
  name: 'claude-desktop',
@@ -26,7 +28,7 @@ export function getAgentTargets(opts = {}) {
26
28
  },
27
29
  {
28
30
  name: 'zed',
29
- configPath: join(home, '.config', 'zed', 'settings.json'),
31
+ configPath: join(os === 'linux' ? xdgConfig : join(home, '.config'), 'zed', 'settings.json'),
30
32
  format: 'zed-json',
31
33
  platform: 'zed',
32
34
  },
@@ -34,7 +36,7 @@ export function getAgentTargets(opts = {}) {
34
36
  name: 'cline',
35
37
  configPath: os === 'darwin'
36
38
  ? join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json')
37
- : join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
39
+ : join(xdgConfig, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
38
40
  format: 'mcp-json',
39
41
  platform: 'cline',
40
42
  },
@@ -96,7 +98,11 @@ export function mergeOpencodeJson(existing) {
96
98
  return base;
97
99
  }
98
100
  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';
101
+ '[mcp_servers.agentdrive.env]\nAGENTDRIVE_AGENT_PLATFORM = "codex"\n' +
102
+ // codex exec runs with approval policy "never", which refuses MCP calls unless the tool is pre-approved; only the read-only ones are.
103
+ '\n[mcp_servers.agentdrive.tools.drive_sessions]\napproval_mode = "approve"\n' +
104
+ '\n[mcp_servers.agentdrive.tools.drive_resume]\napproval_mode = "approve"\n';
105
+ const CODEX_BLOCK_TABLES = new Set(CODEX_BLOCK.match(/^\[.+\]$/gm));
100
106
  // Line-based so every table we don't own keeps its exact text. Drops our
101
107
  // [mcp_servers.agentdrive*] tables plus the [[mcp_servers]] name="agentdrive"
102
108
  // array entry older CLIs wrote, which would make the file invalid TOML next
@@ -113,8 +119,10 @@ export function mergeCodexToml(existing) {
113
119
  const header = section.split('\n', 1)[0].trim();
114
120
  if (i === 0 && !header.startsWith('['))
115
121
  return true;
116
- if (/^\[mcp_servers\.agentdrive(\.[^\]]+)?\]$/.test(header))
117
- return false;
122
+ if (/^\[mcp_servers\.agentdrive(\.[^\]]+)?\]$/.test(header)) {
123
+ // Approvals the user added for other agentdrive tools are theirs to keep.
124
+ return header.startsWith('[mcp_servers.agentdrive.tools.') && !CODEX_BLOCK_TABLES.has(header);
125
+ }
118
126
  if (header === '[[mcp_servers]]' && /^\s*name\s*=\s*"agentdrive"\s*$/m.test(section))
119
127
  return false;
120
128
  return true;
@@ -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
- const group = { hooks: [{ type: 'command', command: hookCommand('codex'), timeout: 10 }] };
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 join(home, '.codex');
107
+ return codexHome(env, home);
106
108
  case 'opencode':
107
- return join(home, '.config', 'opencode');
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
- if ((await readTextIfExists(json)) === null && (await readTextIfExists(join(root, 'opencode.jsonc'))) !== null) {
146
- result.warnings.push('opencode.jsonc found; add the agentdrive MCP entry to it by hand (comments would be lost if merged)');
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; the last turn of a session without an end hook syncs on the next sweep from any harness.
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
- else
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 with no end event (OpenCode always, or a killed agent) never
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
- const homeRules = compileRules(DEFAULT_DENY);
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 || shouldIgnore(ctx.realHome, real, homeRules))
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(home, '.codex', 'sessions'))) {
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 { DocBuilder, asObj, asStr, patchPaths, toolSummary } from './util.js';
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.1",
3
+ "version": "1.1.3",
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.0"
41
+ "@adrive/mcp-server": "^1.1.2"
47
42
  },
48
43
  "devDependencies": {
49
44
  "@types/node": "^22.0.0",