@adrive/cli 0.1.0 → 0.2.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/dist/commands/init.js +96 -14
- package/dist/commands/privacy.js +95 -0
- package/dist/commands/pull.js +7 -3
- package/dist/commands/push.js +14 -2
- package/dist/index.js +25 -1
- package/dist/lib/auto-context.js +116 -0
- package/dist/lib/config.js +7 -0
- package/dist/lib/crypto.js +55 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -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:
|
|
79
|
-
process.stdout.write(` Skipped:
|
|
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(' -
|
|
82
|
-
process.stdout.write(' - Try: `adrive
|
|
83
|
-
process.stdout.write(' -
|
|
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
|
+
}
|
package/dist/commands/pull.js
CHANGED
|
@@ -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,
|
|
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(
|
|
26
|
-
if (!
|
|
29
|
+
process.stdout.write(content);
|
|
30
|
+
if (!content.endsWith('\n'))
|
|
27
31
|
process.stdout.write('\n');
|
|
28
32
|
}
|
|
29
33
|
}
|
package/dist/commands/push.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,7 +14,14 @@ 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
|
-
|
|
17
|
+
import { cmdPrivacy } from './commands/privacy.js';
|
|
18
|
+
// Read from the bundled package.json so we never drift again.
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { dirname as pathDirname, join as pathJoin } from 'node:path';
|
|
22
|
+
const __dirname = pathDirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const pkg = JSON.parse(readFileSync(pathJoin(__dirname, '..', 'package.json'), 'utf8'));
|
|
24
|
+
const VERSION = pkg.version;
|
|
18
25
|
function fail(err) {
|
|
19
26
|
const msg = err instanceof Error ? err.message : String(err);
|
|
20
27
|
process.stderr.write(`${kleur.red('[error]')} ${msg}\n`);
|
|
@@ -74,6 +81,8 @@ program
|
|
|
74
81
|
.option('-w, --workspace <id>', 'Workspace id (default: only workspace, if exactly one)')
|
|
75
82
|
.option('-r, --remote-path <path>', 'Remote path (default: basename of local-path)')
|
|
76
83
|
.option('-m, --message <msg>', 'Commit-style message stored on the version')
|
|
84
|
+
.option('--encrypt', 'Force end-to-end encryption for this upload')
|
|
85
|
+
.option('--plaintext', 'Force plaintext upload, even if private workspace is on')
|
|
77
86
|
.action(wrap((localPath, opts) => cmdPush(localPath, opts)));
|
|
78
87
|
program
|
|
79
88
|
.command('pull <workspace-id-slash-path>')
|
|
@@ -118,4 +127,19 @@ program
|
|
|
118
127
|
.command('plan')
|
|
119
128
|
.description('Show current plan, usage, and quotas')
|
|
120
129
|
.action(wrap(() => cmdPlan()));
|
|
130
|
+
program
|
|
131
|
+
.command('privacy')
|
|
132
|
+
.description('Show or change your privacy + memory preferences')
|
|
133
|
+
.option('--show', 'Just print current preferences')
|
|
134
|
+
.option('--auto-context <on|off>', 'Always-on context for new sessions')
|
|
135
|
+
.option('--capture-artifacts <on|off>', 'Auto-save files your agents write')
|
|
136
|
+
.option('--capture-transcripts <on|off>', 'Auto-save chat history')
|
|
137
|
+
.option('--private-workspace <on|off>', 'End-to-end encrypt your workspace')
|
|
138
|
+
.action(wrap((opts) => cmdPrivacy({
|
|
139
|
+
show: opts.show,
|
|
140
|
+
autoContext: opts.autoContext,
|
|
141
|
+
captureArtifacts: opts.captureArtifacts,
|
|
142
|
+
captureTranscripts: opts.captureTranscripts,
|
|
143
|
+
privateWorkspace: opts.privateWorkspace,
|
|
144
|
+
})));
|
|
121
145
|
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
|
+
}
|
package/dist/lib/config.js
CHANGED
|
@@ -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