@adrive/cli 0.1.0 → 0.2.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.
@@ -1,9 +1,65 @@
1
1
  import prompts from 'prompts';
2
2
  import kleur from 'kleur';
3
3
  import { CliApiClient } from '../lib/api-client.js';
4
- import { DEFAULT_API_URL, readConfig, writeConfig } from '../lib/config.js';
4
+ import { DEFAULT_API_URL, DEFAULT_PREFERENCES, readConfig, writeConfig, } from '../lib/config.js';
5
5
  import { pollAuth } from '../lib/poll-auth.js';
6
6
  import { detectInstalledAgents, wireAgent } from '../lib/detect-agents.js';
7
+ import { writeAllInstructions } from '../lib/auto-context.js';
8
+ async function runPrivacyOptIn(existing, yes) {
9
+ const start = { ...DEFAULT_PREFERENCES, ...(existing ?? {}) };
10
+ process.stdout.write(`\n${kleur.bold('Your AI memory')}\n`);
11
+ process.stdout.write(`${kleur.dim('Save what your agents do so the next session already knows. You’re in control of what gets shared.')}\n\n`);
12
+ if (yes) {
13
+ return { preferences: start };
14
+ }
15
+ const answers = (await prompts([
16
+ {
17
+ type: 'toggle',
18
+ name: 'auto_context',
19
+ message: 'Always-on context — every new session sees what you worked on before',
20
+ initial: start.auto_context,
21
+ active: 'on',
22
+ inactive: 'off',
23
+ },
24
+ {
25
+ type: 'toggle',
26
+ name: 'capture_artifacts',
27
+ message: 'Capture artifacts — files your agents write are saved automatically',
28
+ initial: start.capture_artifacts,
29
+ active: 'on',
30
+ inactive: 'off',
31
+ },
32
+ {
33
+ type: 'toggle',
34
+ name: 'capture_transcripts',
35
+ message: 'Remember conversations — chat history is searchable later',
36
+ initial: start.capture_transcripts,
37
+ active: 'on',
38
+ inactive: 'off',
39
+ },
40
+ {
41
+ type: 'toggle',
42
+ name: 'private_workspace',
43
+ message: 'Private workspace — end-to-end encrypted on your device',
44
+ initial: start.private_workspace,
45
+ active: 'on',
46
+ inactive: 'off',
47
+ },
48
+ ], {
49
+ onCancel: () => {
50
+ // Treat ctrl-c the same as leaving the screen with defaults preserved.
51
+ return false;
52
+ },
53
+ }));
54
+ const preferences = {
55
+ auto_context: answers.auto_context ?? start.auto_context,
56
+ capture_artifacts: answers.capture_artifacts ?? start.capture_artifacts,
57
+ capture_transcripts: answers.capture_transcripts ?? start.capture_transcripts,
58
+ private_workspace: answers.private_workspace ?? start.private_workspace,
59
+ };
60
+ process.stdout.write(`${kleur.dim('Change anytime with `adrive privacy`.')}\n`);
61
+ return { preferences };
62
+ }
7
63
  export async function cmdInit(opts = {}) {
8
64
  const existing = await readConfig();
9
65
  const apiUrl = opts.apiUrl ?? existing?.api_url ?? DEFAULT_API_URL;
@@ -31,15 +87,8 @@ export async function cmdInit(opts = {}) {
31
87
  apiKey = auth.api_key;
32
88
  agentId = auth.agent_id;
33
89
  orgId = auth.org_id;
34
- await writeConfig({
35
- api_url: apiUrl,
36
- api_key: apiKey,
37
- agent_id: agentId,
38
- org_id: orgId,
39
- agent_platform: 'cli',
40
- default_workspace: existing?.default_workspace,
41
- });
42
90
  }
91
+ // Step 2 — wire detected MCP clients.
43
92
  process.stdout.write(`\n${kleur.bold('Step 2.')} Detect installed agent tools.\n`);
44
93
  const installed = await detectInstalledAgents();
45
94
  if (installed.length === 0) {
@@ -74,11 +123,44 @@ export async function cmdInit(opts = {}) {
74
123
  skipped.push(`${t.name} (${msg})`);
75
124
  }
76
125
  }
126
+ // Step 3 — privacy + memory preferences (WhatsApp-style opt-in screen).
127
+ process.stdout.write(`\n${kleur.bold('Step 3.')} Privacy + memory preferences.\n`);
128
+ const { preferences } = await runPrivacyOptIn(existing?.preferences, !!opts.yes);
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 = [];
132
+ if (preferences.auto_context) {
133
+ process.stdout.write(`\n${kleur.bold('Step 4.')} Write auto-context instructions.\n`);
134
+ const results = await writeAllInstructions();
135
+ instructionsWritten = results
136
+ .filter((r) => r.action !== 'unchanged')
137
+ .map((r) => r.path);
138
+ if (instructionsWritten.length > 0) {
139
+ for (const p of instructionsWritten) {
140
+ process.stdout.write(` ${kleur.green('+')} ${p}\n`);
141
+ }
142
+ }
143
+ else {
144
+ process.stdout.write(' No new instructions to write.\n');
145
+ }
146
+ }
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
+ });
77
156
  process.stdout.write(`\n${kleur.bold('Summary')}\n`);
78
- process.stdout.write(` Wired: ${wired.length ? wired.join(', ') : '(none)'}\n`);
79
- process.stdout.write(` Skipped: ${skipped.length ? skipped.join(', ') : '(none)'}\n`);
157
+ process.stdout.write(` Wired: ${wired.length ? wired.join(', ') : '(none)'}\n`);
158
+ process.stdout.write(` Skipped: ${skipped.length ? skipped.join(', ') : '(none)'}\n`);
159
+ process.stdout.write(` Auto-context: ${preferences.auto_context ? 'on' : 'off'}\n`);
160
+ process.stdout.write(` Capture: artifacts ${preferences.capture_artifacts ? 'on' : 'off'}, transcripts ${preferences.capture_transcripts ? 'on' : 'off'}\n`);
161
+ process.stdout.write(` Encryption: ${preferences.private_workspace ? 'on (E2E)' : 'off'}\n`);
80
162
  process.stdout.write('\nNext steps:\n');
81
- process.stdout.write(' - Try: `adrive ls` to list workspaces\n');
82
- process.stdout.write(' - Try: `adrive push README.md` to share a file\n');
83
- process.stdout.write(' - Or ask Claude Code: "share this file via AgentDrive"\n');
163
+ process.stdout.write(' - Open Claude Code, Cursor, or Codex — AgentDrive is wired.\n');
164
+ process.stdout.write(' - Try: `adrive search "what did I work on yesterday"`\n');
165
+ process.stdout.write(' - Change preferences anytime with `adrive privacy`.\n');
84
166
  }
@@ -0,0 +1,95 @@
1
+ import prompts from 'prompts';
2
+ import kleur from 'kleur';
3
+ import { DEFAULT_PREFERENCES, readConfig, writeConfig, } from '../lib/config.js';
4
+ import { writeAllInstructions, removeAllInstructions } from '../lib/auto-context.js';
5
+ function display(p) {
6
+ process.stdout.write(`\n${kleur.bold('AgentDrive privacy + memory preferences')}\n\n`);
7
+ process.stdout.write(` Always-on context ${p.auto_context ? kleur.green('on') : kleur.dim('off')}\n`);
8
+ process.stdout.write(` Capture artifacts ${p.capture_artifacts ? kleur.green('on') : kleur.dim('off')}\n`);
9
+ process.stdout.write(` Remember conversations ${p.capture_transcripts ? kleur.green('on') : kleur.dim('off')}\n`);
10
+ process.stdout.write(` Private workspace (E2E) ${p.private_workspace ? kleur.green('on') : kleur.dim('off')}\n`);
11
+ process.stdout.write(`\n${kleur.dim('Change with `adrive privacy --capture-artifacts on` etc., or run it bare to step through.')}\n`);
12
+ }
13
+ export async function cmdPrivacy(opts = {}) {
14
+ const cfg = await readConfig();
15
+ if (!cfg || !cfg.api_key) {
16
+ throw new Error('Not logged in. Run `adrive init` first.');
17
+ }
18
+ const current = { ...DEFAULT_PREFERENCES, ...(cfg.preferences ?? {}) };
19
+ let next = { ...current };
20
+ const hasFlag = opts.autoContext !== undefined ||
21
+ opts.captureArtifacts !== undefined ||
22
+ opts.captureTranscripts !== undefined ||
23
+ opts.privateWorkspace !== undefined;
24
+ if (opts.show && !hasFlag) {
25
+ display(current);
26
+ return;
27
+ }
28
+ if (hasFlag) {
29
+ if (opts.autoContext !== undefined)
30
+ next.auto_context = opts.autoContext === 'on';
31
+ if (opts.captureArtifacts !== undefined)
32
+ next.capture_artifacts = opts.captureArtifacts === 'on';
33
+ if (opts.captureTranscripts !== undefined)
34
+ next.capture_transcripts = opts.captureTranscripts === 'on';
35
+ if (opts.privateWorkspace !== undefined)
36
+ next.private_workspace = opts.privateWorkspace === 'on';
37
+ }
38
+ else {
39
+ process.stdout.write(`\n${kleur.bold('Your AI memory')}\n`);
40
+ process.stdout.write(`${kleur.dim('You control what gets shared.')}\n\n`);
41
+ const answers = (await prompts([
42
+ {
43
+ type: 'toggle',
44
+ name: 'auto_context',
45
+ message: 'Always-on context',
46
+ initial: current.auto_context,
47
+ active: 'on',
48
+ inactive: 'off',
49
+ },
50
+ {
51
+ type: 'toggle',
52
+ name: 'capture_artifacts',
53
+ message: 'Capture artifacts',
54
+ initial: current.capture_artifacts,
55
+ active: 'on',
56
+ inactive: 'off',
57
+ },
58
+ {
59
+ type: 'toggle',
60
+ name: 'capture_transcripts',
61
+ message: 'Remember conversations',
62
+ initial: current.capture_transcripts,
63
+ active: 'on',
64
+ inactive: 'off',
65
+ },
66
+ {
67
+ type: 'toggle',
68
+ name: 'private_workspace',
69
+ message: 'Private workspace (E2E encrypted)',
70
+ initial: current.private_workspace,
71
+ active: 'on',
72
+ inactive: 'off',
73
+ },
74
+ ]));
75
+ next = {
76
+ auto_context: answers.auto_context ?? current.auto_context,
77
+ capture_artifacts: answers.capture_artifacts ?? current.capture_artifacts,
78
+ capture_transcripts: answers.capture_transcripts ?? current.capture_transcripts,
79
+ private_workspace: answers.private_workspace ?? current.private_workspace,
80
+ };
81
+ }
82
+ await writeConfig({ ...cfg, preferences: next });
83
+ // Reconcile auto-context instructions with the new preference.
84
+ if (next.auto_context && !current.auto_context) {
85
+ await writeAllInstructions();
86
+ }
87
+ else if (!next.auto_context && current.auto_context) {
88
+ await removeAllInstructions();
89
+ }
90
+ else if (next.auto_context) {
91
+ // Keep the instructions in sync if they've drifted.
92
+ await writeAllInstructions();
93
+ }
94
+ display(next);
95
+ }
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import kleur from 'kleur';
3
3
  import { CliApiClient } from '../lib/api-client.js';
4
4
  import { requireLogin } from '../lib/config.js';
5
+ import { decryptContent, isEncrypted } from '../lib/crypto.js';
5
6
  export async function cmdPull(spec, opts = {}) {
6
7
  const cfg = await requireLogin();
7
8
  const client = new CliApiClient({
@@ -17,13 +18,16 @@ export async function cmdPull(spec, opts = {}) {
17
18
  const workspaceId = spec.slice(0, slash);
18
19
  const remotePath = spec.slice(slash + 1);
19
20
  const file = await client.readFileByPath(workspaceId, remotePath);
21
+ const content = isEncrypted(file.content)
22
+ ? decryptContent(cfg.api_key, file.content)
23
+ : file.content;
20
24
  if (opts.out) {
21
- await fs.writeFile(opts.out, file.content, 'utf8');
25
+ await fs.writeFile(opts.out, content, 'utf8');
22
26
  process.stdout.write(`${kleur.green('[ok]')} wrote ${opts.out} (v${file.version})\n`);
23
27
  }
24
28
  else {
25
- process.stdout.write(file.content);
26
- if (!file.content.endsWith('\n'))
29
+ process.stdout.write(content);
30
+ if (!content.endsWith('\n'))
27
31
  process.stdout.write('\n');
28
32
  }
29
33
  }
@@ -3,6 +3,7 @@ import { basename } from 'node:path';
3
3
  import kleur from 'kleur';
4
4
  import { CliApiClient } from '../lib/api-client.js';
5
5
  import { requireLogin } from '../lib/config.js';
6
+ import { encryptContent } from '../lib/crypto.js';
6
7
  export async function cmdPush(localPath, opts = {}) {
7
8
  const cfg = await requireLogin();
8
9
  const client = new CliApiClient({
@@ -11,8 +12,18 @@ export async function cmdPush(localPath, opts = {}) {
11
12
  agentPlatform: cfg.agent_platform ?? 'cli',
12
13
  agentRole: cfg.agent_role,
13
14
  });
14
- const content = await fs.readFile(localPath, 'utf8');
15
+ const raw = await fs.readFile(localPath, 'utf8');
15
16
  const remotePath = opts.remotePath ?? basename(localPath);
17
+ // Encryption: explicit --encrypt always on; explicit --plaintext always off;
18
+ // otherwise honor the user's preference.
19
+ let encrypt;
20
+ if (opts.encrypt)
21
+ encrypt = true;
22
+ else if (opts.plaintext)
23
+ encrypt = false;
24
+ else
25
+ encrypt = cfg.preferences?.private_workspace ?? false;
26
+ const content = encrypt ? encryptContent(cfg.api_key, raw) : raw;
16
27
  let workspaceId = opts.workspace ?? cfg.default_workspace;
17
28
  if (!workspaceId) {
18
29
  const list = await client.listWorkspaces();
@@ -27,5 +38,6 @@ export async function cmdPush(localPath, opts = {}) {
27
38
  }
28
39
  }
29
40
  const result = await client.writeFile(workspaceId, remotePath, content, opts.message);
30
- process.stdout.write(`${kleur.green('[ok]')} pushed ${localPath} -> ${workspaceId}:${result.path} (v${result.version})\n`);
41
+ const tag = encrypt ? kleur.cyan(' (encrypted)') : '';
42
+ process.stdout.write(`${kleur.green('[ok]')} pushed ${localPath} -> ${workspaceId}:${result.path} (v${result.version})${tag}\n`);
31
43
  }
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { cmdContext } from './commands/context.js';
14
14
  import { cmdRunLog } from './commands/run-log.js';
15
15
  import { cmdBilling } from './commands/billing.js';
16
16
  import { cmdPlan } from './commands/plan.js';
17
+ import { cmdPrivacy } from './commands/privacy.js';
17
18
  const VERSION = '0.1.0';
18
19
  function fail(err) {
19
20
  const msg = err instanceof Error ? err.message : String(err);
@@ -74,6 +75,8 @@ program
74
75
  .option('-w, --workspace <id>', 'Workspace id (default: only workspace, if exactly one)')
75
76
  .option('-r, --remote-path <path>', 'Remote path (default: basename of local-path)')
76
77
  .option('-m, --message <msg>', 'Commit-style message stored on the version')
78
+ .option('--encrypt', 'Force end-to-end encryption for this upload')
79
+ .option('--plaintext', 'Force plaintext upload, even if private workspace is on')
77
80
  .action(wrap((localPath, opts) => cmdPush(localPath, opts)));
78
81
  program
79
82
  .command('pull <workspace-id-slash-path>')
@@ -118,4 +121,19 @@ program
118
121
  .command('plan')
119
122
  .description('Show current plan, usage, and quotas')
120
123
  .action(wrap(() => cmdPlan()));
124
+ program
125
+ .command('privacy')
126
+ .description('Show or change your privacy + memory preferences')
127
+ .option('--show', 'Just print current preferences')
128
+ .option('--auto-context <on|off>', 'Always-on context for new sessions')
129
+ .option('--capture-artifacts <on|off>', 'Auto-save files your agents write')
130
+ .option('--capture-transcripts <on|off>', 'Auto-save chat history')
131
+ .option('--private-workspace <on|off>', 'End-to-end encrypt your workspace')
132
+ .action(wrap((opts) => cmdPrivacy({
133
+ show: opts.show,
134
+ autoContext: opts.autoContext,
135
+ captureArtifacts: opts.captureArtifacts,
136
+ captureTranscripts: opts.captureTranscripts,
137
+ privateWorkspace: opts.privateWorkspace,
138
+ })));
121
139
  program.parseAsync(process.argv).catch(fail);
@@ -0,0 +1,116 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ // Small instructions block written into each agent host's persistent
5
+ // preamble (CLAUDE.md, AGENTS.md, cursor rules). Tells the agent when to
6
+ // reach for the AgentDrive MCP tools without the user having to ask each
7
+ // time. Idempotent: bracketed with markers so re-running adrive init
8
+ // updates the same region instead of stacking copies.
9
+ const BEGIN = '<!-- agentdrive:auto-context:begin -->';
10
+ const END = '<!-- agentdrive:auto-context:end -->';
11
+ const INSTRUCTIONS = `${BEGIN}
12
+ ## AgentDrive — your cross-agent memory
13
+
14
+ You have AgentDrive tools available via MCP. Use them like this without
15
+ waiting to be asked:
16
+
17
+ - **Session start** — call \`drive_context\` with a short summary of the
18
+ task. It returns the most relevant artifacts and run history from your
19
+ own and other agents' prior work in the same workspace.
20
+ - **Saving work** — after producing any file or artifact worth keeping,
21
+ call \`drive_write\` so the user's other agents can see it. The user
22
+ has opted into automatic capture if they want everything saved.
23
+ - **Recall** — when the user asks "what did the last session do?",
24
+ "show me what Codex wrote", or anything cross-session, call
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.
29
+
30
+ Privacy: the user controls capture and encryption. If they have
31
+ "Private workspace" on, written content is end-to-end encrypted on
32
+ their device; the AgentDrive server stores only ciphertext.
33
+ ${END}`;
34
+ export function getInstructionsTargets(home = homedir()) {
35
+ return [
36
+ { path: join(home, '.claude', 'CLAUDE.md'), format: 'markdown' },
37
+ { path: join(home, '.codex', 'AGENTS.md'), format: 'markdown' },
38
+ { path: join(home, '.cursor', 'rules', 'agentdrive.mdc'), format: 'cursor-mdc' },
39
+ ];
40
+ }
41
+ function wrap(format, body) {
42
+ if (format === 'cursor-mdc') {
43
+ // Cursor expects MDC frontmatter telling it when to apply the rule.
44
+ return `---\ndescription: AgentDrive — cross-agent memory\nalwaysApply: true\n---\n\n${body}\n`;
45
+ }
46
+ return `\n${body}\n`;
47
+ }
48
+ async function fileExists(p) {
49
+ try {
50
+ await fs.access(p);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ async function ensureDir(p) {
58
+ await fs.mkdir(dirname(p), { recursive: true });
59
+ }
60
+ export async function writeInstructions(target) {
61
+ const block = wrap(target.format, INSTRUCTIONS);
62
+ const existed = await fileExists(target.path);
63
+ if (!existed) {
64
+ await ensureDir(target.path);
65
+ await fs.writeFile(target.path, block, 'utf8');
66
+ return { path: target.path, action: 'created' };
67
+ }
68
+ const current = await fs.readFile(target.path, 'utf8');
69
+ if (current.includes(BEGIN) && current.includes(END)) {
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' };
77
+ }
78
+ await fs.writeFile(target.path, current.trimEnd() + '\n' + block, 'utf8');
79
+ return { path: target.path, action: 'updated' };
80
+ }
81
+ export async function writeAllInstructions(home = homedir()) {
82
+ const targets = getInstructionsTargets(home);
83
+ const out = [];
84
+ for (const t of targets) {
85
+ try {
86
+ 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
+ }
93
+ return out;
94
+ }
95
+ export async function removeAllInstructions(home = homedir()) {
96
+ const targets = getInstructionsTargets(home);
97
+ for (const t of targets) {
98
+ try {
99
+ const current = await fs.readFile(t.path, 'utf8');
100
+ if (!current.includes(BEGIN))
101
+ continue;
102
+ const beginIdx = current.indexOf(BEGIN);
103
+ const endIdx = current.indexOf(END) + END.length;
104
+ const next = (current.slice(0, beginIdx) + current.slice(endIdx)).trimEnd() + '\n';
105
+ if (next.trim() === '') {
106
+ await fs.unlink(t.path);
107
+ }
108
+ else {
109
+ await fs.writeFile(t.path, next, 'utf8');
110
+ }
111
+ }
112
+ catch {
113
+ // ignore
114
+ }
115
+ }
116
+ }
@@ -1,6 +1,12 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
+ export const DEFAULT_PREFERENCES = {
5
+ auto_context: true,
6
+ capture_artifacts: false,
7
+ capture_transcripts: false,
8
+ private_workspace: false,
9
+ };
4
10
  export const DEFAULT_API_URL = 'https://app.agentdrive.sh';
5
11
  export function configDir(env = process.env, home = homedir()) {
6
12
  const xdg = env.XDG_CONFIG_HOME;
@@ -24,6 +30,7 @@ export async function readConfig(path = configPath()) {
24
30
  agent_platform: parsed.agent_platform ?? 'cli',
25
31
  agent_role: parsed.agent_role,
26
32
  default_workspace: parsed.default_workspace,
33
+ preferences: { ...DEFAULT_PREFERENCES, ...(parsed.preferences ?? {}) },
27
34
  };
28
35
  }
29
36
  catch (err) {
@@ -0,0 +1,55 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync, } from 'node:crypto';
2
+ // AgentDrive private-workspace encryption.
3
+ //
4
+ // Goal: a file uploaded with privacy on is opaque to the server. Even if our
5
+ // storage is breached, the bytes are unreadable without the local key.
6
+ //
7
+ // v0 derives the workspace data key from the API key with a scrypt KDF and a
8
+ // fixed salt. That gives a single client-side key per principal — enough to
9
+ // protect content from the server, not enough to protect content from a
10
+ // compromised API key. v1 moves the master key into the OS keychain and uses
11
+ // per-workspace data keys wrapped by it.
12
+ const SALT = Buffer.from('agentdrive.privacy.v0', 'utf8');
13
+ const KEY_LENGTH = 32; // AES-256
14
+ const IV_LENGTH = 12; // GCM standard
15
+ const TAG_LENGTH = 16; // GCM standard
16
+ const SCRYPT_COST = 1 << 14;
17
+ const SCRYPT_MAXMEM = 64 * 1024 * 1024;
18
+ const ENCRYPTED_PREFIX = 'agentdrive-encrypted:v0:';
19
+ export function deriveWorkspaceKey(apiKey) {
20
+ return scryptSync(apiKey, SALT, KEY_LENGTH, { N: SCRYPT_COST, maxmem: SCRYPT_MAXMEM });
21
+ }
22
+ export function encryptContent(apiKey, plaintext) {
23
+ const key = deriveWorkspaceKey(apiKey);
24
+ const iv = randomBytes(IV_LENGTH);
25
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
26
+ const buf = typeof plaintext === 'string' ? Buffer.from(plaintext, 'utf8') : plaintext;
27
+ const ciphertext = Buffer.concat([cipher.update(buf), cipher.final()]);
28
+ const tag = cipher.getAuthTag();
29
+ const payload = {
30
+ ciphertext: ciphertext.toString('base64'),
31
+ iv: iv.toString('base64'),
32
+ tag: tag.toString('base64'),
33
+ };
34
+ return ENCRYPTED_PREFIX + Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
35
+ }
36
+ export function isEncrypted(content) {
37
+ return typeof content === 'string' && content.startsWith(ENCRYPTED_PREFIX);
38
+ }
39
+ export function decryptContent(apiKey, encoded) {
40
+ if (!isEncrypted(encoded)) {
41
+ throw new Error('Content is not AgentDrive-encrypted.');
42
+ }
43
+ const payloadJson = Buffer.from(encoded.slice(ENCRYPTED_PREFIX.length), 'base64').toString('utf8');
44
+ const payload = JSON.parse(payloadJson);
45
+ const iv = Buffer.from(payload.iv, 'base64');
46
+ const tag = Buffer.from(payload.tag, 'base64');
47
+ if (iv.length !== IV_LENGTH || tag.length !== TAG_LENGTH) {
48
+ throw new Error('Malformed encrypted payload.');
49
+ }
50
+ const key = deriveWorkspaceKey(apiKey);
51
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
52
+ decipher.setAuthTag(tag);
53
+ const ciphertext = Buffer.from(payload.ciphertext, 'base64');
54
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adrive/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AgentDrive CLI — the persistent workspace your agents share across Claude Code, Codex, Cursor, and LangGraph.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",