@adrive/cli 0.2.1 → 1.0.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 +1 -1
- package/dist/commands/context.js +18 -1
- package/dist/commands/init.js +8 -0
- package/dist/commands/pull.js +2 -2
- package/dist/commands/push.js +2 -2
- package/dist/commands/timeline.js +65 -0
- package/dist/commands/transcripts.js +56 -0
- package/dist/commands/watch.js +110 -0
- package/dist/index.js +44 -1
- package/dist/lib/api-client.js +15 -0
- package/dist/lib/crypto.js +48 -21
- package/dist/lib/ignore.js +137 -0
- package/dist/lib/keychain.js +93 -0
- package/dist/lib/redactor.js +69 -0
- package/dist/lib/skill.js +117 -0
- package/dist/lib/transcripts/claude.js +112 -0
- package/dist/lib/transcripts/codex.js +168 -0
- package/dist/lib/transcripts/cursor.js +23 -0
- package/dist/lib/transcripts/ingest.js +115 -0
- package/dist/lib/transcripts/types.js +10 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @adrive/cli
|
|
2
2
|
|
|
3
|
-
The `adrive` binary — a thin CLI for [AgentDrive](https://agentdrive.sh), the
|
|
3
|
+
The `adrive` binary — a thin CLI for [AgentDrive](https://agentdrive.sh), the cross-agent artifact workspace with provenance and portable storage across Claude Code, Codex, Cursor, and LangGraph.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
package/dist/commands/context.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import kleur from 'kleur';
|
|
2
2
|
import { CliApiClient } from '../lib/api-client.js';
|
|
3
3
|
import { requireLogin } from '../lib/config.js';
|
|
4
|
+
import { installClaudeSkill, describeSkillAction } from '../lib/skill.js';
|
|
4
5
|
function truncate(s, n) {
|
|
5
6
|
if (!s)
|
|
6
7
|
return '';
|
|
7
8
|
return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
|
|
8
9
|
}
|
|
9
10
|
export async function cmdContext(task, opts = {}) {
|
|
11
|
+
if (opts.installSkill) {
|
|
12
|
+
const result = await installClaudeSkill({ force: opts.force });
|
|
13
|
+
process.stdout.write(`Claude skill ${describeSkillAction(result.action)}: ${kleur.dim(result.path)}\n`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!task) {
|
|
17
|
+
process.stderr.write('error: missing required argument <task>\n');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
10
20
|
const cfg = await requireLogin();
|
|
11
21
|
const client = new CliApiClient({
|
|
12
22
|
baseUrl: cfg.api_url,
|
|
@@ -25,8 +35,15 @@ export async function cmdContext(task, opts = {}) {
|
|
|
25
35
|
return;
|
|
26
36
|
}
|
|
27
37
|
for (const a of res.artifacts) {
|
|
28
|
-
const score = a.score.toFixed(2);
|
|
38
|
+
const score = a.score === null ? 'keyword' : a.score.toFixed(2);
|
|
29
39
|
process.stdout.write(`${kleur.cyan(a.path)} · score=${score} · by=${a.agent_id}@${a.agent_platform}\n`);
|
|
40
|
+
if (a.attribution?.reasons?.length) {
|
|
41
|
+
process.stdout.write(` ${kleur.dim('why: ' + a.attribution.reasons.join(' · '))}\n`);
|
|
42
|
+
}
|
|
43
|
+
if (a.last_run) {
|
|
44
|
+
const lr = a.last_run;
|
|
45
|
+
process.stdout.write(` ${kleur.dim('last run: ' + truncate(lr.task, 80) + ` [${lr.status}, ${lr.agent_platform}]`)}\n`);
|
|
46
|
+
}
|
|
30
47
|
if (a.snippet)
|
|
31
48
|
process.stdout.write(` ${kleur.dim(truncate(a.snippet, 500))}\n`);
|
|
32
49
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DEFAULT_API_URL, DEFAULT_PREFERENCES, readConfig, writeConfig, } from '
|
|
|
5
5
|
import { pollAuth } from '../lib/poll-auth.js';
|
|
6
6
|
import { detectInstalledAgents, wireAgent } from '../lib/detect-agents.js';
|
|
7
7
|
import { writeAllInstructions } from '../lib/auto-context.js';
|
|
8
|
+
import { installClaudeSkill } from '../lib/skill.js';
|
|
8
9
|
async function runPrivacyOptIn(existing, yes) {
|
|
9
10
|
const start = { ...DEFAULT_PREFERENCES, ...(existing ?? {}) };
|
|
10
11
|
process.stdout.write(`\n${kleur.bold('Your AI memory')}\n`);
|
|
@@ -144,6 +145,13 @@ export async function cmdInit(opts = {}) {
|
|
|
144
145
|
process.stdout.write(' No new instructions to write.\n');
|
|
145
146
|
}
|
|
146
147
|
}
|
|
148
|
+
// Step 4b — install the Claude Code skill file. Skips silently when ~/.claude
|
|
149
|
+
// is missing OR when the auto-context preflight was just written above
|
|
150
|
+
// (would double-fire on the same triggers).
|
|
151
|
+
const skillResult = await installClaudeSkill();
|
|
152
|
+
if (skillResult.action === 'created' || skillResult.action === 'updated') {
|
|
153
|
+
process.stdout.write(` ${kleur.green('+')} ${skillResult.path}\n`);
|
|
154
|
+
}
|
|
147
155
|
await writeConfig({
|
|
148
156
|
api_url: apiUrl,
|
|
149
157
|
api_key: apiKey,
|
package/dist/commands/pull.js
CHANGED
|
@@ -2,7 +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 {
|
|
5
|
+
import { decryptContentAsync, isEncrypted } from '../lib/crypto.js';
|
|
6
6
|
export async function cmdPull(spec, opts = {}) {
|
|
7
7
|
const cfg = await requireLogin();
|
|
8
8
|
const client = new CliApiClient({
|
|
@@ -19,7 +19,7 @@ export async function cmdPull(spec, opts = {}) {
|
|
|
19
19
|
const remotePath = spec.slice(slash + 1);
|
|
20
20
|
const file = await client.readFileByPath(workspaceId, remotePath);
|
|
21
21
|
const content = isEncrypted(file.content)
|
|
22
|
-
?
|
|
22
|
+
? await decryptContentAsync(cfg.api_key, file.content)
|
|
23
23
|
: file.content;
|
|
24
24
|
if (opts.out) {
|
|
25
25
|
await fs.writeFile(opts.out, content, 'utf8');
|
package/dist/commands/push.js
CHANGED
|
@@ -3,7 +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 {
|
|
6
|
+
import { encryptContentAsync } from '../lib/crypto.js';
|
|
7
7
|
export async function cmdPush(localPath, opts = {}) {
|
|
8
8
|
const cfg = await requireLogin();
|
|
9
9
|
const client = new CliApiClient({
|
|
@@ -23,7 +23,7 @@ export async function cmdPush(localPath, opts = {}) {
|
|
|
23
23
|
encrypt = false;
|
|
24
24
|
else
|
|
25
25
|
encrypt = cfg.preferences?.private_workspace ?? false;
|
|
26
|
-
const content = encrypt ?
|
|
26
|
+
const content = encrypt ? await encryptContentAsync(cfg.api_key, raw) : raw;
|
|
27
27
|
let workspaceId = opts.workspace ?? cfg.default_workspace;
|
|
28
28
|
if (!workspaceId) {
|
|
29
29
|
const list = await client.listWorkspaces();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import { CliApiClient } from '../lib/api-client.js';
|
|
3
|
+
import { requireLogin } from '../lib/config.js';
|
|
4
|
+
// Platform tints inspired by D4 (variant 2A "tinted pill with letter glyph").
|
|
5
|
+
// Falls through to no-color on unknown / undefined platforms.
|
|
6
|
+
const PLATFORM_GLYPH = {
|
|
7
|
+
'claude-code': { glyph: 'CC', tint: (s) => kleur.magenta().bold(s) },
|
|
8
|
+
codex: { glyph: 'CX', tint: (s) => kleur.cyan().bold(s) },
|
|
9
|
+
cursor: { glyph: 'CR', tint: (s) => kleur.blue().bold(s) },
|
|
10
|
+
langgraph: { glyph: 'LG', tint: (s) => kleur.yellow().bold(s) },
|
|
11
|
+
crewai: { glyph: 'CA', tint: (s) => kleur.green().bold(s) },
|
|
12
|
+
autogen: { glyph: 'AG', tint: (s) => kleur.green().bold(s) },
|
|
13
|
+
hermes: { glyph: 'HM', tint: (s) => kleur.red().bold(s) },
|
|
14
|
+
openclaw: { glyph: 'OC', tint: (s) => kleur.red().bold(s) },
|
|
15
|
+
};
|
|
16
|
+
function badge(platform) {
|
|
17
|
+
const key = platform ?? 'unknown';
|
|
18
|
+
const entry = PLATFORM_GLYPH[key];
|
|
19
|
+
if (!entry)
|
|
20
|
+
return kleur.dim('— ');
|
|
21
|
+
return entry.tint(entry.glyph);
|
|
22
|
+
}
|
|
23
|
+
function actorLabel(ev) {
|
|
24
|
+
if (ev.actor_type === 'human')
|
|
25
|
+
return ev.actor_id;
|
|
26
|
+
return ev.actor_id.length > 14 ? `${ev.actor_id.slice(0, 14)}…` : ev.actor_id;
|
|
27
|
+
}
|
|
28
|
+
function formatTimestamp(iso) {
|
|
29
|
+
const d = new Date(iso);
|
|
30
|
+
const yyyy = d.getUTCFullYear();
|
|
31
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
32
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
33
|
+
const hh = String(d.getUTCHours()).padStart(2, '0');
|
|
34
|
+
const min = String(d.getUTCMinutes()).padStart(2, '0');
|
|
35
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${min}Z`;
|
|
36
|
+
}
|
|
37
|
+
function startOfTodayIso() {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())).toISOString();
|
|
40
|
+
}
|
|
41
|
+
export async function cmdTimeline(opts = {}) {
|
|
42
|
+
const cfg = await requireLogin();
|
|
43
|
+
const client = new CliApiClient({
|
|
44
|
+
baseUrl: cfg.api_url,
|
|
45
|
+
apiKey: cfg.api_key,
|
|
46
|
+
agentPlatform: cfg.agent_platform ?? 'cli',
|
|
47
|
+
agentRole: cfg.agent_role,
|
|
48
|
+
});
|
|
49
|
+
const limit = opts.limit ? Number.parseInt(opts.limit, 10) : undefined;
|
|
50
|
+
const since = opts.today ? startOfTodayIso() : opts.since;
|
|
51
|
+
const { items, truncated } = await client.timeline({ since, limit });
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
process.stdout.write('No events in window.\n');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const ev of items) {
|
|
57
|
+
const action = kleur.cyan(ev.action.padEnd(16));
|
|
58
|
+
const target = ev.target_path ?? ev.target_id;
|
|
59
|
+
process.stdout.write(`${kleur.dim(formatTimestamp(ev.created_at))} ${badge(ev.agent_platform)} ` +
|
|
60
|
+
`${action} ${kleur.dim(actorLabel(ev))} ${kleur.dim('→')} ${target}\n`);
|
|
61
|
+
}
|
|
62
|
+
if (truncated) {
|
|
63
|
+
process.stdout.write(kleur.dim(`\n${items.length} shown; capped by --limit. Re-run with a tighter --since to page back.\n`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import { requireLogin } from '../lib/config.js';
|
|
3
|
+
import { claudeSource } from '../lib/transcripts/claude.js';
|
|
4
|
+
import { codexSource } from '../lib/transcripts/codex.js';
|
|
5
|
+
import { cursorSource } from '../lib/transcripts/cursor.js';
|
|
6
|
+
import { ingestSource } from '../lib/transcripts/ingest.js';
|
|
7
|
+
const SOURCES = {
|
|
8
|
+
'claude-code': claudeSource,
|
|
9
|
+
codex: codexSource,
|
|
10
|
+
cursor: cursorSource,
|
|
11
|
+
};
|
|
12
|
+
function selectSources(platform) {
|
|
13
|
+
if (!platform || platform === 'all') {
|
|
14
|
+
// Cursor is intentionally excluded from --all until Phase 2.5 because its
|
|
15
|
+
// enumerate() throws. Users that want it must opt in explicitly with
|
|
16
|
+
// --platform cursor and get the "not yet" error.
|
|
17
|
+
return [claudeSource, codexSource];
|
|
18
|
+
}
|
|
19
|
+
const source = SOURCES[platform];
|
|
20
|
+
if (!source) {
|
|
21
|
+
throw new Error(`Unknown platform: ${platform}. Supported: claude-code, codex, cursor, all.`);
|
|
22
|
+
}
|
|
23
|
+
return [source];
|
|
24
|
+
}
|
|
25
|
+
export async function cmdTranscriptsIngest(opts = {}) {
|
|
26
|
+
const cfg = await requireLogin();
|
|
27
|
+
if (!cfg.preferences?.capture_transcripts && !opts.dryRun) {
|
|
28
|
+
process.stderr.write(`${kleur.yellow('[warn]')} Remember conversations is off in your preferences.\n` +
|
|
29
|
+
` Enable with \`adrive privacy --capture-transcripts on\`,\n` +
|
|
30
|
+
` or run a one-off dry-run with \`adrive transcripts ingest --dry-run\`.\n`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
const sources = selectSources(opts.platform);
|
|
34
|
+
let totalIngested = 0;
|
|
35
|
+
let totalSkipped = 0;
|
|
36
|
+
for (const source of sources) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await ingestSource(source, cfg, {
|
|
39
|
+
workspace: opts.workspace,
|
|
40
|
+
since: opts.since,
|
|
41
|
+
dryRun: opts.dryRun,
|
|
42
|
+
limit: opts.limit,
|
|
43
|
+
reingest: opts.reingest,
|
|
44
|
+
});
|
|
45
|
+
totalIngested += result.ingested;
|
|
46
|
+
totalSkipped += result.skipped;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
+
process.stderr.write(`${kleur.red('[err]')} ${source.platform}: ${msg}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (sources.length > 1) {
|
|
54
|
+
process.stdout.write(`\n${kleur.bold('Total')}: ${totalIngested} ingested, ${totalSkipped} skipped.\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { resolve, relative, sep, posix } from 'node:path';
|
|
3
|
+
import { watch as chokidarWatch } from 'chokidar';
|
|
4
|
+
import kleur from 'kleur';
|
|
5
|
+
import { CliApiClient } from '../lib/api-client.js';
|
|
6
|
+
import { requireLogin } from '../lib/config.js';
|
|
7
|
+
import { encryptContentAsync } from '../lib/crypto.js';
|
|
8
|
+
import { redact, summarizeHits } from '../lib/redactor.js';
|
|
9
|
+
import { loadRules, shouldIgnore } from '../lib/ignore.js';
|
|
10
|
+
function toPosix(p) {
|
|
11
|
+
return p.split(sep).join(posix.sep);
|
|
12
|
+
}
|
|
13
|
+
export async function cmdWatch(rawRoot, opts = {}) {
|
|
14
|
+
const root = resolve(rawRoot ?? process.cwd());
|
|
15
|
+
const cfg = await requireLogin();
|
|
16
|
+
if (!cfg.preferences?.capture_artifacts && !opts.dryRun) {
|
|
17
|
+
process.stderr.write(`${kleur.yellow('[warn]')} Capture artifacts is off in your preferences.\n` +
|
|
18
|
+
` Either enable it with \`adrive privacy --capture-artifacts on\`\n` +
|
|
19
|
+
` or run a one-off dry-run with \`adrive watch --dry-run\`.\n`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
const dryRun = !!opts.dryRun;
|
|
23
|
+
const encrypt = !opts.noEncrypt && !!cfg.preferences?.private_workspace;
|
|
24
|
+
const debounceMs = opts.debounce ?? 750;
|
|
25
|
+
const rules = await loadRules(root);
|
|
26
|
+
const client = new CliApiClient({
|
|
27
|
+
baseUrl: cfg.api_url,
|
|
28
|
+
apiKey: cfg.api_key,
|
|
29
|
+
agentPlatform: cfg.agent_platform ?? 'cli',
|
|
30
|
+
agentRole: cfg.agent_role,
|
|
31
|
+
});
|
|
32
|
+
let workspaceId = opts.workspace ?? cfg.default_workspace;
|
|
33
|
+
if (!workspaceId && !dryRun) {
|
|
34
|
+
const list = await client.listWorkspaces();
|
|
35
|
+
if (list.length === 1)
|
|
36
|
+
workspaceId = list[0].id;
|
|
37
|
+
else if (list.length === 0)
|
|
38
|
+
throw new Error('No workspaces. Create one first.');
|
|
39
|
+
else
|
|
40
|
+
throw new Error(`Multiple workspaces — specify --workspace <id>.`);
|
|
41
|
+
}
|
|
42
|
+
process.stdout.write(`${kleur.bold('AgentDrive watch')}\n`);
|
|
43
|
+
process.stdout.write(` root: ${root}\n`);
|
|
44
|
+
process.stdout.write(` workspace: ${workspaceId ?? kleur.dim('(dry-run, none)')}\n`);
|
|
45
|
+
process.stdout.write(` mode: ${dryRun ? kleur.yellow('dry-run — nothing uploads') : kleur.green('live')}\n`);
|
|
46
|
+
process.stdout.write(` encryption: ${encrypt ? kleur.cyan('end-to-end on your device') : 'off'}\n`);
|
|
47
|
+
process.stdout.write(` redactor: on (secrets are scrubbed before anything leaves)\n\n`);
|
|
48
|
+
const queue = new Map();
|
|
49
|
+
let timer;
|
|
50
|
+
async function flush() {
|
|
51
|
+
const pending = [...queue.values()];
|
|
52
|
+
queue.clear();
|
|
53
|
+
for (const item of pending) {
|
|
54
|
+
try {
|
|
55
|
+
const raw = await fs.readFile(item.abs, 'utf8');
|
|
56
|
+
const { redacted, hits } = redact(raw);
|
|
57
|
+
const remotePath = toPosix(item.rel);
|
|
58
|
+
const tag = summarizeHits(hits);
|
|
59
|
+
if (dryRun) {
|
|
60
|
+
process.stdout.write(`${kleur.dim('[dry]')} ${item.reason.padEnd(6)} ${remotePath} ${kleur.dim(`(${raw.length}B, redactor: ${tag})`)}\n`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const body = encrypt ? await encryptContentAsync(cfg.api_key, redacted) : redacted;
|
|
64
|
+
const message = `adrive watch: ${item.reason}${hits.length ? ` (redacted ${tag})` : ''}`;
|
|
65
|
+
const result = await client.writeFile(workspaceId, remotePath, body, message);
|
|
66
|
+
process.stdout.write(`${kleur.green('[ok]')} ${item.reason.padEnd(6)} ${remotePath} v${result.version}` +
|
|
67
|
+
(encrypt ? kleur.cyan(' (encrypted)') : '') +
|
|
68
|
+
(hits.length ? kleur.yellow(` (redacted: ${tag})`) : '') +
|
|
69
|
+
'\n');
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
process.stderr.write(`${kleur.red('[err]')} ${item.rel}: ${msg}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function enqueue(absPath, reason) {
|
|
78
|
+
if (shouldIgnore(root, absPath, rules))
|
|
79
|
+
return;
|
|
80
|
+
const rel = relative(root, absPath);
|
|
81
|
+
queue.set(absPath, { abs: absPath, rel, reason });
|
|
82
|
+
if (timer)
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
timer = setTimeout(() => {
|
|
85
|
+
void flush();
|
|
86
|
+
}, debounceMs);
|
|
87
|
+
}
|
|
88
|
+
const watcher = chokidarWatch(root, {
|
|
89
|
+
ignoreInitial: false,
|
|
90
|
+
persistent: true,
|
|
91
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
|
|
92
|
+
ignored: (p) => shouldIgnore(root, resolve(p), rules),
|
|
93
|
+
});
|
|
94
|
+
watcher.on('add', (p) => enqueue(resolve(p), 'add'));
|
|
95
|
+
watcher.on('change', (p) => enqueue(resolve(p), 'change'));
|
|
96
|
+
watcher.on('error', (err) => {
|
|
97
|
+
process.stderr.write(`${kleur.red('[err]')} watcher: ${String(err)}\n`);
|
|
98
|
+
});
|
|
99
|
+
const shutdown = async () => {
|
|
100
|
+
if (timer)
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
await flush();
|
|
103
|
+
await watcher.close();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
};
|
|
106
|
+
process.on('SIGINT', () => void shutdown());
|
|
107
|
+
process.on('SIGTERM', () => void shutdown());
|
|
108
|
+
// Resolve only when the user kills the process.
|
|
109
|
+
await new Promise(() => { });
|
|
110
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,9 @@ 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
|
+
import { cmdWatch } from './commands/watch.js';
|
|
19
|
+
import { cmdTranscriptsIngest } from './commands/transcripts.js';
|
|
20
|
+
import { cmdTimeline } from './commands/timeline.js';
|
|
18
21
|
// Read from the bundled package.json so we never drift again.
|
|
19
22
|
import { readFileSync } from 'node:fs';
|
|
20
23
|
import { fileURLToPath } from 'node:url';
|
|
@@ -101,10 +104,19 @@ program
|
|
|
101
104
|
workspace: opts.workspace,
|
|
102
105
|
})));
|
|
103
106
|
program
|
|
104
|
-
.command('
|
|
107
|
+
.command('timeline')
|
|
108
|
+
.description('Cross-agent activity stream for your org')
|
|
109
|
+
.option('--today', 'Only events since midnight UTC')
|
|
110
|
+
.option('--since <iso>', 'Only events strictly after this ISO datetime')
|
|
111
|
+
.option('--limit <n>', 'Max events to show (default 200, max 1000)')
|
|
112
|
+
.action(wrap((opts) => cmdTimeline(opts)));
|
|
113
|
+
program
|
|
114
|
+
.command('context [task]')
|
|
105
115
|
.description('Auto-pull relevant artifacts for a task')
|
|
106
116
|
.option('--limit <n>', 'Max artifacts to return')
|
|
107
117
|
.option('-w, --workspace <id>', 'Restrict to one workspace')
|
|
118
|
+
.option('--install-skill', 'Install the Claude Code SKILL.md (~/.claude/skills/agentdrive/)')
|
|
119
|
+
.option('--force', 'With --install-skill, write even if the auto-context preflight is already in CLAUDE.md')
|
|
108
120
|
.action(wrap((task, opts) => cmdContext(task, opts)));
|
|
109
121
|
program
|
|
110
122
|
.command('run-log <task>')
|
|
@@ -127,6 +139,37 @@ program
|
|
|
127
139
|
.command('plan')
|
|
128
140
|
.description('Show current plan, usage, and quotas')
|
|
129
141
|
.action(wrap(() => cmdPlan()));
|
|
142
|
+
program
|
|
143
|
+
.command('watch [path]')
|
|
144
|
+
.description('Capture files your agents write under the given path (default: cwd)')
|
|
145
|
+
.option('-w, --workspace <id>', 'Workspace id (default: only workspace)')
|
|
146
|
+
.option('--dry-run', 'Print what would be uploaded; never write')
|
|
147
|
+
.option('--no-encrypt', 'Skip end-to-end encryption even when private workspace is on')
|
|
148
|
+
.option('--debounce <ms>', 'Coalesce changes within this window', (v) => Number(v))
|
|
149
|
+
.action(wrap((path, opts) => cmdWatch(path, {
|
|
150
|
+
workspace: opts.workspace,
|
|
151
|
+
dryRun: opts.dryRun,
|
|
152
|
+
noEncrypt: opts.encrypt === false,
|
|
153
|
+
debounce: opts.debounce,
|
|
154
|
+
})));
|
|
155
|
+
const transcripts = program.command('transcripts').description('Ingest chat transcripts from local agent hosts');
|
|
156
|
+
transcripts
|
|
157
|
+
.command('ingest')
|
|
158
|
+
.description('Read recent transcripts, redact secrets, upload to AgentDrive')
|
|
159
|
+
.option('--platform <p>', 'claude-code | codex | cursor | all (default: all)', 'all')
|
|
160
|
+
.option('-w, --workspace <id>', 'Target workspace id')
|
|
161
|
+
.option('--since <date>', 'Ignore transcripts older than this ISO date')
|
|
162
|
+
.option('--limit <n>', 'Cap how many to ingest', (v) => Number(v))
|
|
163
|
+
.option('--dry-run', 'Print what would be uploaded; never write')
|
|
164
|
+
.option('--reingest', 'Re-upload sessions already in the manifest')
|
|
165
|
+
.action(wrap((opts) => cmdTranscriptsIngest({
|
|
166
|
+
platform: opts.platform,
|
|
167
|
+
workspace: opts.workspace,
|
|
168
|
+
since: opts.since,
|
|
169
|
+
limit: opts.limit,
|
|
170
|
+
dryRun: opts.dryRun,
|
|
171
|
+
reingest: opts.reingest,
|
|
172
|
+
})));
|
|
130
173
|
program
|
|
131
174
|
.command('privacy')
|
|
132
175
|
.description('Show or change your privacy + memory preferences')
|
package/dist/lib/api-client.js
CHANGED
|
@@ -119,4 +119,19 @@ export class CliApiClient {
|
|
|
119
119
|
async billingPlan() {
|
|
120
120
|
return this.request('GET', '/billing/plan');
|
|
121
121
|
}
|
|
122
|
+
async timeline(opts = {}) {
|
|
123
|
+
const params = new URLSearchParams();
|
|
124
|
+
if (opts.since)
|
|
125
|
+
params.set('since', opts.since);
|
|
126
|
+
if (typeof opts.limit === 'number')
|
|
127
|
+
params.set('limit', String(opts.limit));
|
|
128
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
129
|
+
const url = `${this.baseUrl}/v1/org/timeline${query}`;
|
|
130
|
+
const res = await fetch(url, { method: 'GET', headers: this.headers(true) });
|
|
131
|
+
const json = (await res.json());
|
|
132
|
+
if (!res.ok || !json.success) {
|
|
133
|
+
throw new Error(json.error ?? `API request failed (${res.status})`);
|
|
134
|
+
}
|
|
135
|
+
return { items: (json.data ?? []), truncated: !!json.truncated };
|
|
136
|
+
}
|
|
122
137
|
}
|
package/dist/lib/crypto.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, randomBytes, scryptSync
|
|
2
|
-
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
2
|
+
import { loadDek } from './keychain.js';
|
|
3
|
+
// AgentDrive private-workspace encryption (v1).
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
5
|
+
// Sync API (`encryptContent` / `decryptContent`) keeps the legacy v0
|
|
6
|
+
// derivation: scrypt(apiKey, fixedSalt). Used by tests + any caller that
|
|
7
|
+
// can't await. The async API (`encryptContentAsync` / `decryptContentAsync`)
|
|
8
|
+
// uses the keychain-stored DEK from lib/keychain.ts — that's the path the
|
|
9
|
+
// real `adrive push` / `adrive pull` take.
|
|
6
10
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const KEY_LENGTH = 32; // AES-256
|
|
14
|
-
const IV_LENGTH = 12; // GCM standard
|
|
15
|
-
const TAG_LENGTH = 16; // GCM standard
|
|
11
|
+
// Why two paths: a published v0.2.x ciphertext on disk should still decrypt
|
|
12
|
+
// after the upgrade. The async path is opt-in and used for new writes.
|
|
13
|
+
const LEGACY_SALT = Buffer.from('agentdrive.privacy.v0', 'utf8');
|
|
14
|
+
const KEY_LENGTH = 32;
|
|
15
|
+
const IV_LENGTH = 12;
|
|
16
|
+
const TAG_LENGTH = 16;
|
|
16
17
|
const SCRYPT_COST = 1 << 14;
|
|
17
18
|
const SCRYPT_MAXMEM = 64 * 1024 * 1024;
|
|
18
19
|
const ENCRYPTED_PREFIX = 'agentdrive-encrypted:v0:';
|
|
19
20
|
export function deriveWorkspaceKey(apiKey) {
|
|
20
|
-
return scryptSync(apiKey,
|
|
21
|
+
return scryptSync(apiKey, LEGACY_SALT, KEY_LENGTH, { N: SCRYPT_COST, maxmem: SCRYPT_MAXMEM });
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
const key = deriveWorkspaceKey(apiKey);
|
|
23
|
+
function encryptWithKey(key, plaintext) {
|
|
24
24
|
const iv = randomBytes(IV_LENGTH);
|
|
25
25
|
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
26
26
|
const buf = typeof plaintext === 'string' ? Buffer.from(plaintext, 'utf8') : plaintext;
|
|
@@ -33,10 +33,7 @@ export function encryptContent(apiKey, plaintext) {
|
|
|
33
33
|
};
|
|
34
34
|
return ENCRYPTED_PREFIX + Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
return typeof content === 'string' && content.startsWith(ENCRYPTED_PREFIX);
|
|
38
|
-
}
|
|
39
|
-
export function decryptContent(apiKey, encoded) {
|
|
36
|
+
function decryptWithKey(key, encoded) {
|
|
40
37
|
if (!isEncrypted(encoded)) {
|
|
41
38
|
throw new Error('Content is not AgentDrive-encrypted.');
|
|
42
39
|
}
|
|
@@ -47,9 +44,39 @@ export function decryptContent(apiKey, encoded) {
|
|
|
47
44
|
if (iv.length !== IV_LENGTH || tag.length !== TAG_LENGTH) {
|
|
48
45
|
throw new Error('Malformed encrypted payload.');
|
|
49
46
|
}
|
|
50
|
-
const key = deriveWorkspaceKey(apiKey);
|
|
51
47
|
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
52
48
|
decipher.setAuthTag(tag);
|
|
53
49
|
const ciphertext = Buffer.from(payload.ciphertext, 'base64');
|
|
54
50
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
55
51
|
}
|
|
52
|
+
export function encryptContent(apiKey, plaintext) {
|
|
53
|
+
return encryptWithKey(deriveWorkspaceKey(apiKey), plaintext);
|
|
54
|
+
}
|
|
55
|
+
export function decryptContent(apiKey, encoded) {
|
|
56
|
+
if (!isEncrypted(encoded)) {
|
|
57
|
+
throw new Error('Content is not AgentDrive-encrypted.');
|
|
58
|
+
}
|
|
59
|
+
return decryptWithKey(deriveWorkspaceKey(apiKey), encoded);
|
|
60
|
+
}
|
|
61
|
+
export async function encryptContentAsync(apiKey, plaintext) {
|
|
62
|
+
const dek = await loadDek(apiKey);
|
|
63
|
+
return encryptWithKey(dek, plaintext);
|
|
64
|
+
}
|
|
65
|
+
export async function decryptContentAsync(apiKey, encoded) {
|
|
66
|
+
const dek = await loadDek(apiKey);
|
|
67
|
+
try {
|
|
68
|
+
return decryptWithKey(dek, encoded);
|
|
69
|
+
}
|
|
70
|
+
catch (errKeychain) {
|
|
71
|
+
// Fallback for ciphertext written by v0 builds (pre-keychain).
|
|
72
|
+
try {
|
|
73
|
+
return decryptWithKey(deriveWorkspaceKey(apiKey), encoded);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw errKeychain;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function isEncrypted(content) {
|
|
81
|
+
return typeof content === 'string' && content.startsWith(ENCRYPTED_PREFIX);
|
|
82
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { join, relative, sep, posix } from 'node:path';
|
|
3
|
+
// .adriveignore parser. Subset of gitignore semantics: blank lines and lines
|
|
4
|
+
// starting with '#' are skipped; '!' negates a previous pattern; '/' anchors
|
|
5
|
+
// to the project root; '**' matches across directories; '*' matches within a
|
|
6
|
+
// segment.
|
|
7
|
+
//
|
|
8
|
+
// In addition we always layer a default-deny set on top so first-time users
|
|
9
|
+
// don't accidentally upload .env / .ssh / .aws / node_modules even before
|
|
10
|
+
// they edit the file.
|
|
11
|
+
export const DEFAULT_DENY = [
|
|
12
|
+
'.git/',
|
|
13
|
+
'node_modules/',
|
|
14
|
+
'.next/',
|
|
15
|
+
'.open-next/',
|
|
16
|
+
'.turbo/',
|
|
17
|
+
'dist/',
|
|
18
|
+
'build/',
|
|
19
|
+
'coverage/',
|
|
20
|
+
'.cache/',
|
|
21
|
+
'.DS_Store',
|
|
22
|
+
'.env',
|
|
23
|
+
'.env.*',
|
|
24
|
+
'*.pem',
|
|
25
|
+
'*.key',
|
|
26
|
+
'*.p12',
|
|
27
|
+
'*.pfx',
|
|
28
|
+
'*.crt',
|
|
29
|
+
'.ssh/',
|
|
30
|
+
'.aws/',
|
|
31
|
+
'.gcp/',
|
|
32
|
+
'.kube/',
|
|
33
|
+
'.docker/config.json',
|
|
34
|
+
'.npmrc',
|
|
35
|
+
'.pypirc',
|
|
36
|
+
'.netrc',
|
|
37
|
+
'id_rsa',
|
|
38
|
+
'id_ed25519',
|
|
39
|
+
'credentials.json',
|
|
40
|
+
'serviceAccount.json',
|
|
41
|
+
'*.sqlite',
|
|
42
|
+
'*.sqlite3',
|
|
43
|
+
'*.db',
|
|
44
|
+
'__pycache__/',
|
|
45
|
+
];
|
|
46
|
+
function patternToRegex(raw) {
|
|
47
|
+
let pat = raw.trim();
|
|
48
|
+
const negate = pat.startsWith('!');
|
|
49
|
+
if (negate)
|
|
50
|
+
pat = pat.slice(1);
|
|
51
|
+
// Trailing slash means directory-only; treat it as match-anywhere-inside.
|
|
52
|
+
const dirOnly = pat.endsWith('/');
|
|
53
|
+
if (dirOnly)
|
|
54
|
+
pat = pat.slice(0, -1);
|
|
55
|
+
// Anchor to root if leading '/'.
|
|
56
|
+
const anchored = pat.startsWith('/');
|
|
57
|
+
if (anchored)
|
|
58
|
+
pat = pat.slice(1);
|
|
59
|
+
let re = '';
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (i < pat.length) {
|
|
62
|
+
const c = pat[i];
|
|
63
|
+
if (c === '*') {
|
|
64
|
+
// '**' matches anything including slashes; single '*' matches within
|
|
65
|
+
// a path segment only.
|
|
66
|
+
if (pat[i + 1] === '*') {
|
|
67
|
+
re += '.*';
|
|
68
|
+
i += 2;
|
|
69
|
+
if (pat[i] === '/')
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
re += '[^/]*';
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (c === '?') {
|
|
78
|
+
re += '[^/]';
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
else if ('.+^$()[]{}|\\'.includes(c)) {
|
|
82
|
+
re += '\\' + c;
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
re += c;
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// dirOnly: match the dir itself or anything under it.
|
|
91
|
+
if (dirOnly)
|
|
92
|
+
re += '(?:/.*)?';
|
|
93
|
+
// anchored = at root only; unanchored = at any depth.
|
|
94
|
+
const final = anchored ? `^${re}$` : `^(?:.*/)?${re}$`;
|
|
95
|
+
return { re: new RegExp(final), negate };
|
|
96
|
+
}
|
|
97
|
+
export function compileRules(lines) {
|
|
98
|
+
const rules = [];
|
|
99
|
+
for (const raw of lines) {
|
|
100
|
+
const line = raw.replace(/^\s+|\s+$/g, '');
|
|
101
|
+
if (!line || line.startsWith('#'))
|
|
102
|
+
continue;
|
|
103
|
+
const { re, negate } = patternToRegex(line);
|
|
104
|
+
rules.push({ pattern: line, negate, re });
|
|
105
|
+
}
|
|
106
|
+
return rules;
|
|
107
|
+
}
|
|
108
|
+
export async function loadRules(root) {
|
|
109
|
+
const candidates = [join(root, '.adriveignore'), join(root, '.gitignore')];
|
|
110
|
+
const lines = [...DEFAULT_DENY];
|
|
111
|
+
for (const c of candidates) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = await fs.readFile(c, 'utf8');
|
|
114
|
+
lines.push(...raw.split(/\r?\n/));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// ignore missing
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return compileRules(lines);
|
|
121
|
+
}
|
|
122
|
+
function normalizeRelative(root, path) {
|
|
123
|
+
const rel = relative(root, path);
|
|
124
|
+
return rel.split(sep).join(posix.sep);
|
|
125
|
+
}
|
|
126
|
+
export function shouldIgnore(root, path, rules) {
|
|
127
|
+
const rel = normalizeRelative(root, path);
|
|
128
|
+
if (!rel || rel.startsWith('..'))
|
|
129
|
+
return true;
|
|
130
|
+
let ignored = false;
|
|
131
|
+
for (const r of rules) {
|
|
132
|
+
if (r.re.test(rel)) {
|
|
133
|
+
ignored = !r.negate;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return ignored;
|
|
137
|
+
}
|