@adrive/cli 0.3.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 +11 -12
- package/dist/commands/auth-login.js +9 -4
- package/dist/commands/context.js +12 -1
- package/dist/commands/hook.js +152 -0
- package/dist/commands/init.js +106 -74
- package/dist/commands/resume.js +41 -0
- package/dist/commands/sync.js +24 -0
- package/dist/commands/timeline.js +65 -0
- package/dist/index.js +35 -20
- package/dist/lib/api-client.js +32 -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 +58 -0
- package/dist/lib/sync.js +433 -0
- package/dist/lib/transcripts/claude.js +86 -0
- package/dist/lib/transcripts/codex.js +130 -0
- package/dist/lib/transcripts/cursor.js +73 -0
- package/dist/lib/transcripts/gemini.js +116 -0
- package/dist/lib/transcripts/opencode.js +95 -0
- package/dist/lib/transcripts/types.js +10 -0
- package/dist/lib/transcripts/util.js +102 -0
- package/dist/lib/write-file.js +55 -0
- package/package.json +12 -11
- package/dist/commands/transcripts.js +0 -210
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @adrive/cli
|
|
2
2
|
|
|
3
|
-
The `adrive` binary — a thin CLI for [AgentDrive](https://agentdrive.sh), the
|
|
3
|
+
The `adrive` binary — a thin CLI for [AgentDrive](https://agentdrive.sh), the cross-agent artifact workspace with provenance and portable storage across Claude Code, Codex, Cursor, and LangGraph.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -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
|
@@ -7,6 +7,10 @@ function truncate(s, n) {
|
|
|
7
7
|
return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
|
|
8
8
|
}
|
|
9
9
|
export async function cmdContext(task, opts = {}) {
|
|
10
|
+
if (!task) {
|
|
11
|
+
process.stderr.write('error: missing required argument <task>\n');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
10
14
|
const cfg = await requireLogin();
|
|
11
15
|
const client = new CliApiClient({
|
|
12
16
|
baseUrl: cfg.api_url,
|
|
@@ -25,8 +29,15 @@ export async function cmdContext(task, opts = {}) {
|
|
|
25
29
|
return;
|
|
26
30
|
}
|
|
27
31
|
for (const a of res.artifacts) {
|
|
28
|
-
const score = a.score.toFixed(2);
|
|
32
|
+
const score = a.score === null ? 'keyword' : a.score.toFixed(2);
|
|
29
33
|
process.stdout.write(`${kleur.cyan(a.path)} · score=${score} · by=${a.agent_id}@${a.agent_platform}\n`);
|
|
34
|
+
if (a.attribution?.reasons?.length) {
|
|
35
|
+
process.stdout.write(` ${kleur.dim('why: ' + a.attribution.reasons.join(' · '))}\n`);
|
|
36
|
+
}
|
|
37
|
+
if (a.last_run) {
|
|
38
|
+
const lr = a.last_run;
|
|
39
|
+
process.stdout.write(` ${kleur.dim('last run: ' + truncate(lr.task, 80) + ` [${lr.status}, ${lr.agent_platform}]`)}\n`);
|
|
40
|
+
}
|
|
30
41
|
if (a.snippet)
|
|
31
42
|
process.stdout.write(` ${kleur.dim(truncate(a.snippet, 500))}\n`);
|
|
32
43
|
}
|
|
@@ -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,10 +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';
|
|
12
|
+
import { CLAUDE_MANUAL_STEPS, CODEX_TRUST_NOTE, HARNESS_LABELS, HOOK_HARNESSES, harnessInstalled, installClaudePlugin, wireHarness, } from '../lib/harness-wiring.js';
|
|
8
13
|
async function runPrivacyOptIn(existing, yes) {
|
|
9
14
|
const start = { ...DEFAULT_PREFERENCES, ...(existing ?? {}) };
|
|
10
15
|
process.stdout.write(`\n${kleur.bold('Your AI memory')}\n`);
|
|
@@ -60,107 +65,134 @@ async function runPrivacyOptIn(existing, yes) {
|
|
|
60
65
|
process.stdout.write(`${kleur.dim('Change anytime with `adrive privacy`.')}\n`);
|
|
61
66
|
return { preferences };
|
|
62
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
|
+
}
|
|
63
98
|
export async function cmdInit(opts = {}) {
|
|
99
|
+
const yes = !!opts.yes;
|
|
100
|
+
const home = homedir();
|
|
64
101
|
const existing = await readConfig();
|
|
102
|
+
const preferencesSaved = await hasSavedPreferences();
|
|
65
103
|
const apiUrl = opts.apiUrl ?? existing?.api_url ?? DEFAULT_API_URL;
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
process.stdout.write(`Already logged in (api key ${apiKey.slice(0, 12)}...).\n`);
|
|
71
|
-
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) {
|
|
72
108
|
const res = await prompts({
|
|
73
109
|
type: 'confirm',
|
|
74
110
|
name: 'relogin',
|
|
75
111
|
message: 'Log in again?',
|
|
76
112
|
initial: false,
|
|
77
113
|
});
|
|
78
|
-
if (res.relogin)
|
|
79
|
-
|
|
80
|
-
}
|
|
114
|
+
if (res.relogin)
|
|
115
|
+
config.api_key = undefined;
|
|
81
116
|
}
|
|
82
117
|
}
|
|
83
|
-
if (!
|
|
118
|
+
if (!config.api_key) {
|
|
84
119
|
process.stdout.write(`${kleur.bold('Step 1.')} Log in.\n`);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
apiKey = auth.api_key;
|
|
88
|
-
agentId = auth.agent_id;
|
|
89
|
-
orgId = auth.org_id;
|
|
90
|
-
}
|
|
91
|
-
// Step 2 — wire detected MCP clients.
|
|
92
|
-
process.stdout.write(`\n${kleur.bold('Step 2.')} Detect installed agent tools.\n`);
|
|
93
|
-
const installed = await detectInstalledAgents();
|
|
94
|
-
if (installed.length === 0) {
|
|
95
|
-
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 };
|
|
96
122
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
initial: true,
|
|
110
|
-
});
|
|
111
|
-
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;
|
|
112
135
|
}
|
|
113
|
-
if (!
|
|
114
|
-
|
|
136
|
+
if (!(await confirm(yes, `Wire AgentDrive into ${label} (hooks, MCP, commands)?`))) {
|
|
137
|
+
summary.push([label, 'skipped (declined)']);
|
|
115
138
|
continue;
|
|
116
139
|
}
|
|
117
140
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
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;
|
|
120
149
|
}
|
|
121
150
|
catch (err) {
|
|
122
|
-
|
|
123
|
-
|
|
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)']);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const action = await wireAgent(t);
|
|
161
|
+
summary.push([t.name, action === 'unchanged' ? 'MCP wired (already up to date)' : 'MCP wired']);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
summary.push([t.name, `skipped (${errorMessage(err)})`]);
|
|
124
165
|
}
|
|
125
166
|
}
|
|
126
|
-
// Step 3 — privacy + memory preferences (WhatsApp-style opt-in screen).
|
|
127
167
|
process.stdout.write(`\n${kleur.bold('Step 3.')} Privacy + memory preferences.\n`);
|
|
128
|
-
const { preferences } = await runPrivacyOptIn(
|
|
129
|
-
// Step 4 — write auto-context instructions to detected agent hosts so the
|
|
130
|
-
// agent automatically reaches for AgentDrive at the right moments.
|
|
131
|
-
let instructionsWritten = [];
|
|
168
|
+
const { preferences } = await runPrivacyOptIn(config.preferences, yes);
|
|
132
169
|
if (preferences.auto_context) {
|
|
133
170
|
process.stdout.write(`\n${kleur.bold('Step 4.')} Write auto-context instructions.\n`);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
process.stdout.write(` ${kleur.green('+')} ${p}\n`);
|
|
141
|
-
}
|
|
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');
|
|
142
177
|
}
|
|
143
|
-
|
|
144
|
-
process.stdout.write('
|
|
178
|
+
catch (err) {
|
|
179
|
+
process.stdout.write(` ${kleur.yellow('!')} ${errorMessage(err)}\n`);
|
|
145
180
|
}
|
|
146
181
|
}
|
|
147
|
-
await writeConfig({
|
|
148
|
-
api_url: apiUrl,
|
|
149
|
-
api_key: apiKey,
|
|
150
|
-
agent_id: agentId,
|
|
151
|
-
org_id: orgId,
|
|
152
|
-
agent_platform: 'cli',
|
|
153
|
-
default_workspace: existing?.default_workspace,
|
|
154
|
-
preferences,
|
|
155
|
-
});
|
|
182
|
+
await writeConfig({ ...config, preferences });
|
|
156
183
|
process.stdout.write(`\n${kleur.bold('Summary')}\n`);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
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
|
+
}
|
|
162
194
|
process.stdout.write('\nNext steps:\n');
|
|
163
|
-
process.stdout.write(' -
|
|
164
|
-
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');
|
|
165
197
|
process.stdout.write(' - Change preferences anytime with `adrive privacy`.\n');
|
|
166
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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import { CliApiClient } from '../lib/api-client.js';
|
|
3
|
+
import { requireLogin } from '../lib/config.js';
|
|
4
|
+
// Platform tints inspired by D4 (variant 2A "tinted pill with letter glyph").
|
|
5
|
+
// Falls through to no-color on unknown / undefined platforms.
|
|
6
|
+
const PLATFORM_GLYPH = {
|
|
7
|
+
'claude-code': { glyph: 'CC', tint: (s) => kleur.magenta().bold(s) },
|
|
8
|
+
codex: { glyph: 'CX', tint: (s) => kleur.cyan().bold(s) },
|
|
9
|
+
cursor: { glyph: 'CR', tint: (s) => kleur.blue().bold(s) },
|
|
10
|
+
langgraph: { glyph: 'LG', tint: (s) => kleur.yellow().bold(s) },
|
|
11
|
+
crewai: { glyph: 'CA', tint: (s) => kleur.green().bold(s) },
|
|
12
|
+
autogen: { glyph: 'AG', tint: (s) => kleur.green().bold(s) },
|
|
13
|
+
hermes: { glyph: 'HM', tint: (s) => kleur.red().bold(s) },
|
|
14
|
+
openclaw: { glyph: 'OC', tint: (s) => kleur.red().bold(s) },
|
|
15
|
+
};
|
|
16
|
+
function badge(platform) {
|
|
17
|
+
const key = platform ?? 'unknown';
|
|
18
|
+
const entry = PLATFORM_GLYPH[key];
|
|
19
|
+
if (!entry)
|
|
20
|
+
return kleur.dim('— ');
|
|
21
|
+
return entry.tint(entry.glyph);
|
|
22
|
+
}
|
|
23
|
+
function actorLabel(ev) {
|
|
24
|
+
if (ev.actor_type === 'human')
|
|
25
|
+
return ev.actor_id;
|
|
26
|
+
return ev.actor_id.length > 14 ? `${ev.actor_id.slice(0, 14)}…` : ev.actor_id;
|
|
27
|
+
}
|
|
28
|
+
function formatTimestamp(iso) {
|
|
29
|
+
const d = new Date(iso);
|
|
30
|
+
const yyyy = d.getUTCFullYear();
|
|
31
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
32
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
33
|
+
const hh = String(d.getUTCHours()).padStart(2, '0');
|
|
34
|
+
const min = String(d.getUTCMinutes()).padStart(2, '0');
|
|
35
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${min}Z`;
|
|
36
|
+
}
|
|
37
|
+
function startOfTodayIso() {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())).toISOString();
|
|
40
|
+
}
|
|
41
|
+
export async function cmdTimeline(opts = {}) {
|
|
42
|
+
const cfg = await requireLogin();
|
|
43
|
+
const client = new CliApiClient({
|
|
44
|
+
baseUrl: cfg.api_url,
|
|
45
|
+
apiKey: cfg.api_key,
|
|
46
|
+
agentPlatform: cfg.agent_platform ?? 'cli',
|
|
47
|
+
agentRole: cfg.agent_role,
|
|
48
|
+
});
|
|
49
|
+
const limit = opts.limit ? Number.parseInt(opts.limit, 10) : undefined;
|
|
50
|
+
const since = opts.today ? startOfTodayIso() : opts.since;
|
|
51
|
+
const { items, truncated } = await client.timeline({ since, limit });
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
process.stdout.write('No events in window.\n');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const ev of items) {
|
|
57
|
+
const action = kleur.cyan(ev.action.padEnd(16));
|
|
58
|
+
const target = ev.target_path ?? ev.target_id;
|
|
59
|
+
process.stdout.write(`${kleur.dim(formatTimestamp(ev.created_at))} ${badge(ev.agent_platform)} ` +
|
|
60
|
+
`${action} ${kleur.dim(actorLabel(ev))} ${kleur.dim('→')} ${target}\n`);
|
|
61
|
+
}
|
|
62
|
+
if (truncated) {
|
|
63
|
+
process.stdout.write(kleur.dim(`\n${items.length} shown; capped by --limit. Re-run with a tighter --since to page back.\n`));
|
|
64
|
+
}
|
|
65
|
+
}
|