@adrive/cli 1.0.0 → 1.1.1
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 +194 -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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { mergeCodexToml, mergeMcpJson, mergeOpencodeJson } from './detect-agents.js';
|
|
5
|
+
import { HARNESS_COMMANDS, commandFileName, renderCommand } from './harness-commands.js';
|
|
6
|
+
import { SKILL_MARKDOWN } from './skill.js';
|
|
7
|
+
import { readJsonObject, readTextIfExists, writeFileIfChanged, writeJsonIfChanged, } from './write-file.js';
|
|
8
|
+
export const HOOK_HARNESSES = ['codex', 'opencode', 'gemini-cli', 'cursor'];
|
|
9
|
+
export const HARNESS_LABELS = {
|
|
10
|
+
codex: 'Codex',
|
|
11
|
+
opencode: 'OpenCode',
|
|
12
|
+
'gemini-cli': 'Gemini CLI',
|
|
13
|
+
cursor: 'Cursor',
|
|
14
|
+
};
|
|
15
|
+
export const CODEX_TRUST_NOTE = 'Codex: open Codex and approve the AgentDrive hooks via /hooks.';
|
|
16
|
+
// Same form as plugins/agentdrive/hooks/hooks.json: GUI-launched harnesses can
|
|
17
|
+
// miss the global npm bin on PATH, so fall back to npx rather than fail.
|
|
18
|
+
export function hookCommand(harness) {
|
|
19
|
+
return `command -v adrive >/dev/null 2>&1 && adrive hook ${harness} || npx -y @adrive/cli hook ${harness}`;
|
|
20
|
+
}
|
|
21
|
+
function asObject(v) {
|
|
22
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? { ...v } : {};
|
|
23
|
+
}
|
|
24
|
+
function isOurCommand(command) {
|
|
25
|
+
return typeof command === 'string' && command.includes('adrive hook');
|
|
26
|
+
}
|
|
27
|
+
// Replaces our entry where it already sits (dropping duplicates) or appends
|
|
28
|
+
// it, so foreign hooks keep their order and a re-run is byte-identical.
|
|
29
|
+
function upsert(list, desired, isOurs) {
|
|
30
|
+
const entries = Array.isArray(list) ? list : [];
|
|
31
|
+
const first = entries.findIndex((e) => isOurs(asObject(e)));
|
|
32
|
+
if (first === -1)
|
|
33
|
+
return [...entries, desired];
|
|
34
|
+
return entries.flatMap((e, i) => (i === first ? [desired] : isOurs(asObject(e)) ? [] : [e]));
|
|
35
|
+
}
|
|
36
|
+
function groupIsOurs(group) {
|
|
37
|
+
return Array.isArray(group.hooks) && group.hooks.some((h) => isOurCommand(asObject(h).command));
|
|
38
|
+
}
|
|
39
|
+
// Codex and Gemini CLI use Claude Code's {event: [{matcher?, hooks: [handler]}]} shape.
|
|
40
|
+
export function mergeGroupedHooks(existing, groups) {
|
|
41
|
+
const hooks = asObject(existing.hooks);
|
|
42
|
+
for (const [event, group] of Object.entries(groups)) {
|
|
43
|
+
hooks[event] = upsert(hooks[event], group, groupIsOurs);
|
|
44
|
+
}
|
|
45
|
+
return { ...existing, hooks };
|
|
46
|
+
}
|
|
47
|
+
export function codexHookGroups() {
|
|
48
|
+
const group = { hooks: [{ type: 'command', command: hookCommand('codex'), timeout: 10 }] };
|
|
49
|
+
return { SessionStart: group, Stop: group, SessionEnd: group, PreCompact: group };
|
|
50
|
+
}
|
|
51
|
+
export function geminiHookGroups() {
|
|
52
|
+
// Gemini CLI timeouts are milliseconds.
|
|
53
|
+
const group = {
|
|
54
|
+
matcher: '*',
|
|
55
|
+
hooks: [{ type: 'command', command: hookCommand('gemini-cli'), timeout: 10000, name: 'agentdrive' }],
|
|
56
|
+
};
|
|
57
|
+
return { SessionStart: group, AfterAgent: group, SessionEnd: group, PreCompress: group };
|
|
58
|
+
}
|
|
59
|
+
export function mergeCursorHooks(existing) {
|
|
60
|
+
const hooks = asObject(existing.hooks);
|
|
61
|
+
const entry = { command: hookCommand('cursor'), timeout: 10 };
|
|
62
|
+
for (const event of ['sessionStart', 'stop', 'sessionEnd', 'preCompact']) {
|
|
63
|
+
hooks[event] = upsert(hooks[event], entry, (e) => isOurCommand(e.command));
|
|
64
|
+
}
|
|
65
|
+
return { version: 1, ...existing, hooks };
|
|
66
|
+
}
|
|
67
|
+
// Loaded by OpenCode's Bun runtime from ~/.config/opencode/plugins/. It hands
|
|
68
|
+
// the session to `adrive hook opencode` and never waits on it or surfaces errors,
|
|
69
|
+
// so a missing or slow adrive cannot stall OpenCode.
|
|
70
|
+
export const OPENCODE_PLUGIN = `// Generated by \`adrive init\`. Re-run init to update; edits are overwritten.
|
|
71
|
+
import { spawn } from 'node:child_process';
|
|
72
|
+
|
|
73
|
+
function send(payload: Record<string, unknown>): void {
|
|
74
|
+
try {
|
|
75
|
+
const child = spawn('adrive', ['hook', 'opencode'], { stdio: ['pipe', 'ignore', 'ignore'], detached: true });
|
|
76
|
+
child.on('error', () => {});
|
|
77
|
+
child.stdin?.on('error', () => {});
|
|
78
|
+
child.stdin?.end(JSON.stringify(payload));
|
|
79
|
+
child.unref();
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface OpencodeEvent {
|
|
84
|
+
type: string;
|
|
85
|
+
properties?: { info?: { id?: string; parentID?: string; directory?: string }; sessionID?: string };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const AgentDrive = async ({ directory }: { directory: string }) => ({
|
|
89
|
+
event: async ({ event }: { event: OpencodeEvent }) => {
|
|
90
|
+
if (event.type === 'session.created') {
|
|
91
|
+
const info = event.properties?.info;
|
|
92
|
+
if (!info?.id || info.parentID) return;
|
|
93
|
+
send({ hook_event_name: 'session.created', session_id: info.id, cwd: info.directory ?? directory });
|
|
94
|
+
} else if (event.type === 'session.idle' && event.properties?.sessionID) {
|
|
95
|
+
send({ hook_event_name: 'session.idle', session_id: event.properties.sessionID, cwd: directory });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export default { id: 'agentdrive', server: AgentDrive };
|
|
101
|
+
`;
|
|
102
|
+
export function harnessRoot(harness, home) {
|
|
103
|
+
switch (harness) {
|
|
104
|
+
case 'codex':
|
|
105
|
+
return join(home, '.codex');
|
|
106
|
+
case 'opencode':
|
|
107
|
+
return join(home, '.config', 'opencode');
|
|
108
|
+
case 'gemini-cli':
|
|
109
|
+
return join(home, '.gemini');
|
|
110
|
+
case 'cursor':
|
|
111
|
+
return join(home, '.cursor');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const COMMAND_DIRS = {
|
|
115
|
+
codex: 'prompts',
|
|
116
|
+
opencode: 'commands',
|
|
117
|
+
'gemini-cli': join('commands', 'agentdrive'),
|
|
118
|
+
cursor: 'commands',
|
|
119
|
+
};
|
|
120
|
+
export async function harnessInstalled(harness, home) {
|
|
121
|
+
try {
|
|
122
|
+
return (await fs.stat(harnessRoot(harness, home))).isDirectory();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function wireHarness(harness, home) {
|
|
129
|
+
const root = harnessRoot(harness, home);
|
|
130
|
+
const result = { changed: [], warnings: [] };
|
|
131
|
+
const track = async (path, write) => {
|
|
132
|
+
if ((await write) !== 'unchanged')
|
|
133
|
+
result.changed.push(path);
|
|
134
|
+
};
|
|
135
|
+
const mergeJson = async (path, merge) => track(path, writeJsonIfChanged(path, merge((await readJsonObject(path)) ?? {})));
|
|
136
|
+
switch (harness) {
|
|
137
|
+
case 'codex': {
|
|
138
|
+
await mergeJson(join(root, 'hooks.json'), (e) => mergeGroupedHooks(e, codexHookGroups()));
|
|
139
|
+
const toml = join(root, 'config.toml');
|
|
140
|
+
await track(toml, writeFileIfChanged(toml, mergeCodexToml((await readTextIfExists(toml)) ?? ''), { backup: true }));
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'opencode': {
|
|
144
|
+
const json = join(root, 'opencode.json');
|
|
145
|
+
if ((await readTextIfExists(json)) === null && (await readTextIfExists(join(root, 'opencode.jsonc'))) !== null) {
|
|
146
|
+
result.warnings.push('opencode.jsonc found; add the agentdrive MCP entry to it by hand (comments would be lost if merged)');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
await mergeJson(json, mergeOpencodeJson);
|
|
150
|
+
}
|
|
151
|
+
const plugin = join(root, 'plugins', 'agentdrive.ts');
|
|
152
|
+
await track(plugin, writeFileIfChanged(plugin, OPENCODE_PLUGIN));
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'gemini-cli':
|
|
156
|
+
await mergeJson(join(root, 'settings.json'), (e) => mergeGroupedHooks(mergeMcpJson(e, 'gemini-cli'), geminiHookGroups()));
|
|
157
|
+
break;
|
|
158
|
+
case 'cursor':
|
|
159
|
+
await mergeJson(join(root, 'hooks.json'), mergeCursorHooks);
|
|
160
|
+
await mergeJson(join(root, 'mcp.json'), (e) => mergeMcpJson(e, 'cursor'));
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
for (const name of HARNESS_COMMANDS) {
|
|
164
|
+
const path = join(root, COMMAND_DIRS[harness], commandFileName(harness, name));
|
|
165
|
+
await track(path, writeFileIfChanged(path, renderCommand(harness, name)));
|
|
166
|
+
}
|
|
167
|
+
if (harness !== 'gemini-cli') {
|
|
168
|
+
const skill = join(root, 'skills', 'agentdrive', 'SKILL.md');
|
|
169
|
+
await track(skill, writeFileIfChanged(skill, SKILL_MARKDOWN));
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
// This repo is private; agentdrive-mcp carries a public copy of .claude-plugin/ and plugins/.
|
|
174
|
+
export const CLAUDE_MARKETPLACE = 'Rakesh1002/agentdrive-mcp';
|
|
175
|
+
export const CLAUDE_MANUAL_STEPS = [
|
|
176
|
+
`/plugin marketplace add ${CLAUDE_MARKETPLACE}`,
|
|
177
|
+
'/plugin install agentdrive@agentdrive',
|
|
178
|
+
];
|
|
179
|
+
export function installClaudePlugin() {
|
|
180
|
+
const steps = [
|
|
181
|
+
['plugin', 'marketplace', 'add', CLAUDE_MARKETPLACE, '--scope', 'user'],
|
|
182
|
+
['plugin', 'install', 'agentdrive@agentdrive', '-s', 'user', '-y'],
|
|
183
|
+
];
|
|
184
|
+
for (const args of steps) {
|
|
185
|
+
const run = spawnSync('claude', args, { stdio: 'inherit' });
|
|
186
|
+
if (run.error?.code === 'ENOENT')
|
|
187
|
+
return { status: 'no-claude' };
|
|
188
|
+
if (run.error)
|
|
189
|
+
return { status: 'failed', detail: run.error.message };
|
|
190
|
+
if (run.status !== 0)
|
|
191
|
+
return { status: 'failed', detail: `\`claude ${args.join(' ')}\` exited ${run.status}` };
|
|
192
|
+
}
|
|
193
|
+
return { status: 'installed' };
|
|
194
|
+
}
|
package/dist/lib/ignore.js
CHANGED
|
@@ -21,6 +21,15 @@ export const DEFAULT_DENY = [
|
|
|
21
21
|
'.DS_Store',
|
|
22
22
|
'.env',
|
|
23
23
|
'.env.*',
|
|
24
|
+
'.envrc',
|
|
25
|
+
'.dev.vars',
|
|
26
|
+
'*.tfvars',
|
|
27
|
+
'*.tfstate',
|
|
28
|
+
'*.tfstate.*',
|
|
29
|
+
'secrets/',
|
|
30
|
+
'*.keystore',
|
|
31
|
+
'credentials',
|
|
32
|
+
'service-account*.json',
|
|
24
33
|
'*.pem',
|
|
25
34
|
'*.key',
|
|
26
35
|
'*.p12',
|
|
@@ -36,6 +45,18 @@ export const DEFAULT_DENY = [
|
|
|
36
45
|
'.netrc',
|
|
37
46
|
'id_rsa',
|
|
38
47
|
'id_ed25519',
|
|
48
|
+
'id_ecdsa',
|
|
49
|
+
'id_dsa',
|
|
50
|
+
'.gnupg/',
|
|
51
|
+
'.password-store/',
|
|
52
|
+
'.git-credentials',
|
|
53
|
+
'.config/gh/',
|
|
54
|
+
'.config/agentdrive/',
|
|
55
|
+
'.claude.json',
|
|
56
|
+
'.credentials.json',
|
|
57
|
+
'auth.json',
|
|
58
|
+
'oauth_creds.json',
|
|
59
|
+
'*_history',
|
|
39
60
|
'credentials.json',
|
|
40
61
|
'serviceAccount.json',
|
|
41
62
|
'*.sqlite',
|
|
@@ -105,12 +126,11 @@ export function compileRules(lines) {
|
|
|
105
126
|
}
|
|
106
127
|
return rules;
|
|
107
128
|
}
|
|
108
|
-
export async function loadRules(root) {
|
|
109
|
-
const candidates = [join(root, '.adriveignore'), join(root, '.gitignore')];
|
|
129
|
+
export async function loadRules(root, files = ['.adriveignore', '.gitignore']) {
|
|
110
130
|
const lines = [...DEFAULT_DENY];
|
|
111
|
-
for (const
|
|
131
|
+
for (const f of files) {
|
|
112
132
|
try {
|
|
113
|
-
const raw = await fs.readFile(
|
|
133
|
+
const raw = await fs.readFile(join(root, f), 'utf8');
|
|
114
134
|
lines.push(...raw.split(/\r?\n/));
|
|
115
135
|
}
|
|
116
136
|
catch {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
// Hooks see each session's cwd, but some transcripts (Cursor, Gemini) never
|
|
4
|
+
// record it, so `adrive hook` remembers `<harness>:<session id> -> cwd` here
|
|
5
|
+
// for sync to fall back on.
|
|
6
|
+
const MAX_ENTRIES = 500;
|
|
7
|
+
export function sessionCwdPath(configDir) {
|
|
8
|
+
return join(configDir, 'session-cwd.json');
|
|
9
|
+
}
|
|
10
|
+
export async function readSessionCwds(configDir) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(await fs.readFile(sessionCwdPath(configDir), 'utf8'));
|
|
13
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function recordSessionCwd(configDir, key, cwd) {
|
|
20
|
+
const map = await readSessionCwds(configDir);
|
|
21
|
+
if (map[key] === cwd)
|
|
22
|
+
return;
|
|
23
|
+
delete map[key];
|
|
24
|
+
map[key] = cwd;
|
|
25
|
+
const keys = Object.keys(map);
|
|
26
|
+
for (const old of keys.slice(0, Math.max(0, keys.length - MAX_ENTRIES)))
|
|
27
|
+
delete map[old];
|
|
28
|
+
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
29
|
+
// Concurrent hooks may race; rename keeps the file whole even if one update is lost.
|
|
30
|
+
const tmp = `${sessionCwdPath(configDir)}.${process.pid}.tmp`;
|
|
31
|
+
await fs.writeFile(tmp, JSON.stringify(map), { encoding: 'utf8', mode: 0o600 });
|
|
32
|
+
await fs.rename(tmp, sessionCwdPath(configDir));
|
|
33
|
+
}
|
package/dist/lib/skill.js
CHANGED
|
@@ -1,117 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// Source-of-truth for the Claude Code skill file. The shell installer
|
|
5
|
-
// (apps/installer/install.sh) inlines this content too — keep them in sync
|
|
6
|
-
// when SKILL_MARKDOWN changes.
|
|
1
|
+
// Must stay byte-identical to plugins/agentdrive/skills/agentdrive/SKILL.md
|
|
2
|
+
// (the Claude Code plugin copy); test/skill.test.ts enforces it. adrive init
|
|
3
|
+
// writes this into the Codex, OpenCode, and Cursor skill directories.
|
|
7
4
|
export const SKILL_MARKDOWN = `---
|
|
8
5
|
name: agentdrive
|
|
9
|
-
description:
|
|
10
|
-
Persistent cross-platform workspace for AI agents. Use this skill when the
|
|
11
|
-
user wants to share files between agent sessions, retrieve prior context,
|
|
12
|
-
log a run, or work with the AgentDrive MCP server but those tools are not
|
|
13
|
-
available. Calls the local \`adrive\` CLI as a fallback.
|
|
6
|
+
description: Cross-agent session memory and artifact workspace. Use when the user wants to continue work another agent session started (Claude Code, Codex, OpenCode, Gemini CLI, Cursor), asks what an earlier session did, needs prior context for a task, or wants to save an artifact for their other agents.
|
|
14
7
|
---
|
|
15
8
|
|
|
16
|
-
# AgentDrive
|
|
9
|
+
# AgentDrive
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
AgentDrive captures the user's agent sessions in the background and makes any
|
|
12
|
+
of them resumable from any harness. Prefer the \`drive_*\` MCP tools; fall back
|
|
13
|
+
to the \`adrive\` CLI when they are not available.
|
|
20
14
|
|
|
21
|
-
##
|
|
15
|
+
## Sessions
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
- "Continue where I left off", "pick up the Codex session", "what was I
|
|
18
|
+
doing?": call \`drive_resume\` (optionally with \`session_id\` or
|
|
19
|
+
\`platform\`). Summarize the handoff in a few bullets, check the files it
|
|
20
|
+
mentions on disk, then continue the work.
|
|
21
|
+
- "What sessions do I have?", "what did Cursor do today?": call
|
|
22
|
+
\`drive_sessions\` (filters: \`project\`, \`platform\`, \`limit\`).
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
- "What did the last session produce?"
|
|
27
|
-
- "What did Codex / Cursor / Claude work on?"
|
|
28
|
-
- "Pull context for this task"
|
|
29
|
-
- "Save this artifact so the next agent can find it"
|
|
30
|
-
- "Log this run" / "remember what I just did"
|
|
31
|
-
- "Search my workspace for X"
|
|
32
|
-
- The user mentions a workspace, share link, or cross-agent handoff
|
|
24
|
+
Both default to the current project.
|
|
33
25
|
|
|
34
|
-
##
|
|
26
|
+
## Context and search
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
\`\`\`
|
|
28
|
+
- Starting a non-trivial task: call \`drive_context\` with a one-line summary
|
|
29
|
+
of the task to get relevant artifacts and runs from earlier work.
|
|
30
|
+
- Looking for something specific: call \`drive_search\`.
|
|
31
|
+
|
|
32
|
+
When citing a result, say why it surfaced (\`attribution.reasons\`) and mention
|
|
33
|
+
the \`last_run\` task when present.
|
|
34
|
+
|
|
35
|
+
## Saving work
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
After producing a file worth sharing with the user's other agents, call
|
|
38
|
+
\`drive_write\`. Transcripts and files git doesn't track are already captured
|
|
39
|
+
automatically when the user has capture on, so don't re-upload them.
|
|
49
40
|
|
|
50
|
-
##
|
|
41
|
+
## CLI fallback
|
|
42
|
+
|
|
43
|
+
\`\`\`
|
|
44
|
+
adrive resume [session-id] # print a session handoff
|
|
45
|
+
adrive resume --list # list recent sessions
|
|
46
|
+
adrive context "<task>" # relevant artifacts for a task
|
|
47
|
+
adrive search "<query>" # semantic search
|
|
48
|
+
adrive push <file> # save an artifact
|
|
49
|
+
\`\`\`
|
|
51
50
|
|
|
52
|
-
\`adrive
|
|
53
|
-
|
|
54
|
-
(\`reasons: ["high semantic match", "still fresh"]\`). When the artifact has a
|
|
55
|
-
\`last_run\` block, mention the prior run's task so the user sees the
|
|
56
|
-
narrative thread.
|
|
51
|
+
If \`adrive\` says the user is not logged in, ask them to run \`adrive init\`
|
|
52
|
+
(or \`/agentdrive:login\` in Claude Code). Never guess credentials.
|
|
57
53
|
|
|
58
54
|
## Privacy
|
|
59
55
|
|
|
60
|
-
Respect \`adrive privacy
|
|
61
|
-
|
|
56
|
+
Respect \`adrive privacy\`. With "Private workspace" on, content is encrypted
|
|
57
|
+
on the device and the server stores only ciphertext.
|
|
62
58
|
`;
|
|
63
|
-
// Marker that auto-context.ts writes into CLAUDE.md. If present, the user
|
|
64
|
-
// already has the MCP preflight wired and the skill would double-fire.
|
|
65
|
-
const AUTO_CONTEXT_MARKER = '<!-- agentdrive:auto-context:begin -->';
|
|
66
|
-
async function fileExists(p) {
|
|
67
|
-
try {
|
|
68
|
-
await fs.access(p);
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
export async function installClaudeSkill(opts = {}) {
|
|
76
|
-
const home = opts.home ?? homedir();
|
|
77
|
-
const claudeDir = join(home, '.claude');
|
|
78
|
-
const skillPath = join(claudeDir, 'skills', 'agentdrive', 'SKILL.md');
|
|
79
|
-
if (!(await fileExists(claudeDir))) {
|
|
80
|
-
return { path: skillPath, action: 'skipped-no-claude' };
|
|
81
|
-
}
|
|
82
|
-
if (!opts.force) {
|
|
83
|
-
const claudeMdPath = join(claudeDir, 'CLAUDE.md');
|
|
84
|
-
if (await fileExists(claudeMdPath)) {
|
|
85
|
-
const claudeMd = await fs.readFile(claudeMdPath, 'utf8');
|
|
86
|
-
if (claudeMd.includes(AUTO_CONTEXT_MARKER)) {
|
|
87
|
-
return { path: skillPath, action: 'skipped-preflight-installed' };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const existed = await fileExists(skillPath);
|
|
92
|
-
if (existed) {
|
|
93
|
-
const current = await fs.readFile(skillPath, 'utf8');
|
|
94
|
-
if (current === SKILL_MARKDOWN) {
|
|
95
|
-
return { path: skillPath, action: 'unchanged' };
|
|
96
|
-
}
|
|
97
|
-
await fs.writeFile(skillPath, SKILL_MARKDOWN, 'utf8');
|
|
98
|
-
return { path: skillPath, action: 'updated' };
|
|
99
|
-
}
|
|
100
|
-
await fs.mkdir(dirname(skillPath), { recursive: true });
|
|
101
|
-
await fs.writeFile(skillPath, SKILL_MARKDOWN, 'utf8');
|
|
102
|
-
return { path: skillPath, action: 'created' };
|
|
103
|
-
}
|
|
104
|
-
export function describeSkillAction(action) {
|
|
105
|
-
switch (action) {
|
|
106
|
-
case 'created':
|
|
107
|
-
return 'created';
|
|
108
|
-
case 'updated':
|
|
109
|
-
return 'updated';
|
|
110
|
-
case 'unchanged':
|
|
111
|
-
return 'already up to date';
|
|
112
|
-
case 'skipped-no-claude':
|
|
113
|
-
return 'skipped (no ~/.claude directory; Claude Code not installed)';
|
|
114
|
-
case 'skipped-preflight-installed':
|
|
115
|
-
return 'skipped (auto-context preflight is already in CLAUDE.md — use --force to override)';
|
|
116
|
-
}
|
|
117
|
-
}
|