@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 +10 -11
- package/dist/commands/auth-login.js +9 -4
- package/dist/commands/context.js +0 -6
- package/dist/commands/hook.js +152 -0
- package/dist/commands/init.js +106 -82
- package/dist/commands/resume.js +41 -0
- package/dist/commands/sync.js +24 -0
- package/dist/index.js +26 -21
- package/dist/lib/api-client.js +17 -1
- package/dist/lib/auto-context.js +36 -53
- package/dist/lib/config.js +12 -0
- package/dist/lib/consent.js +60 -0
- package/dist/lib/detect-agents.js +73 -153
- package/dist/lib/harness-commands.js +63 -0
- package/dist/lib/harness-wiring.js +193 -0
- package/dist/lib/ignore.js +24 -4
- package/dist/lib/session-cwd.js +33 -0
- package/dist/lib/skill.js +41 -100
- package/dist/lib/sync.js +433 -0
- package/dist/lib/transcripts/claude.js +51 -77
- package/dist/lib/transcripts/codex.js +95 -133
- package/dist/lib/transcripts/cursor.js +68 -18
- package/dist/lib/transcripts/gemini.js +116 -0
- package/dist/lib/transcripts/opencode.js +95 -0
- package/dist/lib/transcripts/types.js +5 -5
- package/dist/lib/transcripts/util.js +102 -0
- package/dist/lib/write-file.js +55 -0
- package/package.json +11 -10
- package/dist/commands/transcripts.js +0 -56
- package/dist/lib/transcripts/ingest.js +0 -115
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`
|
|
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 |
|
|
70
|
+
| Tool | What init writes |
|
|
71
71
|
|---|---|
|
|
72
|
-
| Claude Code |
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
| Continue |
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/context.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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 {
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
81
|
-
}
|
|
114
|
+
if (res.relogin)
|
|
115
|
+
config.api_key = undefined;
|
|
82
116
|
}
|
|
83
117
|
}
|
|
84
|
-
if (!
|
|
118
|
+
if (!config.api_key) {
|
|
85
119
|
process.stdout.write(`${kleur.bold('Step 1.')} Log in.\n`);
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
120
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
process.stdout.write('
|
|
178
|
+
catch (err) {
|
|
179
|
+
process.stdout.write(` ${kleur.yellow('!')} ${errorMessage(err)}\n`);
|
|
146
180
|
}
|
|
147
181
|
}
|
|
148
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
process.stdout.write(`
|
|
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(' -
|
|
172
|
-
process.stdout.write(' -
|
|
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 {
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
.option('--
|
|
163
|
-
.option('--
|
|
164
|
-
.option('--
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
since
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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')
|