@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/dist/lib/api-client.js
CHANGED
|
@@ -47,12 +47,13 @@ export class CliApiClient {
|
|
|
47
47
|
}
|
|
48
48
|
return json.data;
|
|
49
49
|
}
|
|
50
|
-
async requestPage(method, path, body) {
|
|
50
|
+
async requestPage(method, path, body, signal) {
|
|
51
51
|
const url = `${this.baseUrl}/v1${path}`;
|
|
52
52
|
const res = await fetch(url, {
|
|
53
53
|
method,
|
|
54
54
|
headers: this.headers(true),
|
|
55
55
|
body: body ? JSON.stringify(body) : undefined,
|
|
56
|
+
signal,
|
|
56
57
|
});
|
|
57
58
|
const json = await res.json();
|
|
58
59
|
if (!res.ok || !json.success) {
|
|
@@ -94,6 +95,9 @@ export class CliApiClient {
|
|
|
94
95
|
}
|
|
95
96
|
return all;
|
|
96
97
|
}
|
|
98
|
+
async createWorkspace(name) {
|
|
99
|
+
return this.request('POST', '/workspaces', { name });
|
|
100
|
+
}
|
|
97
101
|
async writeFile(workspaceId, path, content, message) {
|
|
98
102
|
return this.request('POST', `/workspaces/${workspaceId}/files`, { path, content, message });
|
|
99
103
|
}
|
|
@@ -113,6 +117,18 @@ export class CliApiClient {
|
|
|
113
117
|
async logRun(body) {
|
|
114
118
|
return this.request('POST', '/runs', body);
|
|
115
119
|
}
|
|
120
|
+
async listRuns(query) {
|
|
121
|
+
const params = new URLSearchParams({ project: query.project });
|
|
122
|
+
if (query.platform)
|
|
123
|
+
params.set('platform', query.platform);
|
|
124
|
+
if (query.limit)
|
|
125
|
+
params.set('limit', String(query.limit));
|
|
126
|
+
const signal = query.timeoutMs ? AbortSignal.timeout(query.timeoutMs) : undefined;
|
|
127
|
+
return (await this.requestPage('GET', `/runs?${params.toString()}`, undefined, signal)).items;
|
|
128
|
+
}
|
|
129
|
+
async getRun(runId) {
|
|
130
|
+
return this.request('GET', `/runs/${encodeURIComponent(runId)}`);
|
|
131
|
+
}
|
|
116
132
|
async billingPortal() {
|
|
117
133
|
return this.request('POST', '/billing/portal', {});
|
|
118
134
|
}
|
package/dist/lib/auto-context.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import {
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readTextIfExists, writeFileIfChanged } from './write-file.js';
|
|
4
5
|
// Small instructions block written into each agent host's persistent
|
|
5
|
-
// preamble (
|
|
6
|
+
// preamble (AGENTS.md, GEMINI.md, cursor rules). Tells the agent when to
|
|
6
7
|
// reach for the AgentDrive MCP tools without the user having to ask each
|
|
7
8
|
// time. Idempotent: bracketed with markers so re-running adrive init
|
|
8
9
|
// updates the same region instead of stacking copies.
|
|
@@ -11,31 +12,30 @@ const END = '<!-- agentdrive:auto-context:end -->';
|
|
|
11
12
|
const INSTRUCTIONS = `${BEGIN}
|
|
12
13
|
## AgentDrive — your cross-agent memory
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
waiting to be
|
|
15
|
+
AgentDrive records the user's agent sessions across Claude Code, Codex,
|
|
16
|
+
OpenCode, Gemini CLI, and Cursor. Use its MCP tools without waiting to be
|
|
17
|
+
asked:
|
|
16
18
|
|
|
17
|
-
- **Session start** —
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **
|
|
24
|
-
|
|
25
|
-
\`drive_search\` (semantic search) or \`drive_recall\` (extracted
|
|
26
|
-
facts).
|
|
27
|
-
- **Run logging** — at the end of a meaningful session, call
|
|
28
|
-
\`drive_log_run\` so the next agent in the chain can see what you did.
|
|
19
|
+
- **Session start** — if the user wants to continue earlier work, call
|
|
20
|
+
\`drive_resume\` (or \`drive_sessions\` to pick a session) and check the
|
|
21
|
+
files it mentions on disk before continuing. For a new task, call
|
|
22
|
+
\`drive_context\` with a short summary of the task.
|
|
23
|
+
- **Recall** — for "what did the last session do?" or "show me what Codex
|
|
24
|
+
wrote", call \`drive_sessions\`, \`drive_search\`, or \`drive_recall\`.
|
|
25
|
+
- **Saving work** — after producing a file worth sharing with the user's
|
|
26
|
+
other agents, call \`drive_write\`.
|
|
29
27
|
|
|
30
|
-
Privacy: the user controls capture and encryption
|
|
31
|
-
"Private workspace" on,
|
|
32
|
-
|
|
28
|
+
Privacy: the user controls capture and encryption with \`adrive privacy\`.
|
|
29
|
+
With "Private workspace" on, content is end-to-end encrypted on their
|
|
30
|
+
device; the AgentDrive server stores only ciphertext.
|
|
33
31
|
${END}`;
|
|
34
32
|
export function getInstructionsTargets(home = homedir()) {
|
|
33
|
+
const opencode = join(home, '.config', 'opencode');
|
|
35
34
|
return [
|
|
36
|
-
{ path: join(home, '.
|
|
37
|
-
{ path: join(
|
|
38
|
-
{
|
|
35
|
+
{ root: join(home, '.codex'), path: join(home, '.codex', 'AGENTS.md'), format: 'markdown' },
|
|
36
|
+
{ root: opencode, path: join(opencode, 'AGENTS.md'), format: 'markdown' },
|
|
37
|
+
{ root: join(home, '.gemini'), path: join(home, '.gemini', 'GEMINI.md'), format: 'markdown' },
|
|
38
|
+
{ root: join(home, '.cursor'), path: join(home, '.cursor', 'rules', 'agentdrive.mdc'), format: 'cursor-mdc' },
|
|
39
39
|
];
|
|
40
40
|
}
|
|
41
41
|
function wrap(format, body) {
|
|
@@ -45,50 +45,33 @@ function wrap(format, body) {
|
|
|
45
45
|
}
|
|
46
46
|
return `\n${body}\n`;
|
|
47
47
|
}
|
|
48
|
-
async function
|
|
48
|
+
async function dirExists(p) {
|
|
49
49
|
try {
|
|
50
|
-
await fs.
|
|
51
|
-
return true;
|
|
50
|
+
return (await fs.stat(p)).isDirectory();
|
|
52
51
|
}
|
|
53
52
|
catch {
|
|
54
53
|
return false;
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
|
-
async function ensureDir(p) {
|
|
58
|
-
await fs.mkdir(dirname(p), { recursive: true });
|
|
59
|
-
}
|
|
60
56
|
export async function writeInstructions(target) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
const current = await readTextIfExists(target.path);
|
|
58
|
+
let next;
|
|
59
|
+
if (current === null) {
|
|
60
|
+
next = wrap(target.format, INSTRUCTIONS);
|
|
61
|
+
}
|
|
62
|
+
else if (current.includes(BEGIN) && current.includes(END)) {
|
|
63
|
+
next = current.slice(0, current.indexOf(BEGIN)) + INSTRUCTIONS + current.slice(current.indexOf(END) + END.length);
|
|
67
64
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const beginIdx = current.indexOf(BEGIN);
|
|
71
|
-
const endIdx = current.indexOf(END) + END.length;
|
|
72
|
-
const next = current.slice(0, beginIdx) + INSTRUCTIONS + current.slice(endIdx);
|
|
73
|
-
if (next === current)
|
|
74
|
-
return { path: target.path, action: 'unchanged' };
|
|
75
|
-
await fs.writeFile(target.path, next, 'utf8');
|
|
76
|
-
return { path: target.path, action: 'updated' };
|
|
65
|
+
else {
|
|
66
|
+
next = current.trimEnd() + '\n' + wrap(target.format, INSTRUCTIONS);
|
|
77
67
|
}
|
|
78
|
-
await
|
|
79
|
-
return { path: target.path, action: 'updated' };
|
|
68
|
+
return { path: target.path, action: await writeFileIfChanged(target.path, next, { backup: true }) };
|
|
80
69
|
}
|
|
81
70
|
export async function writeAllInstructions(home = homedir()) {
|
|
82
|
-
const targets = getInstructionsTargets(home);
|
|
83
71
|
const out = [];
|
|
84
|
-
for (const t of
|
|
85
|
-
|
|
72
|
+
for (const t of getInstructionsTargets(home)) {
|
|
73
|
+
if (await dirExists(t.root))
|
|
86
74
|
out.push(await writeInstructions(t));
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// Best-effort across hosts. A missing parent dir for an
|
|
90
|
-
// agent the user hasn't installed is fine to skip silently.
|
|
91
|
-
}
|
|
92
75
|
}
|
|
93
76
|
return out;
|
|
94
77
|
}
|
package/dist/lib/config.js
CHANGED
|
@@ -31,6 +31,7 @@ export async function readConfig(path = configPath()) {
|
|
|
31
31
|
agent_role: parsed.agent_role,
|
|
32
32
|
default_workspace: parsed.default_workspace,
|
|
33
33
|
preferences: { ...DEFAULT_PREFERENCES, ...(parsed.preferences ?? {}) },
|
|
34
|
+
capture_notice_at: parsed.capture_notice_at,
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
catch (err) {
|
|
@@ -40,6 +41,17 @@ export async function readConfig(path = configPath()) {
|
|
|
40
41
|
throw err;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
// readConfig fills in default preferences, so this reads the raw file to tell
|
|
45
|
+
// a fresh install apart from one where the user already chose.
|
|
46
|
+
export async function hasSavedPreferences(path = configPath()) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(await fs.readFile(path, 'utf8'));
|
|
49
|
+
return !!parsed?.preferences && typeof parsed.preferences === 'object';
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
43
55
|
export async function writeConfig(config, path = configPath()) {
|
|
44
56
|
await fs.mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
45
57
|
const payload = JSON.stringify(config, null, 2);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import { DEFAULT_PREFERENCES } from './config.js';
|
|
4
|
+
function notice(on) {
|
|
5
|
+
return `
|
|
6
|
+
${kleur.bold(`AgentDrive capture is ${on ? 'on' : 'off'}`)}
|
|
7
|
+
- ${on ? 'Captured' : 'When on, captures'}: your agent session transcripts, and files your agents write that git doesn't track.
|
|
8
|
+
- Secrets are redacted on this device before anything is uploaded.
|
|
9
|
+
- Turn on "Private workspace" to encrypt everything end-to-end on your device.
|
|
10
|
+
- ${on ? 'Change or turn it off anytime' : 'Turn it on'}: ${kleur.bold('adrive privacy')}
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
// `preferencesSaved` is whether the on-disk config already had a preferences
|
|
14
|
+
// block: only a fresh install gets capture switched on, so a choice made before
|
|
15
|
+
// the notice existed is never overridden.
|
|
16
|
+
export async function ensureCaptureConsent(config, client, opts) {
|
|
17
|
+
const next = { ...config };
|
|
18
|
+
if (!next.capture_notice_at) {
|
|
19
|
+
if (!opts.preferencesSaved) {
|
|
20
|
+
next.preferences = { ...DEFAULT_PREFERENCES, ...(next.preferences ?? {}), capture_transcripts: true, capture_artifacts: true };
|
|
21
|
+
}
|
|
22
|
+
const prefs = next.preferences;
|
|
23
|
+
process.stdout.write(notice(!!prefs?.capture_transcripts || !!prefs?.capture_artifacts));
|
|
24
|
+
next.capture_notice_at = (opts.now ?? (() => new Date()))().toISOString();
|
|
25
|
+
}
|
|
26
|
+
if (!next.default_workspace) {
|
|
27
|
+
try {
|
|
28
|
+
next.default_workspace = await pickDefaultWorkspace(client, opts.interactive);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
// Login already succeeded; a workspace can still be chosen later with --workspace.
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
process.stdout.write(`${kleur.yellow('[warn]')} could not set a default workspace: ${msg}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
async function pickDefaultWorkspace(client, interactive) {
|
|
39
|
+
const workspaces = await client.listWorkspaces();
|
|
40
|
+
if (workspaces.length === 0) {
|
|
41
|
+
const ws = await client.createWorkspace('Personal');
|
|
42
|
+
process.stdout.write(`Created workspace ${kleur.bold(ws.name)} (${ws.id}) as your default.\n`);
|
|
43
|
+
return ws.id;
|
|
44
|
+
}
|
|
45
|
+
if (workspaces.length === 1)
|
|
46
|
+
return workspaces[0].id;
|
|
47
|
+
if (interactive) {
|
|
48
|
+
const res = (await prompts({
|
|
49
|
+
type: 'select',
|
|
50
|
+
name: 'ws',
|
|
51
|
+
message: 'Default workspace for captured sessions',
|
|
52
|
+
choices: workspaces.map((w) => ({ title: `${w.name} (${w.id})`, value: w.id })),
|
|
53
|
+
}));
|
|
54
|
+
if (res.ws)
|
|
55
|
+
return res.ws;
|
|
56
|
+
}
|
|
57
|
+
const first = workspaces[0];
|
|
58
|
+
process.stdout.write(`Using ${kleur.bold(first.name)} (${first.id}) as your default workspace. Change it in ~/.config/agentdrive/config.json.\n`);
|
|
59
|
+
return first.id;
|
|
60
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { homedir, platform } from 'node:os';
|
|
3
|
-
import {
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readJsonObject, writeJsonIfChanged } from './write-file.js';
|
|
4
5
|
export function getAgentTargets(opts = {}) {
|
|
5
6
|
const env = opts.env ?? process.env;
|
|
6
7
|
const home = opts.home ?? homedir();
|
|
@@ -11,43 +12,23 @@ export function getAgentTargets(opts = {}) {
|
|
|
11
12
|
? join(env.APPDATA ?? join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
|
|
12
13
|
: join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
13
14
|
return [
|
|
14
|
-
{
|
|
15
|
-
name: 'claude-code',
|
|
16
|
-
configPath: join(home, '.claude', 'mcp-servers.json'),
|
|
17
|
-
format: 'mcp-json',
|
|
18
|
-
defaultPlatform: 'claude-code',
|
|
19
|
-
},
|
|
20
15
|
{
|
|
21
16
|
name: 'claude-desktop',
|
|
22
17
|
configPath: claudeDesktop,
|
|
23
18
|
format: 'mcp-json',
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: 'codex',
|
|
28
|
-
configPath: join(home, '.codex', 'config.toml'),
|
|
29
|
-
format: 'codex-toml',
|
|
30
|
-
defaultPlatform: 'codex',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: 'cursor',
|
|
34
|
-
configPath: join(home, '.cursor', 'mcp.json'),
|
|
35
|
-
format: 'cursor-json',
|
|
36
|
-
defaultPlatform: 'cursor',
|
|
19
|
+
platform: 'claude-code',
|
|
37
20
|
},
|
|
38
21
|
{
|
|
39
22
|
name: 'continue',
|
|
40
23
|
configPath: join(home, '.continue', 'config.json'),
|
|
41
24
|
format: 'continue-json',
|
|
42
|
-
|
|
25
|
+
platform: 'continue',
|
|
43
26
|
},
|
|
44
27
|
{
|
|
45
28
|
name: 'zed',
|
|
46
|
-
configPath:
|
|
47
|
-
? join(home, '.config', 'zed', 'settings.json')
|
|
48
|
-
: join(home, '.config', 'zed', 'settings.json'),
|
|
29
|
+
configPath: join(home, '.config', 'zed', 'settings.json'),
|
|
49
30
|
format: 'zed-json',
|
|
50
|
-
|
|
31
|
+
platform: 'zed',
|
|
51
32
|
},
|
|
52
33
|
{
|
|
53
34
|
name: 'cline',
|
|
@@ -55,7 +36,7 @@ export function getAgentTargets(opts = {}) {
|
|
|
55
36
|
? join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json')
|
|
56
37
|
: join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
|
|
57
38
|
format: 'mcp-json',
|
|
58
|
-
|
|
39
|
+
platform: 'cline',
|
|
59
40
|
},
|
|
60
41
|
];
|
|
61
42
|
}
|
|
@@ -68,156 +49,95 @@ async function fileExists(path) {
|
|
|
68
49
|
return false;
|
|
69
50
|
}
|
|
70
51
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!raw.trim())
|
|
75
|
-
return {};
|
|
76
|
-
return JSON.parse(raw);
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
const e = err;
|
|
80
|
-
if (e.code === 'ENOENT')
|
|
81
|
-
return null;
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function mcpEntry(env, platformOverride) {
|
|
86
|
-
const envBlock = {
|
|
87
|
-
AGENTDRIVE_API_KEY: env.apiKey,
|
|
88
|
-
};
|
|
89
|
-
if (env.apiUrl)
|
|
90
|
-
envBlock.AGENTDRIVE_API_URL = env.apiUrl;
|
|
91
|
-
const plat = env.agentPlatform ?? platformOverride;
|
|
92
|
-
if (plat)
|
|
93
|
-
envBlock.AGENTDRIVE_AGENT_PLATFORM = plat;
|
|
52
|
+
// No API key here: the MCP server reads it from ~/.config/agentdrive/config.json,
|
|
53
|
+
// so harness configs never hold a secret.
|
|
54
|
+
export function mcpEntry(agentPlatform) {
|
|
94
55
|
return {
|
|
95
56
|
command: 'npx',
|
|
96
57
|
args: ['-y', '@adrive/mcp-server'],
|
|
97
|
-
env:
|
|
58
|
+
env: { AGENTDRIVE_AGENT_PLATFORM: agentPlatform },
|
|
98
59
|
};
|
|
99
60
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
servers.agentdrive = mcpEntry(
|
|
61
|
+
function asObject(v) {
|
|
62
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? { ...v } : {};
|
|
63
|
+
}
|
|
64
|
+
export function mergeMcpJson(existing, agentPlatform) {
|
|
65
|
+
const base = asObject(existing);
|
|
66
|
+
const servers = asObject(base.mcpServers);
|
|
67
|
+
servers.agentdrive = mcpEntry(agentPlatform);
|
|
107
68
|
base.mcpServers = servers;
|
|
108
69
|
return base;
|
|
109
70
|
}
|
|
110
|
-
export function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const rawList = base.mcpServers;
|
|
116
|
-
const list = Array.isArray(rawList) ? [...rawList] : [];
|
|
117
|
-
const filtered = list.filter((s) => {
|
|
118
|
-
if (!s || typeof s !== 'object')
|
|
119
|
-
return true;
|
|
120
|
-
return s.name !== 'agentdrive';
|
|
121
|
-
});
|
|
122
|
-
filtered.push({
|
|
123
|
-
name: 'agentdrive',
|
|
124
|
-
...mcpEntry(env, 'continue'),
|
|
125
|
-
});
|
|
71
|
+
export function mergeContinueJson(existing) {
|
|
72
|
+
const base = asObject(existing);
|
|
73
|
+
const list = Array.isArray(base.mcpServers) ? base.mcpServers : [];
|
|
74
|
+
const filtered = list.filter((s) => asObject(s).name !== 'agentdrive');
|
|
75
|
+
filtered.push({ name: 'agentdrive', ...mcpEntry('continue') });
|
|
126
76
|
base.mcpServers = filtered;
|
|
127
77
|
return base;
|
|
128
78
|
}
|
|
129
|
-
export function mergeZedJson(existing
|
|
130
|
-
const base = existing
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
? { ...ctxRaw }
|
|
134
|
-
: {};
|
|
135
|
-
ctx.agentdrive = mcpEntry(env, 'zed');
|
|
79
|
+
export function mergeZedJson(existing) {
|
|
80
|
+
const base = asObject(existing);
|
|
81
|
+
const ctx = asObject(base.context_servers);
|
|
82
|
+
ctx.agentdrive = mcpEntry('zed');
|
|
136
83
|
base.context_servers = ctx;
|
|
137
84
|
return base;
|
|
138
85
|
}
|
|
139
|
-
export function
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
86
|
+
export function mergeOpencodeJson(existing) {
|
|
87
|
+
const base = asObject(existing);
|
|
88
|
+
const mcp = asObject(base.mcp);
|
|
89
|
+
mcp.agentdrive = {
|
|
90
|
+
type: 'local',
|
|
91
|
+
command: ['npx', '-y', '@adrive/mcp-server'],
|
|
92
|
+
environment: { AGENTDRIVE_AGENT_PLATFORM: 'opencode' },
|
|
93
|
+
enabled: true,
|
|
94
|
+
};
|
|
95
|
+
base.mcp = mcp;
|
|
96
|
+
return base;
|
|
97
|
+
}
|
|
98
|
+
const CODEX_BLOCK = '[mcp_servers.agentdrive]\ncommand = "npx"\nargs = ["-y", "@adrive/mcp-server"]\n\n' +
|
|
99
|
+
'[mcp_servers.agentdrive.env]\nAGENTDRIVE_AGENT_PLATFORM = "codex"\n';
|
|
100
|
+
// Line-based so every table we don't own keeps its exact text. Drops our
|
|
101
|
+
// [mcp_servers.agentdrive*] tables plus the [[mcp_servers]] name="agentdrive"
|
|
102
|
+
// array entry older CLIs wrote, which would make the file invalid TOML next
|
|
103
|
+
// to a [mcp_servers.*] table.
|
|
104
|
+
export function mergeCodexToml(existing) {
|
|
105
|
+
const sections = [];
|
|
106
|
+
for (const line of existing.split(/(?<=\n)/)) {
|
|
107
|
+
if (sections.length === 0 || /^\s*\[/.test(line))
|
|
108
|
+
sections.push(line);
|
|
109
|
+
else
|
|
110
|
+
sections[sections.length - 1] += line;
|
|
151
111
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
112
|
+
const kept = sections.filter((section, i) => {
|
|
113
|
+
const header = section.split('\n', 1)[0].trim();
|
|
114
|
+
if (i === 0 && !header.startsWith('['))
|
|
115
|
+
return true;
|
|
116
|
+
if (/^\[mcp_servers\.agentdrive(\.[^\]]+)?\]$/.test(header))
|
|
117
|
+
return false;
|
|
118
|
+
if (header === '[[mcp_servers]]' && /^\s*name\s*=\s*"agentdrive"\s*$/m.test(section))
|
|
119
|
+
return false;
|
|
120
|
+
return true;
|
|
158
121
|
});
|
|
159
|
-
|
|
122
|
+
const rest = kept.join('').trimEnd();
|
|
123
|
+
return rest ? `${rest}\n\n${CODEX_BLOCK}` : CODEX_BLOCK;
|
|
160
124
|
}
|
|
161
|
-
export async function wireAgent(target
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
125
|
+
export async function wireAgent(target) {
|
|
126
|
+
const existing = await readJsonObject(target.configPath);
|
|
127
|
+
if (existing === null)
|
|
164
128
|
throw new Error(`Config not found: ${target.configPath}`);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const existing = exists ? await readJsonSafe(target.configPath) : {};
|
|
174
|
-
let merged;
|
|
175
|
-
switch (target.format) {
|
|
176
|
-
case 'mcp-json':
|
|
177
|
-
merged = mergeMcpJson(existing, env, target.defaultPlatform);
|
|
178
|
-
break;
|
|
179
|
-
case 'cursor-json':
|
|
180
|
-
merged = mergeCursorJson(existing, env);
|
|
181
|
-
break;
|
|
182
|
-
case 'continue-json':
|
|
183
|
-
merged = mergeContinueJson(existing, env);
|
|
184
|
-
break;
|
|
185
|
-
case 'zed-json':
|
|
186
|
-
merged = mergeZedJson(existing, env);
|
|
187
|
-
break;
|
|
188
|
-
default:
|
|
189
|
-
throw new Error(`Unknown format: ${String(target.format)}`);
|
|
190
|
-
}
|
|
191
|
-
await fs.writeFile(target.configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
|
|
129
|
+
const merged = target.format === 'continue-json'
|
|
130
|
+
? mergeContinueJson(existing)
|
|
131
|
+
: target.format === 'zed-json'
|
|
132
|
+
? mergeZedJson(existing)
|
|
133
|
+
: mergeMcpJson(existing, target.platform);
|
|
134
|
+
return writeJsonIfChanged(target.configPath, merged);
|
|
192
135
|
}
|
|
193
136
|
export async function detectInstalledAgents(opts = {}) {
|
|
194
|
-
const targets = getAgentTargets(opts);
|
|
195
137
|
const present = [];
|
|
196
|
-
for (const t of
|
|
138
|
+
for (const t of getAgentTargets(opts)) {
|
|
197
139
|
if (await fileExists(t.configPath))
|
|
198
140
|
present.push(t);
|
|
199
141
|
}
|
|
200
142
|
return present;
|
|
201
143
|
}
|
|
202
|
-
export async function wireAllInstalled(env, opts = {}) {
|
|
203
|
-
const all = getAgentTargets(opts);
|
|
204
|
-
const wired = [];
|
|
205
|
-
const skipped = [];
|
|
206
|
-
for (const t of all) {
|
|
207
|
-
const exists = await fileExists(t.configPath);
|
|
208
|
-
const forceWrite = opts.includeMissing?.includes(t.name) ?? false;
|
|
209
|
-
if (!exists && !forceWrite) {
|
|
210
|
-
skipped.push({ name: t.name, reason: 'config not found' });
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
await wireAgent(t, env, { writeIfMissing: forceWrite });
|
|
215
|
-
wired.push(t.name);
|
|
216
|
-
}
|
|
217
|
-
catch (err) {
|
|
218
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
219
|
-
skipped.push({ name: t.name, reason: msg });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return { wired, skipped };
|
|
223
|
-
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const ARGS = '{{ARGS}}';
|
|
2
|
+
const TEMPLATES = {
|
|
3
|
+
resume: {
|
|
4
|
+
description: 'Resume an AgentDrive session from any agent harness',
|
|
5
|
+
argumentHint: '[session-id]',
|
|
6
|
+
body: `Resume earlier agent work from AgentDrive.
|
|
7
|
+
|
|
8
|
+
1. Call the \`drive_resume\` MCP tool. If a session id was given (${ARGS}), pass it as \`session_id\`; otherwise call it with no arguments to get the most recent session for this project.
|
|
9
|
+
2. Summarize the handoff for the user in 3-5 bullets: the goal, what is done, what is left, and any open questions.
|
|
10
|
+
3. Before relying on the handoff, check the current state on disk of the files it mentions; the work may have changed since that session ended.
|
|
11
|
+
4. Continue the work from where it stopped.
|
|
12
|
+
|
|
13
|
+
If the \`drive_resume\` tool is not available, run \`adrive resume\` in the shell (add the session id if one was given) and use its output as the handoff.
|
|
14
|
+
`,
|
|
15
|
+
},
|
|
16
|
+
sessions: {
|
|
17
|
+
description: 'List recent AgentDrive sessions across agent harnesses',
|
|
18
|
+
argumentHint: '[platform or project]',
|
|
19
|
+
body: `List recent agent sessions captured by AgentDrive.
|
|
20
|
+
|
|
21
|
+
Call the \`drive_sessions\` MCP tool. If a filter was given (${ARGS}), pass a harness name (claude-code, codex, opencode, gemini-cli, cursor) as \`platform\` and anything else as \`project\`.
|
|
22
|
+
|
|
23
|
+
Show the result as a compact list: session id, harness, project, when it was last active, and a one-line summary. Then tell the user they can pick one up with the AgentDrive resume command and its session id.
|
|
24
|
+
|
|
25
|
+
If the \`drive_sessions\` tool is not available, run \`adrive resume --list\` in the shell instead.
|
|
26
|
+
`,
|
|
27
|
+
},
|
|
28
|
+
login: {
|
|
29
|
+
description: 'Log in to AgentDrive and connect your other agent harnesses',
|
|
30
|
+
body: `Log the user in to AgentDrive and connect their other agent tools.
|
|
31
|
+
|
|
32
|
+
1. Run \`npx -y @adrive/cli auth login\` in the shell. It opens a browser for sign-in and waits until it completes; tell the user to finish signing in there.
|
|
33
|
+
2. When it succeeds, run \`npx -y @adrive/cli init --yes\` to wire AgentDrive into Codex, OpenCode, Gemini CLI, and Cursor where they are installed.
|
|
34
|
+
3. Relay init's summary to the user, including any one-time step it asks for (such as approving the hooks in Codex via /hooks).
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
// Login is plugin-only: init itself is what wires the other harnesses, so it
|
|
39
|
+
// has already logged in by the time their commands exist.
|
|
40
|
+
export const HARNESS_COMMANDS = ['resume', 'sessions'];
|
|
41
|
+
// Codex prompts, OpenCode, and Cursor commands share a flat namespace with
|
|
42
|
+
// built-ins such as /resume and /sessions, so they get a prefix.
|
|
43
|
+
export function commandFileName(harness, name) {
|
|
44
|
+
if (harness === 'gemini-cli')
|
|
45
|
+
return `${name}.toml`;
|
|
46
|
+
if (harness === 'claude-code')
|
|
47
|
+
return `${name}.md`;
|
|
48
|
+
return `agentdrive-${name}.md`;
|
|
49
|
+
}
|
|
50
|
+
export function renderCommand(harness, name) {
|
|
51
|
+
const t = TEMPLATES[name];
|
|
52
|
+
if (harness === 'gemini-cli') {
|
|
53
|
+
return `description = ${JSON.stringify(t.description)}\nprompt = """\n${t.body.replace(ARGS, '{{args}}')}"""\n`;
|
|
54
|
+
}
|
|
55
|
+
if (harness === 'cursor') {
|
|
56
|
+
return t.body.replace(ARGS, 'the text after the command, if any');
|
|
57
|
+
}
|
|
58
|
+
const body = t.body.replace(ARGS, '$ARGUMENTS');
|
|
59
|
+
const front = [`description: ${t.description}`];
|
|
60
|
+
if (harness !== 'opencode' && t.argumentHint)
|
|
61
|
+
front.push(`argument-hint: "${t.argumentHint}"`);
|
|
62
|
+
return `---\n${front.join('\n')}\n---\n\n${body}`;
|
|
63
|
+
}
|