@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,102 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { zstdDecompressSync } from 'node:zlib';
|
|
3
|
+
const SUMMARY_CHARS = 160;
|
|
4
|
+
const TITLE_CHARS = 120;
|
|
5
|
+
// Only these input keys feed tool summaries, so file bodies (`content`,
|
|
6
|
+
// `new_string`, ...) never reach a tool line.
|
|
7
|
+
const SUMMARY_KEYS = ['command', 'cmd', 'file_path', 'filePath', 'notebook_path', 'path', 'pattern', 'query', 'url', 'description'];
|
|
8
|
+
export function asObj(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function asStr(value) {
|
|
12
|
+
return typeof value === 'string' ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
export async function readText(path) {
|
|
15
|
+
try {
|
|
16
|
+
const buf = await fs.readFile(path);
|
|
17
|
+
return (path.endsWith('.zst') ? zstdDecompressSync(buf) : buf).toString('utf8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function jsonLines(raw) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
26
|
+
if (!line.trim())
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = asObj(JSON.parse(line));
|
|
30
|
+
if (parsed)
|
|
31
|
+
out.push(parsed);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Permissive: a torn last line or unknown record never fails a parse.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
export function truncate(value, max) {
|
|
40
|
+
const flat = value.replace(/\s+/g, ' ').trim();
|
|
41
|
+
return flat.length <= max ? flat : `${flat.slice(0, max - 3)}...`;
|
|
42
|
+
}
|
|
43
|
+
export function toolSummary(input) {
|
|
44
|
+
const obj = asObj(input);
|
|
45
|
+
if (!obj)
|
|
46
|
+
return typeof input === 'string' && input.trim() ? truncate(input.split('\n')[0], SUMMARY_CHARS) : undefined;
|
|
47
|
+
for (const key of SUMMARY_KEYS) {
|
|
48
|
+
const v = obj[key];
|
|
49
|
+
const s = Array.isArray(v) ? v.filter((x) => typeof x === 'string').join(' ') : asStr(v);
|
|
50
|
+
if (s && s.trim())
|
|
51
|
+
return truncate(s, SUMMARY_CHARS);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export function patchPaths(patch) {
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const m of patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm))
|
|
58
|
+
out.push(m[1].trim());
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
export async function mtimeOf(path) {
|
|
62
|
+
try {
|
|
63
|
+
return (await fs.stat(path)).mtime;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export class DocBuilder {
|
|
70
|
+
turns = [];
|
|
71
|
+
files = new Set();
|
|
72
|
+
addTurn(role, text, ts, tools = []) {
|
|
73
|
+
const last = this.turns[this.turns.length - 1];
|
|
74
|
+
if (!text.trim()) {
|
|
75
|
+
if (tools.length === 0)
|
|
76
|
+
return;
|
|
77
|
+
// Harnesses log tool calls as separate records; fold them into the
|
|
78
|
+
// turn they belong to instead of emitting empty turns.
|
|
79
|
+
if (last && last.role === role) {
|
|
80
|
+
last.tools = [...(last.tools ?? []), ...tools];
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.turns.push(tools.length > 0 ? { role, text, ts, tools } : { role, text, ts });
|
|
85
|
+
}
|
|
86
|
+
touch(path) {
|
|
87
|
+
if (path && path.trim())
|
|
88
|
+
this.files.add(path.trim());
|
|
89
|
+
}
|
|
90
|
+
build(entry, cwd, title) {
|
|
91
|
+
if (this.turns.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const firstUser = this.turns.find((t) => t.role === 'user' && t.text.trim());
|
|
94
|
+
return {
|
|
95
|
+
entry,
|
|
96
|
+
turns: this.turns,
|
|
97
|
+
cwd: cwd ?? entry.cwd,
|
|
98
|
+
title: title?.trim() ? truncate(title, TITLE_CHARS) : firstUser ? truncate(firstUser.text, TITLE_CHARS) : undefined,
|
|
99
|
+
filesTouched: [...this.files],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
export async function readTextIfExists(path) {
|
|
4
|
+
try {
|
|
5
|
+
return await fs.readFile(path, 'utf8');
|
|
6
|
+
}
|
|
7
|
+
catch (err) {
|
|
8
|
+
if (err.code === 'ENOENT')
|
|
9
|
+
return null;
|
|
10
|
+
throw err;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// Throws on unparseable content so a merge never replaces a file it could not
|
|
14
|
+
// read (JSONC with comments, a half-written file) with only our keys.
|
|
15
|
+
export async function readJsonObject(path) {
|
|
16
|
+
const raw = await readTextIfExists(path);
|
|
17
|
+
if (raw === null)
|
|
18
|
+
return null;
|
|
19
|
+
if (!raw.trim())
|
|
20
|
+
return {};
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error(`${path} is not plain JSON; left untouched`);
|
|
27
|
+
}
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
29
|
+
throw new Error(`${path} is not a JSON object; left untouched`);
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
export async function writeFileIfChanged(path, content, opts = {}) {
|
|
34
|
+
const current = await readTextIfExists(path);
|
|
35
|
+
if (current === content)
|
|
36
|
+
return 'unchanged';
|
|
37
|
+
await fs.mkdir(dirname(path), { recursive: true });
|
|
38
|
+
if (current !== null && opts.backup) {
|
|
39
|
+
// The first .bak is the user's pre-AgentDrive original; later runs must not overwrite it.
|
|
40
|
+
// 0600 because an original Codex config.toml can hold the API key init just removed.
|
|
41
|
+
try {
|
|
42
|
+
await fs.copyFile(path, `${path}.bak`, constants.COPYFILE_EXCL);
|
|
43
|
+
await fs.chmod(`${path}.bak`, 0o600);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err.code !== 'EEXIST')
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await fs.writeFile(path, content, 'utf8');
|
|
51
|
+
return current === null ? 'created' : 'updated';
|
|
52
|
+
}
|
|
53
|
+
export async function writeJsonIfChanged(path, value) {
|
|
54
|
+
return writeFileIfChanged(path, `${JSON.stringify(value, null, 2)}\n`, { backup: true });
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrive/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "AgentDrive CLI — cross-agent artifact workspace with provenance and portable storage across Claude Code, Codex, Cursor, and LangGraph.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,18 +37,13 @@
|
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build": "tsc && node -e \"import('node:fs').then(fs => fs.chmodSync('dist/index.js', 0o755))\"",
|
|
42
|
-
"dev": "tsc --watch",
|
|
43
|
-
"typecheck": "tsc --noEmit",
|
|
44
|
-
"test": "vitest run"
|
|
45
|
-
},
|
|
46
40
|
"dependencies": {
|
|
47
41
|
"chokidar": "^5.0.0",
|
|
48
42
|
"commander": "^12.1.0",
|
|
49
43
|
"kleur": "^4.1.5",
|
|
50
44
|
"open": "^10.1.0",
|
|
51
|
-
"prompts": "^2.4.2"
|
|
45
|
+
"prompts": "^2.4.2",
|
|
46
|
+
"@adrive/mcp-server": "^1.1.0"
|
|
52
47
|
},
|
|
53
48
|
"devDependencies": {
|
|
54
49
|
"@types/node": "^22.0.0",
|
|
@@ -57,6 +52,12 @@
|
|
|
57
52
|
"vitest": "^2.1.0"
|
|
58
53
|
},
|
|
59
54
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
55
|
+
"node": ">=22.15"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc && node -e \"import('node:fs').then(fs => fs.chmodSync('dist/index.js', 0o755))\"",
|
|
59
|
+
"dev": "tsc --watch",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run"
|
|
61
62
|
}
|
|
62
|
-
}
|
|
63
|
+
}
|
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import kleur from 'kleur';
|
|
5
|
-
import { CliApiClient } from '../api-client.js';
|
|
6
|
-
import { configDir } from '../config.js';
|
|
7
|
-
import { encryptContentAsync } from '../crypto.js';
|
|
8
|
-
import { redact, summarizeHits } from '../redactor.js';
|
|
9
|
-
function manifestPath() {
|
|
10
|
-
return join(configDir(), 'transcripts.manifest.json');
|
|
11
|
-
}
|
|
12
|
-
async function readManifest() {
|
|
13
|
-
try {
|
|
14
|
-
const raw = await fs.readFile(manifestPath(), 'utf8');
|
|
15
|
-
return JSON.parse(raw);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return { ingested: {} };
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
async function writeManifest(m) {
|
|
22
|
-
await fs.mkdir(configDir(), { recursive: true, mode: 0o700 });
|
|
23
|
-
await fs.writeFile(manifestPath(), JSON.stringify(m, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
24
|
-
}
|
|
25
|
-
function formatTranscript(turns) {
|
|
26
|
-
const lines = [];
|
|
27
|
-
for (const t of turns) {
|
|
28
|
-
lines.push(`## ${t.role}${t.ts ? ` _(${t.ts})_` : ''}`);
|
|
29
|
-
lines.push('');
|
|
30
|
-
lines.push(t.text);
|
|
31
|
-
lines.push('');
|
|
32
|
-
}
|
|
33
|
-
return lines.join('\n');
|
|
34
|
-
}
|
|
35
|
-
function buildMarkdown(platform, doc) {
|
|
36
|
-
return (`# Transcript — ${platform} / ${doc.entry.sessionId}\n\n` +
|
|
37
|
-
`Project: ${doc.entry.label}\nLast modified: ${doc.entry.mtime.toISOString()}\n` +
|
|
38
|
-
`Turns: ${doc.turns.length}\n\n` +
|
|
39
|
-
formatTranscript(doc.turns));
|
|
40
|
-
}
|
|
41
|
-
export async function ingestSource(source, cfg, opts = {}) {
|
|
42
|
-
const home = opts.home ?? homedir();
|
|
43
|
-
const since = opts.since ? new Date(opts.since) : undefined;
|
|
44
|
-
if (since && Number.isNaN(since.getTime())) {
|
|
45
|
-
throw new Error('Invalid --since date.');
|
|
46
|
-
}
|
|
47
|
-
let entries = await source.enumerate({ home, since });
|
|
48
|
-
if (typeof opts.limit === 'number')
|
|
49
|
-
entries = entries.slice(0, opts.limit);
|
|
50
|
-
if (entries.length === 0) {
|
|
51
|
-
process.stdout.write(`No ${source.platform} transcripts found.\n`);
|
|
52
|
-
return { platform: source.platform, ingested: 0, skipped: 0 };
|
|
53
|
-
}
|
|
54
|
-
const manifest = await readManifest();
|
|
55
|
-
const client = new CliApiClient({
|
|
56
|
-
baseUrl: cfg.api_url,
|
|
57
|
-
apiKey: cfg.api_key,
|
|
58
|
-
agentPlatform: cfg.agent_platform ?? 'cli',
|
|
59
|
-
agentRole: cfg.agent_role,
|
|
60
|
-
});
|
|
61
|
-
let workspaceId = opts.workspace ?? cfg.default_workspace;
|
|
62
|
-
if (!workspaceId && !opts.dryRun) {
|
|
63
|
-
const list = await client.listWorkspaces();
|
|
64
|
-
if (list.length === 1)
|
|
65
|
-
workspaceId = list[0].id;
|
|
66
|
-
else if (list.length === 0)
|
|
67
|
-
throw new Error('No workspaces. Create one first.');
|
|
68
|
-
else
|
|
69
|
-
throw new Error('Multiple workspaces — specify --workspace <id>.');
|
|
70
|
-
}
|
|
71
|
-
const encrypt = !!cfg.preferences?.private_workspace;
|
|
72
|
-
process.stdout.write(`${kleur.bold(`Found ${entries.length} ${source.platform} transcripts.`)}\n`);
|
|
73
|
-
let ingested = 0;
|
|
74
|
-
let skipped = 0;
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
if (!opts.reingest && manifest.ingested[entry.manifestKey]) {
|
|
77
|
-
skipped++;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
const doc = await source.parseSession(entry);
|
|
81
|
-
if (!doc) {
|
|
82
|
-
skipped++;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const md = buildMarkdown(source.platform, doc);
|
|
86
|
-
const { redacted, hits } = redact(md);
|
|
87
|
-
const tag = summarizeHits(hits);
|
|
88
|
-
const remotePath = `transcripts/${source.platform}/${entry.sessionId}.md`;
|
|
89
|
-
if (opts.dryRun) {
|
|
90
|
-
process.stdout.write(`${kleur.dim('[dry]')} ${remotePath} ${kleur.dim(`(${doc.turns.length} turns, redactor: ${tag})`)}\n`);
|
|
91
|
-
ingested++;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
const body = encrypt ? await encryptContentAsync(cfg.api_key, redacted) : redacted;
|
|
96
|
-
await client.writeFile(workspaceId, remotePath, body, `transcript ingest (${tag})`);
|
|
97
|
-
manifest.ingested[entry.manifestKey] = new Date().toISOString();
|
|
98
|
-
ingested++;
|
|
99
|
-
process.stdout.write(`${kleur.green('[ok]')} ${remotePath}` +
|
|
100
|
-
(encrypt ? kleur.cyan(' (encrypted)') : '') +
|
|
101
|
-
(hits.length ? kleur.yellow(` (redacted: ${tag})`) : '') +
|
|
102
|
-
'\n');
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
-
const base = entry.path.split('/').pop() ?? entry.path;
|
|
107
|
-
process.stderr.write(`${kleur.red('[err]')} ${base}: ${msg}\n`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (!opts.dryRun)
|
|
111
|
-
await writeManifest(manifest);
|
|
112
|
-
process.stdout.write(`\nIngested ${ingested}, skipped ${skipped}.\n`);
|
|
113
|
-
return { platform: source.platform, ingested, skipped };
|
|
114
|
-
}
|
|
115
|
-
export { buildMarkdown, formatTranscript };
|