@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/dist/index.js
CHANGED
|
@@ -16,7 +16,10 @@ 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';
|
|
22
|
+
import { cmdTimeline } from './commands/timeline.js';
|
|
20
23
|
// Read from the bundled package.json so we never drift again.
|
|
21
24
|
import { readFileSync } from 'node:fs';
|
|
22
25
|
import { fileURLToPath } from 'node:url';
|
|
@@ -103,7 +106,14 @@ program
|
|
|
103
106
|
workspace: opts.workspace,
|
|
104
107
|
})));
|
|
105
108
|
program
|
|
106
|
-
.command('
|
|
109
|
+
.command('timeline')
|
|
110
|
+
.description('Cross-agent activity stream for your org')
|
|
111
|
+
.option('--today', 'Only events since midnight UTC')
|
|
112
|
+
.option('--since <iso>', 'Only events strictly after this ISO datetime')
|
|
113
|
+
.option('--limit <n>', 'Max events to show (default 200, max 1000)')
|
|
114
|
+
.action(wrap((opts) => cmdTimeline(opts)));
|
|
115
|
+
program
|
|
116
|
+
.command('context [task]')
|
|
107
117
|
.description('Auto-pull relevant artifacts for a task')
|
|
108
118
|
.option('--limit <n>', 'Max artifacts to return')
|
|
109
119
|
.option('-w, --workspace <id>', 'Restrict to one workspace')
|
|
@@ -142,24 +152,29 @@ program
|
|
|
142
152
|
noEncrypt: opts.encrypt === false,
|
|
143
153
|
debounce: opts.debounce,
|
|
144
154
|
})));
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
.
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
.
|
|
152
|
-
.option('--
|
|
153
|
-
.option('--
|
|
154
|
-
.option('--
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
since
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 })));
|
|
163
178
|
program
|
|
164
179
|
.command('privacy')
|
|
165
180
|
.description('Show or change your privacy + memory preferences')
|
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,10 +117,37 @@ 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
|
}
|
|
119
135
|
async billingPlan() {
|
|
120
136
|
return this.request('GET', '/billing/plan');
|
|
121
137
|
}
|
|
138
|
+
async timeline(opts = {}) {
|
|
139
|
+
const params = new URLSearchParams();
|
|
140
|
+
if (opts.since)
|
|
141
|
+
params.set('since', opts.since);
|
|
142
|
+
if (typeof opts.limit === 'number')
|
|
143
|
+
params.set('limit', String(opts.limit));
|
|
144
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
145
|
+
const url = `${this.baseUrl}/v1/org/timeline${query}`;
|
|
146
|
+
const res = await fetch(url, { method: 'GET', headers: this.headers(true) });
|
|
147
|
+
const json = (await res.json());
|
|
148
|
+
if (!res.ok || !json.success) {
|
|
149
|
+
throw new Error(json.error ?? `API request failed (${res.status})`);
|
|
150
|
+
}
|
|
151
|
+
return { items: (json.data ?? []), truncated: !!json.truncated };
|
|
152
|
+
}
|
|
122
153
|
}
|
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
|
-
}
|