@adrive/cli 1.0.0 → 1.1.0

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
@@ -65,19 +65,18 @@ If the browser does not auto-launch (e.g. headless server, SSH session), the URL
65
65
 
66
66
  ## Auto-detect + wire
67
67
 
68
- `adrive init` looks for these config files and merges the AgentDrive MCP server entry in (preserving any servers you already have):
68
+ `adrive init` logs you in, then wires every agent harness it finds. API keys stay in `~/.config/agentdrive/config.json`; no harness config ever holds one.
69
69
 
70
- | Tool | Config path |
70
+ | Tool | What init writes |
71
71
  |---|---|
72
- | Claude Code | `~/.claude/mcp-servers.json` |
73
- | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS), `%APPDATA%/Claude/claude_desktop_config.json` (Windows), `~/.config/Claude/claude_desktop_config.json` (Linux) |
74
- | Codex CLI | `~/.codex/config.toml` |
75
- | Cursor | `~/.cursor/mcp.json` |
76
- | Cline | `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` (or the Linux equivalent) |
77
- | Continue | `~/.continue/config.json` |
78
- | Zed | `~/.config/zed/settings.json` |
79
-
80
- Existing entries are merged, not overwritten. An existing `agentdrive` entry is replaced in place.
72
+ | Claude Code | Installs the `agentdrive` plugin (`claude plugin marketplace add Rakesh1002/agentdrive` + `claude plugin install agentdrive@agentdrive`) |
73
+ | Codex CLI | `~/.codex/hooks.json`, `[mcp_servers.agentdrive]` in `~/.codex/config.toml`, `~/.codex/prompts/agentdrive-*.md`, `~/.codex/skills/agentdrive/` |
74
+ | OpenCode | `~/.config/opencode/plugins/agentdrive.ts`, `mcp.agentdrive` in `opencode.json`, `commands/agentdrive-*.md`, `skills/agentdrive/` |
75
+ | Gemini CLI | `hooks` + `mcpServers` in `~/.gemini/settings.json`, `~/.gemini/commands/agentdrive/*.toml` |
76
+ | Cursor | `~/.cursor/hooks.json`, `~/.cursor/mcp.json`, `~/.cursor/commands/agentdrive-*.md`, `~/.cursor/skills/agentdrive/` |
77
+ | Claude Desktop, Cline, Continue, Zed | MCP server entry in their existing config |
78
+
79
+ Merges preserve everything else in each file, the first change to an existing file saves `<file>.bak`, and re-running init changes nothing. Codex runs new hooks only after you approve them in Codex via `/hooks`.
81
80
 
82
81
  ## License
83
82
 
@@ -1,23 +1,28 @@
1
1
  import kleur from 'kleur';
2
2
  import { CliApiClient } from '../lib/api-client.js';
3
- import { DEFAULT_API_URL, readConfig, writeConfig } from '../lib/config.js';
3
+ import { DEFAULT_API_URL, hasSavedPreferences, readConfig, writeConfig } from '../lib/config.js';
4
+ import { ensureCaptureConsent } from '../lib/consent.js';
4
5
  import { pollAuth } from '../lib/poll-auth.js';
5
6
  export async function cmdAuthLogin(opts = {}) {
6
7
  const existing = await readConfig();
8
+ const preferencesSaved = await hasSavedPreferences();
7
9
  const apiUrl = opts.apiUrl ?? existing?.api_url ?? DEFAULT_API_URL;
8
10
  const client = new CliApiClient({ baseUrl: apiUrl, agentPlatform: 'cli' });
9
11
  const result = await pollAuth(client);
10
- await writeConfig({
12
+ const saved = {
13
+ ...existing,
11
14
  api_url: apiUrl,
12
15
  api_key: result.api_key,
13
16
  agent_id: result.agent_id,
14
17
  org_id: result.org_id,
15
18
  agent_platform: 'cli',
16
- default_workspace: existing?.default_workspace,
17
- });
19
+ };
20
+ await writeConfig(saved);
18
21
  process.stdout.write(`${kleur.green('[ok]')} API key saved.\n`);
19
22
  if (result.agent_id)
20
23
  process.stdout.write(` agent_id: ${result.agent_id}\n`);
21
24
  if (result.org_id)
22
25
  process.stdout.write(` org_id: ${result.org_id}\n`);
26
+ const authed = new CliApiClient({ baseUrl: apiUrl, apiKey: result.api_key, agentPlatform: 'cli' });
27
+ await writeConfig(await ensureCaptureConsent(saved, authed, { interactive: !!process.stdin.isTTY, preferencesSaved }));
23
28
  }
@@ -1,18 +1,12 @@
1
1
  import kleur from 'kleur';
2
2
  import { CliApiClient } from '../lib/api-client.js';
3
3
  import { requireLogin } from '../lib/config.js';
4
- import { installClaudeSkill, describeSkillAction } from '../lib/skill.js';
5
4
  function truncate(s, n) {
6
5
  if (!s)
7
6
  return '';
8
7
  return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
9
8
  }
10
9
  export async function cmdContext(task, opts = {}) {
11
- if (opts.installSkill) {
12
- const result = await installClaudeSkill({ force: opts.force });
13
- process.stdout.write(`Claude skill ${describeSkillAction(result.action)}: ${kleur.dim(result.path)}\n`);
14
- return;
15
- }
16
10
  if (!task) {
17
11
  process.stderr.write('error: missing required argument <task>\n');
18
12
  process.exit(1);
@@ -0,0 +1,152 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { projectKeyFor, scopeProjectKey } from '@adrive/mcp-server/project';
3
+ import { sessionActivityAt, sessionRunId } from '@adrive/mcp-server/session';
4
+ import { CliApiClient } from '../lib/api-client.js';
5
+ import { configDir, readConfig } from '../lib/config.js';
6
+ import { HARNESSES } from '../lib/sync.js';
7
+ import { recordSessionCwd } from '../lib/session-cwd.js';
8
+ import { asObj, asStr, truncate } from '../lib/transcripts/util.js';
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
11
+ // work to a detached `adrive sync`, never throws, and always exits 0. stdout
12
+ // is reserved for the harness's JSON protocol.
13
+ const STDIN_TIMEOUT_MS = 1000;
14
+ const CONTEXT_TIMEOUT_MS = 1500;
15
+ const MAX_CONTEXT_SESSIONS = 3;
16
+ const NOT_CONNECTED = 'AgentDrive is installed but not connected: run `adrive auth login`.';
17
+ 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']);
20
+ export function classifyEvent(name) {
21
+ if (START_EVENTS.has(name))
22
+ return 'start';
23
+ if (FORCE_EVENTS.has(name))
24
+ return 'force';
25
+ if (TURN_EVENTS.has(name))
26
+ return 'turn';
27
+ if (name === 'afterFileEdit')
28
+ return 'file-edit';
29
+ return 'other';
30
+ }
31
+ function spawnDetachedSync(args) {
32
+ const child = spawn(process.execPath, [process.argv[1], 'sync', ...args], { detached: true, stdio: 'ignore' });
33
+ child.on('error', () => undefined);
34
+ child.unref();
35
+ }
36
+ function age(ms) {
37
+ const minutes = Math.max(1, Math.round(ms / 60_000));
38
+ if (minutes < 60)
39
+ return `${minutes}m ago`;
40
+ const hours = Math.round(minutes / 60);
41
+ return hours < 48 ? `${hours}h ago` : `${Math.round(hours / 24)}d ago`;
42
+ }
43
+ async function recentSessions(cfg, harness, cwd, sessionId, now) {
44
+ 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(() => []);
46
+ 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
+ .map((r) => {
51
+ const title = truncate(asStr(r.metadata?.title) || r.task, 120);
52
+ return `- ${r.agent_platform} · ${age(now - sessionActivityAt(r))} · "${title}" (id ${r.run_id})`;
53
+ });
54
+ if (lines.length === 0)
55
+ return undefined;
56
+ return [
57
+ 'Recent AgentDrive sessions in this project:',
58
+ ...lines,
59
+ 'To continue one, call the drive_resume tool (or /agentdrive:resume) with its id.',
60
+ ].join('\n');
61
+ }
62
+ function formatContext(harness, context) {
63
+ if (!context || harness === 'opencode')
64
+ return '';
65
+ if (harness === 'cursor')
66
+ return JSON.stringify({ additional_context: context });
67
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context } });
68
+ }
69
+ function parsePayload(raw) {
70
+ try {
71
+ return asObj(JSON.parse(raw)) ?? {};
72
+ }
73
+ catch {
74
+ return {};
75
+ }
76
+ }
77
+ export async function runHook(harness, raw, deps = {}) {
78
+ // Gemini and Cursor parse stdout as JSON on every event.
79
+ const emptyOut = harness === 'gemini-cli' || harness === 'cursor' ? '{}' : '';
80
+ let out = '';
81
+ try {
82
+ if (!HARNESSES.includes(harness))
83
+ return emptyOut;
84
+ const h = harness;
85
+ const payload = parsePayload(raw);
86
+ const event = classifyEvent(asStr(payload.hook_event_name) ?? '');
87
+ const cursorRoot = h === 'cursor' && Array.isArray(payload.workspace_roots) ? asStr(payload.workspace_roots[0]) : undefined;
88
+ const sessionId = asStr(h === 'cursor' ? payload.conversation_id ?? payload.session_id : payload.session_id);
89
+ const knownCwd = cursorRoot ?? asStr(payload.cwd);
90
+ const cwd = knownCwd ?? process.cwd();
91
+ const transcript = asStr(payload.transcript_path);
92
+ const cfg = await readConfig();
93
+ const capturing = !!cfg?.preferences?.capture_transcripts || !!cfg?.preferences?.capture_artifacts;
94
+ if (cfg?.api_key && capturing && sessionId && knownCwd) {
95
+ await recordSessionCwd(configDir(), sessionRunId(h, sessionId), knownCwd);
96
+ }
97
+ if (event === 'other' || event === 'file-edit')
98
+ return emptyOut;
99
+ if (event === 'start') {
100
+ const context = !cfg?.api_key
101
+ ? NOT_CONNECTED
102
+ : cfg.preferences?.auto_context
103
+ ? await recentSessions(cfg, h, cwd, sessionId, deps.now ?? Date.now())
104
+ : undefined;
105
+ out = formatContext(h, context);
106
+ }
107
+ if (cfg?.api_key && capturing) {
108
+ (deps.spawnSync ?? spawnDetachedSync)([
109
+ '--quiet',
110
+ '--harness',
111
+ h,
112
+ ...(sessionId ? ['--session', sessionId] : []),
113
+ ...(transcript ? ['--transcript', transcript] : []),
114
+ '--cwd',
115
+ cwd,
116
+ ...(event === 'force' ? ['--force'] : event === 'start' ? ['--start'] : []),
117
+ ]);
118
+ }
119
+ }
120
+ catch {
121
+ // A hook failure must never surface in, or block, the agent session.
122
+ }
123
+ return out || emptyOut;
124
+ }
125
+ function readStdin(timeoutMs) {
126
+ return new Promise((resolve) => {
127
+ if (process.stdin.isTTY)
128
+ return resolve('');
129
+ let data = '';
130
+ const done = () => {
131
+ clearTimeout(timer);
132
+ process.stdin.destroy();
133
+ resolve(data);
134
+ };
135
+ const timer = setTimeout(done, timeoutMs);
136
+ process.stdin.setEncoding('utf8');
137
+ process.stdin.on('data', (chunk) => (data += chunk));
138
+ process.stdin.on('end', done);
139
+ process.stdin.on('error', done);
140
+ });
141
+ }
142
+ export async function cmdHook(harness) {
143
+ try {
144
+ const out = await runHook(harness, await readStdin(STDIN_TIMEOUT_MS));
145
+ if (out)
146
+ process.stdout.write(`${out}\n`);
147
+ }
148
+ catch {
149
+ // runHook already swallows; this guards stdin/stdout edge cases.
150
+ }
151
+ process.exitCode = 0;
152
+ }
@@ -1,11 +1,15 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
1
4
  import prompts from 'prompts';
2
5
  import kleur from 'kleur';
3
6
  import { CliApiClient } from '../lib/api-client.js';
4
- import { DEFAULT_API_URL, DEFAULT_PREFERENCES, readConfig, writeConfig, } from '../lib/config.js';
7
+ import { DEFAULT_API_URL, DEFAULT_PREFERENCES, hasSavedPreferences, readConfig, writeConfig, } from '../lib/config.js';
8
+ import { ensureCaptureConsent } from '../lib/consent.js';
5
9
  import { pollAuth } from '../lib/poll-auth.js';
6
10
  import { detectInstalledAgents, wireAgent } from '../lib/detect-agents.js';
7
11
  import { writeAllInstructions } from '../lib/auto-context.js';
8
- import { installClaudeSkill } from '../lib/skill.js';
12
+ import { CLAUDE_MANUAL_STEPS, CODEX_TRUST_NOTE, HARNESS_LABELS, HOOK_HARNESSES, harnessInstalled, installClaudePlugin, wireHarness, } from '../lib/harness-wiring.js';
9
13
  async function runPrivacyOptIn(existing, yes) {
10
14
  const start = { ...DEFAULT_PREFERENCES, ...(existing ?? {}) };
11
15
  process.stdout.write(`\n${kleur.bold('Your AI memory')}\n`);
@@ -61,114 +65,134 @@ async function runPrivacyOptIn(existing, yes) {
61
65
  process.stdout.write(`${kleur.dim('Change anytime with `adrive privacy`.')}\n`);
62
66
  return { preferences };
63
67
  }
68
+ async function confirm(yes, message) {
69
+ if (yes)
70
+ return true;
71
+ const res = (await prompts({ type: 'confirm', name: 'ok', message, initial: true }));
72
+ return !!res.ok;
73
+ }
74
+ function errorMessage(err) {
75
+ return err instanceof Error ? err.message : String(err);
76
+ }
77
+ async function wireClaudeCode(yes, home) {
78
+ if (!(await dirExists(join(home, '.claude'))))
79
+ return 'skipped (not installed)';
80
+ if (!(await confirm(yes, 'Install the AgentDrive plugin into Claude Code?')))
81
+ return 'skipped (declined)';
82
+ const result = installClaudePlugin();
83
+ if (result.status === 'installed')
84
+ return 'wired (plugin agentdrive@agentdrive)';
85
+ process.stdout.write(` In Claude Code, run:\n${CLAUDE_MANUAL_STEPS.map((s) => ` ${s}`).join('\n')}\n`);
86
+ return result.status === 'no-claude'
87
+ ? 'manual step needed (`claude` not on PATH; see above)'
88
+ : `manual step needed (${result.detail}; see above)`;
89
+ }
90
+ async function dirExists(p) {
91
+ try {
92
+ return (await fs.stat(p)).isDirectory();
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
64
98
  export async function cmdInit(opts = {}) {
99
+ const yes = !!opts.yes;
100
+ const home = homedir();
65
101
  const existing = await readConfig();
102
+ const preferencesSaved = await hasSavedPreferences();
66
103
  const apiUrl = opts.apiUrl ?? existing?.api_url ?? DEFAULT_API_URL;
67
- let apiKey = existing?.api_key;
68
- let agentId = existing?.agent_id;
69
- let orgId = existing?.org_id;
70
- if (apiKey) {
71
- process.stdout.write(`Already logged in (api key ${apiKey.slice(0, 12)}...).\n`);
72
- if (!opts.yes) {
104
+ let config = { ...existing, api_url: apiUrl, agent_platform: 'cli' };
105
+ if (config.api_key) {
106
+ process.stdout.write(`Already logged in (api key ${config.api_key.slice(0, 12)}...).\n`);
107
+ if (!yes) {
73
108
  const res = await prompts({
74
109
  type: 'confirm',
75
110
  name: 'relogin',
76
111
  message: 'Log in again?',
77
112
  initial: false,
78
113
  });
79
- if (res.relogin) {
80
- apiKey = undefined;
81
- }
114
+ if (res.relogin)
115
+ config.api_key = undefined;
82
116
  }
83
117
  }
84
- if (!apiKey) {
118
+ if (!config.api_key) {
85
119
  process.stdout.write(`${kleur.bold('Step 1.')} Log in.\n`);
86
- const client = new CliApiClient({ baseUrl: apiUrl, agentPlatform: 'cli' });
87
- const auth = await pollAuth(client);
88
- apiKey = auth.api_key;
89
- agentId = auth.agent_id;
90
- orgId = auth.org_id;
91
- }
92
- // Step 2 — wire detected MCP clients.
93
- process.stdout.write(`\n${kleur.bold('Step 2.')} Detect installed agent tools.\n`);
94
- const installed = await detectInstalledAgents();
95
- if (installed.length === 0) {
96
- process.stdout.write(' No agent configs found. Skipping MCP wiring.\n');
120
+ const auth = await pollAuth(new CliApiClient({ baseUrl: apiUrl, agentPlatform: 'cli' }));
121
+ config = { ...config, api_key: auth.api_key, agent_id: auth.agent_id, org_id: auth.org_id };
97
122
  }
98
- else {
99
- process.stdout.write(` Found: ${installed.map((t) => t.name).join(', ')}\n`);
100
- }
101
- const wired = [];
102
- const skipped = [];
103
- for (const t of installed) {
104
- let go = opts.yes ?? false;
105
- if (!go) {
106
- const res = await prompts({
107
- type: 'confirm',
108
- name: 'wire',
109
- message: `Wire AgentDrive MCP into ${t.name} (${t.configPath})?`,
110
- initial: true,
111
- });
112
- go = !!res.wire;
123
+ config = await ensureCaptureConsent(config, new CliApiClient({ baseUrl: apiUrl, apiKey: config.api_key, agentPlatform: 'cli' }), { interactive: !yes, preferencesSaved });
124
+ // Saved before wiring: hooks and the MCP server read the key from this file.
125
+ await writeConfig(config);
126
+ process.stdout.write(`\n${kleur.bold('Step 2.')} Connect agent harnesses.\n`);
127
+ const summary = [];
128
+ summary.push(['Claude Code', await wireClaudeCode(yes, home)]);
129
+ let codexWired = false;
130
+ for (const h of HOOK_HARNESSES) {
131
+ const label = HARNESS_LABELS[h];
132
+ if (!(await harnessInstalled(h, home))) {
133
+ summary.push([label, 'skipped (not installed)']);
134
+ continue;
135
+ }
136
+ if (!(await confirm(yes, `Wire AgentDrive into ${label} (hooks, MCP, commands)?`))) {
137
+ summary.push([label, 'skipped (declined)']);
138
+ continue;
139
+ }
140
+ try {
141
+ const wiring = await wireHarness(h, home);
142
+ for (const p of wiring.changed)
143
+ process.stdout.write(` ${kleur.green('+')} ${p}\n`);
144
+ for (const w of wiring.warnings)
145
+ process.stdout.write(` ${kleur.yellow('!')} ${label}: ${w}\n`);
146
+ summary.push([label, wiring.changed.length ? 'wired' : 'wired (already up to date)']);
147
+ if (h === 'codex')
148
+ codexWired = true;
113
149
  }
114
- if (!go) {
115
- skipped.push(t.name);
150
+ catch (err) {
151
+ summary.push([label, `skipped (${errorMessage(err)})`]);
152
+ }
153
+ }
154
+ for (const t of await detectInstalledAgents({ home })) {
155
+ if (!(await confirm(yes, `Wire AgentDrive MCP into ${t.name} (${t.configPath})?`))) {
156
+ summary.push([t.name, 'skipped (declined)']);
116
157
  continue;
117
158
  }
118
159
  try {
119
- await wireAgent(t, { apiKey: apiKey, apiUrl, agentPlatform: t.defaultPlatform });
120
- wired.push(t.name);
160
+ const action = await wireAgent(t);
161
+ summary.push([t.name, action === 'unchanged' ? 'MCP wired (already up to date)' : 'MCP wired']);
121
162
  }
122
163
  catch (err) {
123
- const msg = err instanceof Error ? err.message : String(err);
124
- skipped.push(`${t.name} (${msg})`);
164
+ summary.push([t.name, `skipped (${errorMessage(err)})`]);
125
165
  }
126
166
  }
127
- // Step 3 — privacy + memory preferences (WhatsApp-style opt-in screen).
128
167
  process.stdout.write(`\n${kleur.bold('Step 3.')} Privacy + memory preferences.\n`);
129
- const { preferences } = await runPrivacyOptIn(existing?.preferences, !!opts.yes);
130
- // Step 4 — write auto-context instructions to detected agent hosts so the
131
- // agent automatically reaches for AgentDrive at the right moments.
132
- let instructionsWritten = [];
168
+ const { preferences } = await runPrivacyOptIn(config.preferences, yes);
133
169
  if (preferences.auto_context) {
134
170
  process.stdout.write(`\n${kleur.bold('Step 4.')} Write auto-context instructions.\n`);
135
- const results = await writeAllInstructions();
136
- instructionsWritten = results
137
- .filter((r) => r.action !== 'unchanged')
138
- .map((r) => r.path);
139
- if (instructionsWritten.length > 0) {
140
- for (const p of instructionsWritten) {
141
- process.stdout.write(` ${kleur.green('+')} ${p}\n`);
142
- }
171
+ try {
172
+ const written = (await writeAllInstructions(home)).filter((r) => r.action !== 'unchanged');
173
+ for (const r of written)
174
+ process.stdout.write(` ${kleur.green('+')} ${r.path}\n`);
175
+ if (written.length === 0)
176
+ process.stdout.write(' No new instructions to write.\n');
143
177
  }
144
- else {
145
- process.stdout.write(' No new instructions to write.\n');
178
+ catch (err) {
179
+ process.stdout.write(` ${kleur.yellow('!')} ${errorMessage(err)}\n`);
146
180
  }
147
181
  }
148
- // Step 4b — install the Claude Code skill file. Skips silently when ~/.claude
149
- // is missing OR when the auto-context preflight was just written above
150
- // (would double-fire on the same triggers).
151
- const skillResult = await installClaudeSkill();
152
- if (skillResult.action === 'created' || skillResult.action === 'updated') {
153
- process.stdout.write(` ${kleur.green('+')} ${skillResult.path}\n`);
154
- }
155
- await writeConfig({
156
- api_url: apiUrl,
157
- api_key: apiKey,
158
- agent_id: agentId,
159
- org_id: orgId,
160
- agent_platform: 'cli',
161
- default_workspace: existing?.default_workspace,
162
- preferences,
163
- });
182
+ await writeConfig({ ...config, preferences });
164
183
  process.stdout.write(`\n${kleur.bold('Summary')}\n`);
165
- process.stdout.write(` Wired: ${wired.length ? wired.join(', ') : '(none)'}\n`);
166
- process.stdout.write(` Skipped: ${skipped.length ? skipped.join(', ') : '(none)'}\n`);
167
- process.stdout.write(` Auto-context: ${preferences.auto_context ? 'on' : 'off'}\n`);
168
- process.stdout.write(` Capture: artifacts ${preferences.capture_artifacts ? 'on' : 'off'}, transcripts ${preferences.capture_transcripts ? 'on' : 'off'}\n`);
169
- process.stdout.write(` Encryption: ${preferences.private_workspace ? 'on (E2E)' : 'off'}\n`);
184
+ const width = Math.max(...summary.map(([name]) => name.length)) + 2;
185
+ for (const [name, status] of summary) {
186
+ process.stdout.write(` ${name.padEnd(width)}${status}\n`);
187
+ }
188
+ process.stdout.write(` ${'Auto-context'.padEnd(width)}${preferences.auto_context ? 'on' : 'off'}\n`);
189
+ process.stdout.write(` ${'Capture'.padEnd(width)}transcripts ${preferences.capture_transcripts ? 'on' : 'off'}, artifacts ${preferences.capture_artifacts ? 'on' : 'off'}\n`);
190
+ process.stdout.write(` ${'Encryption'.padEnd(width)}${preferences.private_workspace ? 'on (E2E)' : 'off'}\n`);
191
+ if (codexWired) {
192
+ process.stdout.write(`\n${kleur.bold('One-time step')}\n ${CODEX_TRUST_NOTE}\n`);
193
+ }
170
194
  process.stdout.write('\nNext steps:\n');
171
- process.stdout.write(' - Open Claude Code, Cursor, or Codex — AgentDrive is wired.\n');
172
- process.stdout.write(' - Try: `adrive search "what did I work on yesterday"`\n');
195
+ process.stdout.write(' - Start a session in any wired harness; it is captured in the background.\n');
196
+ process.stdout.write(' - Pick it up anywhere with the resume command (/agentdrive:resume in Claude Code) or `adrive resume`.\n');
173
197
  process.stdout.write(' - Change preferences anytime with `adrive privacy`.\n');
174
198
  }
@@ -0,0 +1,41 @@
1
+ import { projectKeyFor, scopeProjectKey } from '@adrive/mcp-server/project';
2
+ import { buildHandoff, isEncrypted, sessionActivityAt } from '@adrive/mcp-server/session';
3
+ import { CliApiClient } from '../lib/api-client.js';
4
+ import { requireLogin } from '../lib/config.js';
5
+ import { decryptContentAsync } from '../lib/crypto.js';
6
+ function title(run) {
7
+ const t = run.metadata?.title;
8
+ return typeof t === 'string' && t ? t : run.task;
9
+ }
10
+ export async function cmdResume(id, opts = {}) {
11
+ const cfg = await requireLogin();
12
+ const client = new CliApiClient({
13
+ baseUrl: cfg.api_url,
14
+ apiKey: cfg.api_key,
15
+ agentPlatform: cfg.agent_platform ?? 'cli',
16
+ agentRole: cfg.agent_role,
17
+ });
18
+ const project = scopeProjectKey(projectKeyFor(process.cwd()), !!cfg.preferences?.private_workspace);
19
+ if (opts.list) {
20
+ const runs = await client.listRuns({ project, limit: 10 });
21
+ if (runs.length === 0) {
22
+ process.stdout.write(`No sessions for project ${project}.\n`);
23
+ return;
24
+ }
25
+ for (const r of runs) {
26
+ const when = new Date(sessionActivityAt(r)).toISOString();
27
+ process.stdout.write(`${r.run_id} ${r.agent_platform} ${when} ${title(r)}\n`);
28
+ }
29
+ return;
30
+ }
31
+ const run = id ? await client.getRun(id) : (await client.listRuns({ project, limit: 1 }))[0];
32
+ if (!run)
33
+ throw new Error(`No sessions for project ${project}.`);
34
+ const transcriptPath = run.metadata?.transcript_path;
35
+ if (typeof transcriptPath !== 'string' || !run.workspace_id) {
36
+ throw new Error(`Session ${run.run_id} has no uploaded transcript.`);
37
+ }
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`);
41
+ }
@@ -0,0 +1,24 @@
1
+ import kleur from 'kleur';
2
+ import { HARNESSES, runSync } from '../lib/sync.js';
3
+ export async function cmdSync(opts = {}) {
4
+ if (opts.harness && !HARNESSES.includes(opts.harness)) {
5
+ throw new Error(`Unknown harness: ${opts.harness}. Supported: ${HARNESSES.join(', ')}.`);
6
+ }
7
+ const result = await runSync({ ...opts, harness: opts.harness });
8
+ if (opts.quiet)
9
+ return;
10
+ if (result.skipped) {
11
+ const hint = {
12
+ 'not-logged-in': 'not logged in; run `adrive auth login`',
13
+ 'capture-off': 'capture is off; enable with `adrive privacy --capture-transcripts on` or `--capture-artifacts on`',
14
+ locked: 'another sync is running',
15
+ 'no-workspace': 'several workspaces and no default_workspace in your config',
16
+ };
17
+ process.stdout.write(`${kleur.yellow('[skip]')} ${hint[result.skipped]}\n`);
18
+ return;
19
+ }
20
+ for (const [harness, s] of Object.entries(result.perHarness)) {
21
+ process.stdout.write(`${harness}: ${s.parsed} sessions, ${s.files} files${opts.dryRun ? ' (dry run)' : ''}\n`);
22
+ }
23
+ process.stdout.write(`${kleur.bold('Synced')} ${result.synced.length}, unchanged ${result.unchanged}, debounced ${result.debounced}, failed ${result.failed}.\n`);
24
+ }
package/dist/index.js CHANGED
@@ -16,7 +16,9 @@ import { cmdBilling } from './commands/billing.js';
16
16
  import { cmdPlan } from './commands/plan.js';
17
17
  import { cmdPrivacy } from './commands/privacy.js';
18
18
  import { cmdWatch } from './commands/watch.js';
19
- import { cmdTranscriptsIngest } from './commands/transcripts.js';
19
+ import { cmdHook } from './commands/hook.js';
20
+ import { cmdSync } from './commands/sync.js';
21
+ import { cmdResume } from './commands/resume.js';
20
22
  import { cmdTimeline } from './commands/timeline.js';
21
23
  // Read from the bundled package.json so we never drift again.
22
24
  import { readFileSync } from 'node:fs';
@@ -115,8 +117,6 @@ program
115
117
  .description('Auto-pull relevant artifacts for a task')
116
118
  .option('--limit <n>', 'Max artifacts to return')
117
119
  .option('-w, --workspace <id>', 'Restrict to one workspace')
118
- .option('--install-skill', 'Install the Claude Code SKILL.md (~/.claude/skills/agentdrive/)')
119
- .option('--force', 'With --install-skill, write even if the auto-context preflight is already in CLAUDE.md')
120
120
  .action(wrap((task, opts) => cmdContext(task, opts)));
121
121
  program
122
122
  .command('run-log <task>')
@@ -152,24 +152,29 @@ program
152
152
  noEncrypt: opts.encrypt === false,
153
153
  debounce: opts.debounce,
154
154
  })));
155
- const transcripts = program.command('transcripts').description('Ingest chat transcripts from local agent hosts');
156
- transcripts
157
- .command('ingest')
158
- .description('Read recent transcripts, redact secrets, upload to AgentDrive')
159
- .option('--platform <p>', 'claude-code | codex | cursor | all (default: all)', 'all')
160
- .option('-w, --workspace <id>', 'Target workspace id')
161
- .option('--since <date>', 'Ignore transcripts older than this ISO date')
162
- .option('--limit <n>', 'Cap how many to ingest', (v) => Number(v))
163
- .option('--dry-run', 'Print what would be uploaded; never write')
164
- .option('--reingest', 'Re-upload sessions already in the manifest')
165
- .action(wrap((opts) => cmdTranscriptsIngest({
166
- platform: opts.platform,
167
- workspace: opts.workspace,
168
- since: opts.since,
169
- limit: opts.limit,
170
- dryRun: opts.dryRun,
171
- reingest: opts.reingest,
172
- })));
155
+ program
156
+ .command('hook <harness>')
157
+ .description('Lifecycle hook entry for claude-code | codex | opencode | gemini-cli | cursor (reads the event JSON on stdin)')
158
+ .action((harness) => cmdHook(harness));
159
+ program
160
+ .command('sync')
161
+ .description('Upload agent sessions and the untracked files they wrote to AgentDrive')
162
+ .option('--harness <h>', 'claude-code | codex | opencode | gemini-cli | cursor')
163
+ .option('--session <id>', 'Sync this harness session (with --harness)')
164
+ .option('--transcript <path>', 'Transcript file of that session')
165
+ .option('--cwd <dir>', 'Session working directory, used when the transcript has none')
166
+ .option('--force', 'Ignore the debounce and mark the named session finished')
167
+ .option('--start', 'Session-start sync: always sweep for recent sessions')
168
+ .option('--all', 'Sweep full history instead of changes since the last sweep')
169
+ .option('--quiet', 'Log to sync.log in the config dir instead of stdout')
170
+ .option('--dry-run', 'Parse and report; never upload')
171
+ .action(wrap((opts) => cmdSync(opts)));
172
+ program
173
+ .command('resume [id]')
174
+ .description('Print a handoff for a prior session of this project (latest if no id)')
175
+ .option('--list', 'List recent sessions for this project')
176
+ .option('--max-chars <n>', 'Handoff size budget in characters', (v) => Number(v))
177
+ .action(wrap((id, opts) => cmdResume(id, { list: opts.list, maxChars: opts.maxChars })));
173
178
  program
174
179
  .command('privacy')
175
180
  .description('Show or change your privacy + memory preferences')