@adrive/mcp-server 1.0.0 → 1.1.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 +17 -25
- package/dist/api-client.d.ts +36 -1
- package/dist/api-client.js +26 -2
- package/dist/index.js +25 -74
- package/dist/project.d.ts +3 -0
- package/dist/project.js +69 -0
- package/dist/registry.d.ts +20 -0
- package/dist/registry.js +73 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.js +158 -0
- package/dist/tools/drive-context.d.ts +1 -0
- package/dist/tools/drive-context.js +1 -1
- package/dist/tools/drive-resume.d.ts +17 -0
- package/dist/tools/drive-resume.js +37 -0
- package/dist/tools/drive-search.d.ts +4 -4
- package/dist/tools/drive-search.js +2 -0
- package/dist/tools/drive-sessions.d.ts +23 -0
- package/dist/tools/drive-sessions.js +41 -0
- package/package.json +15 -10
package/README.md
CHANGED
|
@@ -16,37 +16,28 @@ npx @adrive/mcp-server
|
|
|
16
16
|
|
|
17
17
|
| Variable | Required | Default | Notes |
|
|
18
18
|
|---|---|---|---|
|
|
19
|
-
| `AGENTDRIVE_API_KEY` |
|
|
19
|
+
| `AGENTDRIVE_API_KEY` | no | key saved by `adrive auth login` | Falls back to `~/.config/agentdrive/config.json`, so client configs never need the key inline. |
|
|
20
20
|
| `AGENTDRIVE_API_URL` | no | `https://app.agentdrive.sh` | Override for self-host or staging. |
|
|
21
|
-
| `AGENTDRIVE_AGENT_PLATFORM` | no | `
|
|
21
|
+
| `AGENTDRIVE_AGENT_PLATFORM` | no | `unknown` | Identifies which host is calling. Valid: `claude-code | codex | opencode | gemini-cli | cursor | langgraph | crewai | autogen | hermes | openclaw | unknown`. Unknown values downgrade to `unknown` server-side. |
|
|
22
22
|
| `AGENTDRIVE_AGENT_ROLE` | no | (empty) | Free-form role label (`planner`, `reviewer`, etc.). Stamped on every request for provenance. |
|
|
23
23
|
|
|
24
|
+
The server derives the current project from its working directory (git remote, else repo path), which scopes `drive_sessions` and `drive_resume`.
|
|
25
|
+
|
|
24
26
|
## Client configuration
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
`curl -fsSL https://app.agentdrive.sh/install | sh` runs `adrive init`, which wires every detected harness. For Claude Code, install the plugin instead: `/plugin marketplace add Rakesh1002/agentdrive` then `/plugin install agentdrive@agentdrive`.
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
"mcpServers": {
|
|
31
|
-
"agentdrive": {
|
|
32
|
-
"command": "npx",
|
|
33
|
-
"args": ["-y", "@adrive/mcp-server"],
|
|
34
|
-
"env": {
|
|
35
|
-
"AGENTDRIVE_API_KEY": "agentdrive_live_..."
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
```
|
|
30
|
+
Manual entries, for reference:
|
|
41
31
|
|
|
42
|
-
### Codex CLI (`~/.codex/config.toml`
|
|
32
|
+
### Codex CLI (`~/.codex/config.toml`)
|
|
43
33
|
|
|
44
34
|
```toml
|
|
45
|
-
[
|
|
46
|
-
name = "agentdrive"
|
|
35
|
+
[mcp_servers.agentdrive]
|
|
47
36
|
command = "npx"
|
|
48
37
|
args = ["-y", "@adrive/mcp-server"]
|
|
49
|
-
|
|
38
|
+
|
|
39
|
+
[mcp_servers.agentdrive.env]
|
|
40
|
+
AGENTDRIVE_AGENT_PLATFORM = "codex"
|
|
50
41
|
```
|
|
51
42
|
|
|
52
43
|
### Cursor (`~/.cursor/mcp.json`)
|
|
@@ -57,16 +48,13 @@ env = { AGENTDRIVE_API_KEY = "agentdrive_live_...", AGENTDRIVE_AGENT_PLATFORM =
|
|
|
57
48
|
"agentdrive": {
|
|
58
49
|
"command": "npx",
|
|
59
50
|
"args": ["-y", "@adrive/mcp-server"],
|
|
60
|
-
"env": {
|
|
61
|
-
"AGENTDRIVE_API_KEY": "agentdrive_live_...",
|
|
62
|
-
"AGENTDRIVE_AGENT_PLATFORM": "cursor"
|
|
63
|
-
}
|
|
51
|
+
"env": { "AGENTDRIVE_AGENT_PLATFORM": "cursor" }
|
|
64
52
|
}
|
|
65
53
|
}
|
|
66
54
|
}
|
|
67
55
|
```
|
|
68
56
|
|
|
69
|
-
## Tools (
|
|
57
|
+
## Tools (22)
|
|
70
58
|
|
|
71
59
|
**Files**
|
|
72
60
|
- `drive_write` — create or update a file with versioning
|
|
@@ -83,6 +71,10 @@ env = { AGENTDRIVE_API_KEY = "agentdrive_live_...", AGENTDRIVE_AGENT_PLATFORM =
|
|
|
83
71
|
- `drive_activity` — workspace event feed
|
|
84
72
|
- `drive_versions` — file version history
|
|
85
73
|
|
|
74
|
+
**Sessions**
|
|
75
|
+
- `drive_sessions` — recent sessions in this project from every harness
|
|
76
|
+
- `drive_resume` — load a prior session's handoff (defaults to the latest from another harness)
|
|
77
|
+
|
|
86
78
|
**Search and memory**
|
|
87
79
|
- `drive_search` — semantic search across artifacts
|
|
88
80
|
- `drive_recall` — recall facts and preferences (narrows to extracted facts in a future release)
|
package/dist/api-client.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface ApiClientConfig {
|
|
|
3
3
|
apiKey: string;
|
|
4
4
|
agentPlatform?: string;
|
|
5
5
|
agentRole?: string;
|
|
6
|
+
project?: string;
|
|
7
|
+
fetchImpl?: (input: string, init?: RequestInit) => Promise<Response>;
|
|
6
8
|
}
|
|
7
9
|
export interface PagedResponse<T> {
|
|
8
10
|
items: T[];
|
|
@@ -51,11 +53,35 @@ export interface RunRequestBody {
|
|
|
51
53
|
duration_ms?: number;
|
|
52
54
|
metadata?: Record<string, unknown>;
|
|
53
55
|
}
|
|
56
|
+
export interface AgentRunRecord {
|
|
57
|
+
run_id: string;
|
|
58
|
+
org_id: string;
|
|
59
|
+
workspace_id?: string;
|
|
60
|
+
agent_id: string;
|
|
61
|
+
agent_platform: string;
|
|
62
|
+
agent_role?: string;
|
|
63
|
+
task: string;
|
|
64
|
+
artifacts_referenced: string[];
|
|
65
|
+
artifacts_produced: string[];
|
|
66
|
+
duration_ms?: number;
|
|
67
|
+
status: RunStatus;
|
|
68
|
+
metadata: Record<string, unknown>;
|
|
69
|
+
created_at: number;
|
|
70
|
+
updated_at?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface ListRunsQuery {
|
|
73
|
+
project: string;
|
|
74
|
+
platform?: string;
|
|
75
|
+
limit?: number;
|
|
76
|
+
cursor?: string;
|
|
77
|
+
}
|
|
54
78
|
export declare class ApiClient {
|
|
79
|
+
readonly project?: string;
|
|
80
|
+
readonly agentPlatform: string;
|
|
55
81
|
private baseUrl;
|
|
56
82
|
private apiKey;
|
|
57
|
-
private agentPlatform;
|
|
58
83
|
private agentRole;
|
|
84
|
+
private fetchImpl;
|
|
59
85
|
constructor(config: ApiClientConfig);
|
|
60
86
|
private headers;
|
|
61
87
|
rawFetch(path: string, init: RequestInit): Promise<Response>;
|
|
@@ -81,6 +107,13 @@ export declare class ApiClient {
|
|
|
81
107
|
version: number;
|
|
82
108
|
updated_at: string;
|
|
83
109
|
}>;
|
|
110
|
+
readFileByPath(workspaceId: string, path: string): Promise<{
|
|
111
|
+
id: string;
|
|
112
|
+
path: string;
|
|
113
|
+
content: string;
|
|
114
|
+
version: number;
|
|
115
|
+
updated_at: string;
|
|
116
|
+
}>;
|
|
84
117
|
listFilesPage(workspaceId: string, prefix: string | undefined, cursor: string | null): Promise<PagedResponse<{
|
|
85
118
|
id: string;
|
|
86
119
|
path: string;
|
|
@@ -232,6 +265,8 @@ export declare class ApiClient {
|
|
|
232
265
|
run_id: string;
|
|
233
266
|
created_at: number;
|
|
234
267
|
}>;
|
|
268
|
+
listRuns(query: ListRunsQuery): Promise<PagedResponse<AgentRunRecord>>;
|
|
269
|
+
getRun(runId: string): Promise<AgentRunRecord>;
|
|
235
270
|
sandboxClone(workspaceId: string, body: {
|
|
236
271
|
url: string;
|
|
237
272
|
branch?: string;
|
package/dist/api-client.js
CHANGED
|
@@ -12,15 +12,19 @@ export async function listAllPages(fetchPage, maxPages = 10) {
|
|
|
12
12
|
return all;
|
|
13
13
|
}
|
|
14
14
|
export class ApiClient {
|
|
15
|
+
project;
|
|
16
|
+
agentPlatform;
|
|
15
17
|
baseUrl;
|
|
16
18
|
apiKey;
|
|
17
|
-
agentPlatform;
|
|
18
19
|
agentRole;
|
|
20
|
+
fetchImpl;
|
|
19
21
|
constructor(config) {
|
|
20
22
|
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
21
23
|
this.apiKey = config.apiKey;
|
|
22
24
|
this.agentPlatform = config.agentPlatform ?? 'claude-code';
|
|
23
25
|
this.agentRole = config.agentRole ?? '';
|
|
26
|
+
this.project = config.project;
|
|
27
|
+
this.fetchImpl = config.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
24
28
|
}
|
|
25
29
|
headers() {
|
|
26
30
|
return {
|
|
@@ -32,7 +36,7 @@ export class ApiClient {
|
|
|
32
36
|
}
|
|
33
37
|
async rawFetch(path, init) {
|
|
34
38
|
const url = `${this.baseUrl}/v1${path}`;
|
|
35
|
-
return
|
|
39
|
+
return this.fetchImpl(url, {
|
|
36
40
|
...init,
|
|
37
41
|
headers: { ...this.headers(), ...init.headers },
|
|
38
42
|
});
|
|
@@ -68,6 +72,13 @@ export class ApiClient {
|
|
|
68
72
|
async readFile(workspaceId, fileId) {
|
|
69
73
|
return this.request('GET', `/workspaces/${workspaceId}/files/${fileId}?content=true`);
|
|
70
74
|
}
|
|
75
|
+
async readFileByPath(workspaceId, path) {
|
|
76
|
+
const page = await this.listFilesPage(workspaceId, path, null);
|
|
77
|
+
const match = page.items.find((f) => f.path === path);
|
|
78
|
+
if (!match)
|
|
79
|
+
throw new Error(`File not found: ${path}`);
|
|
80
|
+
return this.readFile(workspaceId, match.id);
|
|
81
|
+
}
|
|
71
82
|
async listFilesPage(workspaceId, prefix, cursor) {
|
|
72
83
|
const params = new URLSearchParams();
|
|
73
84
|
if (prefix)
|
|
@@ -195,6 +206,19 @@ export class ApiClient {
|
|
|
195
206
|
async logRun(body) {
|
|
196
207
|
return this.request('POST', '/runs', body);
|
|
197
208
|
}
|
|
209
|
+
async listRuns(query) {
|
|
210
|
+
const params = new URLSearchParams({ project: query.project });
|
|
211
|
+
if (query.platform)
|
|
212
|
+
params.set('platform', query.platform);
|
|
213
|
+
if (query.limit)
|
|
214
|
+
params.set('limit', String(query.limit));
|
|
215
|
+
if (query.cursor)
|
|
216
|
+
params.set('cursor', query.cursor);
|
|
217
|
+
return this.requestPage('GET', `/runs?${params.toString()}`);
|
|
218
|
+
}
|
|
219
|
+
async getRun(runId) {
|
|
220
|
+
return this.request('GET', `/runs/${encodeURIComponent(runId)}`);
|
|
221
|
+
}
|
|
198
222
|
// --- Sandbox (Wave 5 Stream D) ---
|
|
199
223
|
async sandboxClone(workspaceId, body) {
|
|
200
224
|
return this.request('POST', `/workspaces/${workspaceId}/sandbox/clone`, body);
|
package/dist/index.js
CHANGED
|
@@ -1,59 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { ApiClient } from './
|
|
5
|
-
import { driveWriteSchema, driveWrite } from './tools/drive-write.js';
|
|
6
|
-
import { driveReadSchema, driveRead } from './tools/drive-read.js';
|
|
7
|
-
import { driveListSchema, driveList } from './tools/drive-list.js';
|
|
8
|
-
import { driveShareSchema, driveShare } from './tools/drive-share.js';
|
|
9
|
-
import { driveCommentSchema, driveComment } from './tools/drive-comment.js';
|
|
10
|
-
import { driveFeedbackSchema, driveFeedback } from './tools/drive-feedback.js';
|
|
11
|
-
import { driveActivitySchema, driveActivity } from './tools/drive-activity.js';
|
|
12
|
-
import { driveInviteSchema, driveInvite } from './tools/drive-invite.js';
|
|
13
|
-
import { driveRolesSchema, driveRoles } from './tools/drive-roles.js';
|
|
14
|
-
import { driveOrgSchema, driveOrg } from './tools/drive-org.js';
|
|
15
|
-
import { driveDeleteSchema, driveDelete } from './tools/drive-delete.js';
|
|
16
|
-
import { driveVersionsSchema, driveVersions } from './tools/drive-versions.js';
|
|
17
|
-
import { driveSearchSchema, driveSearch } from './tools/drive-search.js';
|
|
18
|
-
import { driveRecallSchema, driveRecall } from './tools/drive-recall.js';
|
|
19
|
-
import { driveLogRunSchema, driveLogRun } from './tools/drive-log-run.js';
|
|
20
|
-
import { driveContextSchema, driveContext } from './tools/drive-context.js';
|
|
21
|
-
import { driveSandboxCloneSchema, driveSandboxClone } from './tools/drive-sandbox-clone.js';
|
|
22
|
-
import { driveSandboxExecSchema, driveSandboxExec } from './tools/drive-sandbox-exec.js';
|
|
23
|
-
import { driveSandboxReadSchema, driveSandboxRead } from './tools/drive-sandbox-read.js';
|
|
24
|
-
import { driveSandboxCommitSchema, driveSandboxCommit } from './tools/drive-sandbox-commit.js';
|
|
4
|
+
import { ApiClient, TOOLS, SERVER_INSTRUCTIONS } from './registry.js';
|
|
25
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { projectKeyFor, scopeProjectKey } from './project.js';
|
|
26
10
|
const require = createRequire(import.meta.url);
|
|
27
11
|
const pkg = require('../package.json');
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
12
|
+
function readCliConfig() {
|
|
13
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
14
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), '.config');
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(join(base, 'agentdrive', 'config.json'), 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const fileConfig = readCliConfig();
|
|
23
|
+
const cliConfig = process.env.AGENTDRIVE_API_KEY ? {} : fileConfig;
|
|
24
|
+
const BASE_URL = process.env.AGENTDRIVE_API_URL || cliConfig.api_url || 'https://app.agentdrive.sh';
|
|
25
|
+
const API_KEY = process.env.AGENTDRIVE_API_KEY || cliConfig.api_key;
|
|
26
|
+
const AGENT_PLATFORM = process.env.AGENTDRIVE_AGENT_PLATFORM || 'unknown';
|
|
31
27
|
const AGENT_ROLE = process.env.AGENTDRIVE_AGENT_ROLE || '';
|
|
32
|
-
// Cross-host preamble shipped via MCP `instructions` so every MCP-aware
|
|
33
|
-
// client (Claude Code, Codex, Cursor, Zed, Cline, Continue, OpenClaw) gets
|
|
34
|
-
// the same usage guidance at session start without needing a per-host
|
|
35
|
-
// markdown file. Hosts that ALSO read a markdown rules file
|
|
36
|
-
// (~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, ~/.cursor/rules/agentdrive.mdc)
|
|
37
|
-
// continue to get the more verbose adrive init preamble via auto-context.ts.
|
|
38
|
-
const SERVER_INSTRUCTIONS = `AgentDrive — cross-agent artifact workspace with provenance for multi-agent workflows.
|
|
39
|
-
|
|
40
|
-
Reach for these tools without waiting to be asked:
|
|
41
|
-
|
|
42
|
-
- **Session start:** call \`drive_context\` with a short summary of the
|
|
43
|
-
current task. Returns the most relevant artifacts and last_run
|
|
44
|
-
summaries from prior agents' work in the same workspace, each with
|
|
45
|
-
an \`attribution\` block explaining *why* it was surfaced.
|
|
46
|
-
- **Cross-agent recall:** call \`drive_search\` (semantic) when the
|
|
47
|
-
user asks "what did Codex / Cursor / the last session do?"
|
|
48
|
-
- **Saving work:** call \`drive_write\` for any artifact worth keeping
|
|
49
|
-
so the next agent in the chain can read it back.
|
|
50
|
-
- **Provenance:** call \`drive_log_run\` at the end of meaningful
|
|
51
|
-
work so the cross-agent timeline stays accurate.
|
|
52
|
-
|
|
53
|
-
Privacy: if the user has Private Workspace on, written content is
|
|
54
|
-
encrypted client-side; the AgentDrive server only stores ciphertext.`;
|
|
55
28
|
if (!API_KEY) {
|
|
56
|
-
console.error('AGENTDRIVE_API_KEY
|
|
29
|
+
console.error('No AgentDrive API key found. Set AGENTDRIVE_API_KEY or run `adrive auth login`.');
|
|
57
30
|
process.exit(1);
|
|
58
31
|
}
|
|
59
32
|
const client = new ApiClient({
|
|
@@ -61,6 +34,7 @@ const client = new ApiClient({
|
|
|
61
34
|
apiKey: API_KEY,
|
|
62
35
|
agentPlatform: AGENT_PLATFORM,
|
|
63
36
|
agentRole: AGENT_ROLE,
|
|
37
|
+
project: scopeProjectKey(projectKeyFor(process.cwd()), !!fileConfig.preferences?.private_workspace),
|
|
64
38
|
});
|
|
65
39
|
const server = new McpServer({
|
|
66
40
|
name: 'agentdrive',
|
|
@@ -69,32 +43,9 @@ const server = new McpServer({
|
|
|
69
43
|
}, {
|
|
70
44
|
instructions: SERVER_INSTRUCTIONS,
|
|
71
45
|
});
|
|
72
|
-
|
|
73
|
-
server.tool(
|
|
74
|
-
|
|
75
|
-
server.tool('drive_share', 'Generate a share link for a workspace or specific file. Optionally email-gate the link so only a specific person can access it. Humans open this link to view, comment, or edit files.', driveShareSchema, (args) => driveShare(client, args));
|
|
76
|
-
server.tool('drive_comment', 'Add a comment to a file.', driveCommentSchema, (args) => driveComment(client, args));
|
|
77
|
-
server.tool('drive_feedback', 'Read human comments on a file. Use this to check if humans have left feedback.', driveFeedbackSchema, (args) => driveFeedback(client, args));
|
|
78
|
-
server.tool('drive_activity', 'Get recent activity in a workspace (file writes, comments, shares).', driveActivitySchema, (args) => driveActivity(client, args));
|
|
79
|
-
server.tool('drive_invite', 'Invite a human to your organization by email. They will receive a verification code to join.', driveInviteSchema, (args) => driveInvite(client, args));
|
|
80
|
-
server.tool('drive_roles', 'Manage per-workspace role delegation. Assign, list, or remove access for specific emails (view/comment/edit).', driveRolesSchema, (args) => driveRoles(client, args));
|
|
81
|
-
server.tool('drive_org', 'View your organization details: agents, members, plan, and usage.', driveOrgSchema, (args) => driveOrg(client, args));
|
|
82
|
-
server.tool('drive_delete', 'Delete a file from a workspace.', driveDeleteSchema, (args) => driveDelete(client, args));
|
|
83
|
-
server.tool('drive_versions', 'View version history for a file in a workspace.', driveVersionsSchema, (args) => driveVersions(client, args));
|
|
84
|
-
server.tool('drive_search', "Semantic search across your org's artifacts in AgentDrive. Returns the top-N most relevant files. Use this BEFORE writing similar artifacts to avoid duplication and surface prior context.", driveSearchSchema, (args) => driveSearch(client, args));
|
|
85
|
-
server.tool('drive_recall', 'Recall persistent facts and preferences your agents have written. In v1 this returns semantic search results across artifacts; v1.5 will narrow to auto-extracted facts (preferences, decisions, key entities).', driveRecallSchema, (args) => driveRecall(client, args));
|
|
86
|
-
server.tool('drive_log_run', "Log an agent run to AgentDrive's provenance store. Captures task, status, referenced/produced artifacts, and cross-platform identity (which agent on which tool). Used for retrospective context retrieval and team-wide audit.", driveLogRunSchema, (args) => driveLogRun(client, args));
|
|
87
|
-
server.tool('drive_context', 'Auto-pull the most relevant artifacts from prior agent sessions for the task you are about to do. Combines semantic relevance with recency. Call this at the START of any non-trivial task to ground the agent in prior context — replaces manual copy-paste of artifacts between sessions.', driveContextSchema, (args) => driveContext(client, args));
|
|
88
|
-
// The four drive_sandbox_* tools are EXPERIMENTAL. They require the
|
|
89
|
-
// Cloudflare Containers plan on the AgentDrive worker; without that
|
|
90
|
-
// binding the API returns 503 and these tools surface a clean
|
|
91
|
-
// "not configured" message. Marked experimental in the description
|
|
92
|
-
// so MCP-aware hosts that surface tool metadata can communicate the
|
|
93
|
-
// status; not part of the v1 cross-agent value prop.
|
|
94
|
-
server.tool('drive_sandbox_clone', '[EXPERIMENTAL] Clone a git repo into a per-workspace Linux sandbox. Requires Cloudflare Containers on the AgentDrive worker; gracefully degrades to a 503 with "sandbox not configured" otherwise.', driveSandboxCloneSchema, (args) => driveSandboxClone(client, args));
|
|
95
|
-
server.tool('drive_sandbox_exec', '[EXPERIMENTAL] Run a shell command inside the per-workspace Linux sandbox. Captures stdout/stderr (64 KB cap), exit code, duration. Requires Cloudflare Containers; degrades to 503 otherwise.', driveSandboxExecSchema, (args) => driveSandboxExec(client, args));
|
|
96
|
-
server.tool('drive_sandbox_read', '[EXPERIMENTAL] Read a file from the per-workspace sandbox filesystem (build artifacts, test output, logs). Requires Cloudflare Containers; degrades to 503 otherwise.', driveSandboxReadSchema, (args) => driveSandboxRead(client, args));
|
|
97
|
-
server.tool('drive_sandbox_commit', '[EXPERIMENTAL] Stage + commit the current sandbox worktree. Surfaces as an AgentDrive artifact at commits/<sha>.json. Requires Cloudflare Containers; degrades to 503 otherwise.', driveSandboxCommitSchema, (args) => driveSandboxCommit(client, args));
|
|
46
|
+
for (const tool of TOOLS) {
|
|
47
|
+
server.tool(tool.name, tool.description, tool.schema, (args) => tool.handler(client, args));
|
|
48
|
+
}
|
|
98
49
|
async function main() {
|
|
99
50
|
const transport = new StdioServerTransport();
|
|
100
51
|
await server.connect(transport);
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
function git(cwd, args) {
|
|
5
|
+
try {
|
|
6
|
+
const out = execFileSync('git', ['-C', cwd, ...args], {
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
timeout: 1000,
|
|
9
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
10
|
+
}).trim();
|
|
11
|
+
return out || null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function sha8(value) {
|
|
18
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 8);
|
|
19
|
+
}
|
|
20
|
+
function slug(value) {
|
|
21
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]/g, '-');
|
|
22
|
+
}
|
|
23
|
+
export function normalizeRemote(url) {
|
|
24
|
+
const trimmed = url.trim();
|
|
25
|
+
let host;
|
|
26
|
+
let path;
|
|
27
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/\/)(.+)$/.exec(trimmed);
|
|
28
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(trimmed);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
host = parsed.hostname;
|
|
37
|
+
path = parsed.pathname;
|
|
38
|
+
}
|
|
39
|
+
else if (scp) {
|
|
40
|
+
host = scp[1];
|
|
41
|
+
path = scp[2];
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const segments = path
|
|
47
|
+
.replace(/\.git\/?$/, '')
|
|
48
|
+
.split('/')
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map(slug);
|
|
51
|
+
if (!host || segments.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
return [slug(host), ...segments].join('__');
|
|
54
|
+
}
|
|
55
|
+
export function projectKeyFor(cwd) {
|
|
56
|
+
const remote = git(cwd, ['config', '--get', 'remote.origin.url']);
|
|
57
|
+
const normalized = remote ? normalizeRemote(remote) : null;
|
|
58
|
+
if (normalized)
|
|
59
|
+
return normalized.slice(0, 200);
|
|
60
|
+
const toplevel = git(cwd, ['rev-parse', '--show-toplevel']);
|
|
61
|
+
if (toplevel)
|
|
62
|
+
return `${slug(basename(toplevel)).slice(0, 150)}-${sha8(toplevel)}`;
|
|
63
|
+
return `local__${slug(basename(cwd)).slice(0, 150)}-${sha8(cwd)}`;
|
|
64
|
+
}
|
|
65
|
+
// A private workspace must not leak repo names through storage paths or the
|
|
66
|
+
// project index, so the key is replaced by a stable hash of itself.
|
|
67
|
+
export function scopeProjectKey(key, privateWorkspace) {
|
|
68
|
+
return privateWorkspace ? `p-${createHash('sha256').update(key).digest('hex').slice(0, 16)}` : key;
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { ApiClient } from './api-client.js';
|
|
3
|
+
export interface ToolResult {
|
|
4
|
+
[x: string]: unknown;
|
|
5
|
+
content: Array<{
|
|
6
|
+
type: 'text';
|
|
7
|
+
text: string;
|
|
8
|
+
}>;
|
|
9
|
+
isError?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ToolDef {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
schema: z.ZodRawShape;
|
|
15
|
+
handler: (client: ApiClient, args: any) => Promise<ToolResult>;
|
|
16
|
+
}
|
|
17
|
+
export declare const SERVER_INSTRUCTIONS = "AgentDrive \u2014 cross-agent artifact workspace with provenance for multi-agent workflows.\n\nReach for these tools without waiting to be asked:\n\n- **Session start:** call `drive_context` with a short summary of the\n current task. Returns the most relevant artifacts and last_run\n summaries from prior agents' work in the same workspace, each with\n an `attribution` block explaining *why* it was surfaced.\n- **Continuing earlier work:** if the user is picking up where a prior\n session left off (or a SessionStart note mentions prior sessions),\n call `drive_sessions` to list this project's recent sessions across\n harnesses, then `drive_resume` to load one's handoff brief.\n- **Cross-agent recall:** call `drive_search` (semantic) when the\n user asks \"what did Codex / Cursor / the last session do?\"\n- **Saving work:** call `drive_write` for any artifact worth keeping\n so the next agent in the chain can read it back.\n- **Provenance:** call `drive_log_run` at the end of meaningful\n work so the cross-agent timeline stays accurate.\n\nPrivacy: if the user has Private Workspace on, written content is\nencrypted client-side; the AgentDrive server only stores ciphertext.";
|
|
18
|
+
export declare const TOOLS: ToolDef[];
|
|
19
|
+
export { ApiClient } from './api-client.js';
|
|
20
|
+
export type { ApiClientConfig } from './api-client.js';
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { driveWriteSchema, driveWrite } from './tools/drive-write.js';
|
|
2
|
+
import { driveReadSchema, driveRead } from './tools/drive-read.js';
|
|
3
|
+
import { driveListSchema, driveList } from './tools/drive-list.js';
|
|
4
|
+
import { driveShareSchema, driveShare } from './tools/drive-share.js';
|
|
5
|
+
import { driveCommentSchema, driveComment } from './tools/drive-comment.js';
|
|
6
|
+
import { driveFeedbackSchema, driveFeedback } from './tools/drive-feedback.js';
|
|
7
|
+
import { driveActivitySchema, driveActivity } from './tools/drive-activity.js';
|
|
8
|
+
import { driveInviteSchema, driveInvite } from './tools/drive-invite.js';
|
|
9
|
+
import { driveRolesSchema, driveRoles } from './tools/drive-roles.js';
|
|
10
|
+
import { driveOrgSchema, driveOrg } from './tools/drive-org.js';
|
|
11
|
+
import { driveDeleteSchema, driveDelete } from './tools/drive-delete.js';
|
|
12
|
+
import { driveVersionsSchema, driveVersions } from './tools/drive-versions.js';
|
|
13
|
+
import { driveSearchSchema, driveSearch } from './tools/drive-search.js';
|
|
14
|
+
import { driveRecallSchema, driveRecall } from './tools/drive-recall.js';
|
|
15
|
+
import { driveLogRunSchema, driveLogRun } from './tools/drive-log-run.js';
|
|
16
|
+
import { driveContextSchema, driveContext } from './tools/drive-context.js';
|
|
17
|
+
import { driveSandboxCloneSchema, driveSandboxClone } from './tools/drive-sandbox-clone.js';
|
|
18
|
+
import { driveSandboxExecSchema, driveSandboxExec } from './tools/drive-sandbox-exec.js';
|
|
19
|
+
import { driveSandboxReadSchema, driveSandboxRead } from './tools/drive-sandbox-read.js';
|
|
20
|
+
import { driveSandboxCommitSchema, driveSandboxCommit } from './tools/drive-sandbox-commit.js';
|
|
21
|
+
import { driveSessionsSchema, driveSessions } from './tools/drive-sessions.js';
|
|
22
|
+
import { driveResumeSchema, driveResume } from './tools/drive-resume.js';
|
|
23
|
+
// Shared by every MCP-aware host. Stdio and the hosted Streamable-HTTP server
|
|
24
|
+
// surface the same guidance via the MCP `instructions` field at session start.
|
|
25
|
+
export const SERVER_INSTRUCTIONS = `AgentDrive — cross-agent artifact workspace with provenance for multi-agent workflows.
|
|
26
|
+
|
|
27
|
+
Reach for these tools without waiting to be asked:
|
|
28
|
+
|
|
29
|
+
- **Session start:** call \`drive_context\` with a short summary of the
|
|
30
|
+
current task. Returns the most relevant artifacts and last_run
|
|
31
|
+
summaries from prior agents' work in the same workspace, each with
|
|
32
|
+
an \`attribution\` block explaining *why* it was surfaced.
|
|
33
|
+
- **Continuing earlier work:** if the user is picking up where a prior
|
|
34
|
+
session left off (or a SessionStart note mentions prior sessions),
|
|
35
|
+
call \`drive_sessions\` to list this project's recent sessions across
|
|
36
|
+
harnesses, then \`drive_resume\` to load one's handoff brief.
|
|
37
|
+
- **Cross-agent recall:** call \`drive_search\` (semantic) when the
|
|
38
|
+
user asks "what did Codex / Cursor / the last session do?"
|
|
39
|
+
- **Saving work:** call \`drive_write\` for any artifact worth keeping
|
|
40
|
+
so the next agent in the chain can read it back.
|
|
41
|
+
- **Provenance:** call \`drive_log_run\` at the end of meaningful
|
|
42
|
+
work so the cross-agent timeline stays accurate.
|
|
43
|
+
|
|
44
|
+
Privacy: if the user has Private Workspace on, written content is
|
|
45
|
+
encrypted client-side; the AgentDrive server only stores ciphertext.`;
|
|
46
|
+
// Single source of truth for the 22 tools. Consumed by the stdio server
|
|
47
|
+
// (packages/mcp-server/src/index.ts) and the hosted Streamable-HTTP endpoint
|
|
48
|
+
// (apps/worker/src/routes/mcp.ts).
|
|
49
|
+
export const TOOLS = [
|
|
50
|
+
{ name: 'drive_write', description: 'Write a markdown file to a workspace. Creates or updates the file with automatic versioning.', schema: driveWriteSchema, handler: driveWrite },
|
|
51
|
+
{ name: 'drive_read', description: "Read a file's content from a workspace.", schema: driveReadSchema, handler: driveRead },
|
|
52
|
+
{ name: 'drive_list', description: 'List files in a workspace, optionally filtered by path prefix.', schema: driveListSchema, handler: driveList },
|
|
53
|
+
{ name: 'drive_share', description: 'Generate a share link for a workspace or specific file. Optionally email-gate the link so only a specific person can access it. Humans open this link to view, comment, or edit files.', schema: driveShareSchema, handler: driveShare },
|
|
54
|
+
{ name: 'drive_comment', description: 'Add a comment to a file.', schema: driveCommentSchema, handler: driveComment },
|
|
55
|
+
{ name: 'drive_feedback', description: 'Read human comments on a file. Use this to check if humans have left feedback.', schema: driveFeedbackSchema, handler: driveFeedback },
|
|
56
|
+
{ name: 'drive_activity', description: 'Get recent activity in a workspace (file writes, comments, shares).', schema: driveActivitySchema, handler: driveActivity },
|
|
57
|
+
{ name: 'drive_invite', description: 'Invite a human to your organization by email. They will receive a verification code to join.', schema: driveInviteSchema, handler: driveInvite },
|
|
58
|
+
{ name: 'drive_roles', description: 'Manage per-workspace role delegation. Assign, list, or remove access for specific emails (view/comment/edit).', schema: driveRolesSchema, handler: driveRoles },
|
|
59
|
+
{ name: 'drive_org', description: 'View your organization details: agents, members, plan, and usage.', schema: driveOrgSchema, handler: driveOrg },
|
|
60
|
+
{ name: 'drive_delete', description: 'Delete a file from a workspace.', schema: driveDeleteSchema, handler: driveDelete },
|
|
61
|
+
{ name: 'drive_versions', description: 'View version history for a file in a workspace.', schema: driveVersionsSchema, handler: driveVersions },
|
|
62
|
+
{ name: 'drive_search', description: "Semantic search across your org's artifacts in AgentDrive. Returns the top-N most relevant files. Use this BEFORE writing similar artifacts to avoid duplication and surface prior context.", schema: driveSearchSchema, handler: driveSearch },
|
|
63
|
+
{ name: 'drive_recall', description: 'Recall persistent facts and preferences your agents have written. In v1 this returns semantic search results across artifacts; v1.5 will narrow to auto-extracted facts (preferences, decisions, key entities).', schema: driveRecallSchema, handler: driveRecall },
|
|
64
|
+
{ name: 'drive_log_run', description: "Log an agent run to AgentDrive's provenance store. Captures task, status, referenced/produced artifacts, and cross-platform identity (which agent on which tool). Used for retrospective context retrieval and team-wide audit.", schema: driveLogRunSchema, handler: driveLogRun },
|
|
65
|
+
{ name: 'drive_context', description: 'Auto-pull the most relevant artifacts from prior agent sessions for the task you are about to do. Combines semantic relevance with recency. Call this at the START of any non-trivial task to ground the agent in prior context — replaces manual copy-paste of artifacts between sessions.', schema: driveContextSchema, handler: driveContext },
|
|
66
|
+
{ name: 'drive_sessions', description: 'List recent agent sessions for this project across harnesses (Claude Code, Codex, OpenCode, Gemini CLI, Cursor), newest first. Use to find prior work to continue.', schema: driveSessionsSchema, handler: driveSessions },
|
|
67
|
+
{ name: 'drive_resume', description: "Load a compact handoff brief (goal, files touched, recent turns) from a prior session, by default the latest one from a different harness, so you can continue that work.", schema: driveResumeSchema, handler: driveResume },
|
|
68
|
+
{ name: 'drive_sandbox_clone', description: '[EXPERIMENTAL] Clone a git repo into a per-workspace Linux sandbox. Requires Cloudflare Containers on the AgentDrive worker; gracefully degrades to a 503 with "sandbox not configured" otherwise.', schema: driveSandboxCloneSchema, handler: driveSandboxClone },
|
|
69
|
+
{ name: 'drive_sandbox_exec', description: '[EXPERIMENTAL] Run a shell command inside the per-workspace Linux sandbox. Captures stdout/stderr (64 KB cap), exit code, duration. Requires Cloudflare Containers; degrades to 503 otherwise.', schema: driveSandboxExecSchema, handler: driveSandboxExec },
|
|
70
|
+
{ name: 'drive_sandbox_read', description: '[EXPERIMENTAL] Read a file from the per-workspace sandbox filesystem (build artifacts, test output, logs). Requires Cloudflare Containers; degrades to 503 otherwise.', schema: driveSandboxReadSchema, handler: driveSandboxRead },
|
|
71
|
+
{ name: 'drive_sandbox_commit', description: '[EXPERIMENTAL] Stage + commit the current sandbox worktree. Surfaces as an AgentDrive artifact at commits/<sha>.json. Requires Cloudflare Containers; degrades to 503 otherwise.', schema: driveSandboxCommitSchema, handler: driveSandboxCommit },
|
|
72
|
+
];
|
|
73
|
+
export { ApiClient } from './api-client.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type SessionHarness = 'claude-code' | 'codex' | 'opencode' | 'gemini-cli' | 'cursor';
|
|
2
|
+
export interface SessionTool {
|
|
3
|
+
name: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
summary?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SessionTurn {
|
|
8
|
+
role: string;
|
|
9
|
+
text: string;
|
|
10
|
+
ts?: string;
|
|
11
|
+
tools?: SessionTool[];
|
|
12
|
+
}
|
|
13
|
+
export interface SessionDoc {
|
|
14
|
+
harness: SessionHarness;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
project: string;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
turns: SessionTurn[];
|
|
21
|
+
filesTouched: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function isEncrypted(content: string): boolean;
|
|
24
|
+
export declare function sessionRunId(harness: SessionHarness, sessionId: string): string;
|
|
25
|
+
export declare function sessionPath(project: string, harness: SessionHarness, sessionId: string): string;
|
|
26
|
+
export declare function sessionActivityAt(run: {
|
|
27
|
+
created_at: number;
|
|
28
|
+
updated_at?: number;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}): number;
|
|
31
|
+
export declare function renderSession(doc: SessionDoc): string;
|
|
32
|
+
export declare function buildHandoff(markdown: string, maxChars?: number): string;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Must stay in sync with ENCRYPTED_PREFIX in packages/cli/src/lib/crypto.ts.
|
|
2
|
+
const ENCRYPTED_PREFIX = 'agentdrive-encrypted:v0:';
|
|
3
|
+
const DEFAULT_HANDOFF_CHARS = 12000;
|
|
4
|
+
const GOAL_CHARS = 1500;
|
|
5
|
+
const MAX_HANDOFF_FILES = 50;
|
|
6
|
+
const TURN_HEADER = /^## ([a-z0-9_-]+)(?: {2}_\((.*)\)_)?$/;
|
|
7
|
+
const TOOL_LINE = '> tool: ';
|
|
8
|
+
// Turn text lines that could be read as a turn header or tool line get a
|
|
9
|
+
// leading backslash so the parser never splits inside a turn body.
|
|
10
|
+
const COLLIDING_LINE = /^\\*(## |> tool: )/;
|
|
11
|
+
function oneLine(value) {
|
|
12
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
13
|
+
}
|
|
14
|
+
function sanitizeId(value) {
|
|
15
|
+
return value.replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 128);
|
|
16
|
+
}
|
|
17
|
+
function roleSlug(role) {
|
|
18
|
+
return role.toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'system';
|
|
19
|
+
}
|
|
20
|
+
export function isEncrypted(content) {
|
|
21
|
+
return content.startsWith(ENCRYPTED_PREFIX);
|
|
22
|
+
}
|
|
23
|
+
export function sessionRunId(harness, sessionId) {
|
|
24
|
+
return `${harness}:${sanitizeId(sessionId)}`;
|
|
25
|
+
}
|
|
26
|
+
export function sessionPath(project, harness, sessionId) {
|
|
27
|
+
return `sessions/${project}/${harness}/${sanitizeId(sessionId)}.md`;
|
|
28
|
+
}
|
|
29
|
+
// Project listings are ordered by last activity, so ages must be shown from it too.
|
|
30
|
+
export function sessionActivityAt(run) {
|
|
31
|
+
const at = run.metadata?.last_activity_at;
|
|
32
|
+
return typeof at === 'number' ? at : run.updated_at ?? run.created_at;
|
|
33
|
+
}
|
|
34
|
+
export function renderSession(doc) {
|
|
35
|
+
const out = [`# Session — ${doc.harness} / ${oneLine(doc.sessionId)}`, ''];
|
|
36
|
+
out.push(`Project: ${oneLine(doc.project)}`);
|
|
37
|
+
if (doc.cwd)
|
|
38
|
+
out.push(`Cwd: ${oneLine(doc.cwd)}`);
|
|
39
|
+
if (doc.title)
|
|
40
|
+
out.push(`Title: ${oneLine(doc.title)}`);
|
|
41
|
+
out.push(`Updated: ${oneLine(doc.updatedAt)}`);
|
|
42
|
+
out.push(`Turns: ${doc.turns.length}`);
|
|
43
|
+
out.push('', 'Files touched:');
|
|
44
|
+
for (const f of doc.filesTouched)
|
|
45
|
+
out.push(`- ${oneLine(f)}`);
|
|
46
|
+
for (const turn of doc.turns) {
|
|
47
|
+
out.push('', turn.ts ? `## ${roleSlug(turn.role)} _(${oneLine(turn.ts)})_` : `## ${roleSlug(turn.role)}`, '');
|
|
48
|
+
const text = turn.text.replace(/\r\n?/g, '\n').trim();
|
|
49
|
+
if (text) {
|
|
50
|
+
for (const line of text.split('\n'))
|
|
51
|
+
out.push(COLLIDING_LINE.test(line) ? `\\${line}` : line);
|
|
52
|
+
}
|
|
53
|
+
if (turn.tools && turn.tools.length > 0) {
|
|
54
|
+
if (text)
|
|
55
|
+
out.push('');
|
|
56
|
+
for (const tool of turn.tools) {
|
|
57
|
+
const detail = tool.path ?? tool.summary;
|
|
58
|
+
out.push(`${TOOL_LINE}${oneLine(tool.name)}${detail ? ` ${oneLine(detail)}` : ''}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return `${out.join('\n')}\n`;
|
|
63
|
+
}
|
|
64
|
+
function parseSession(markdown) {
|
|
65
|
+
const parsed = { meta: [], files: [], turns: [] };
|
|
66
|
+
let current = null;
|
|
67
|
+
let inFiles = false;
|
|
68
|
+
for (const line of markdown.replace(/\r\n?/g, '\n').split('\n')) {
|
|
69
|
+
const header = TURN_HEADER.exec(line);
|
|
70
|
+
if (header) {
|
|
71
|
+
current = { role: header[1], lines: [line] };
|
|
72
|
+
parsed.turns.push(current);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (current) {
|
|
76
|
+
current.lines.push(line);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const title = /^# Session — (\S+) \/ (.+)$/.exec(line);
|
|
80
|
+
if (title) {
|
|
81
|
+
parsed.harness = title[1];
|
|
82
|
+
parsed.sessionId = title[2];
|
|
83
|
+
}
|
|
84
|
+
else if (line === 'Files touched:') {
|
|
85
|
+
inFiles = true;
|
|
86
|
+
}
|
|
87
|
+
else if (inFiles && line.startsWith('- ')) {
|
|
88
|
+
parsed.files.push(line.slice(2));
|
|
89
|
+
}
|
|
90
|
+
else if (/^(Project|Cwd|Title|Updated|Turns): /.test(line)) {
|
|
91
|
+
parsed.meta.push(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
function turnBody(turn) {
|
|
97
|
+
return turn.lines
|
|
98
|
+
.slice(1)
|
|
99
|
+
.filter((l) => !l.startsWith(TOOL_LINE))
|
|
100
|
+
.join('\n')
|
|
101
|
+
.trim();
|
|
102
|
+
}
|
|
103
|
+
function turnBlock(turn) {
|
|
104
|
+
const [header, ...rest] = turn.lines;
|
|
105
|
+
return [`#${header}`, ...rest].join('\n').trim();
|
|
106
|
+
}
|
|
107
|
+
function truncate(s, n) {
|
|
108
|
+
return s.length <= n ? s : `${s.slice(0, n - 3)}...`;
|
|
109
|
+
}
|
|
110
|
+
export function buildHandoff(markdown, maxChars = DEFAULT_HANDOFF_CHARS) {
|
|
111
|
+
const session = parseSession(markdown);
|
|
112
|
+
const from = session.harness ?? 'prior';
|
|
113
|
+
const intro = `Handoff from a ${from} session${session.sessionId ? ` (${session.sessionId})` : ''}. Continue this work; verify file state on disk before assuming it.`;
|
|
114
|
+
const sections = [intro];
|
|
115
|
+
if (session.meta.length > 0)
|
|
116
|
+
sections.push(session.meta.join('\n'));
|
|
117
|
+
const firstUser = session.turns.find((t) => t.role === 'user');
|
|
118
|
+
const goal = firstUser ? turnBody(firstUser) : '';
|
|
119
|
+
if (goal)
|
|
120
|
+
sections.push(`## Goal\n\n${truncate(goal, GOAL_CHARS)}`);
|
|
121
|
+
if (session.files.length > 0) {
|
|
122
|
+
const shown = session.files.slice(0, MAX_HANDOFF_FILES).map((f) => `- ${f}`);
|
|
123
|
+
const more = session.files.length - shown.length;
|
|
124
|
+
if (more > 0)
|
|
125
|
+
shown.push(`- (+${more} more)`);
|
|
126
|
+
sections.push(`## Files touched\n\n${shown.join('\n')}`);
|
|
127
|
+
}
|
|
128
|
+
if (session.turns.length === 0) {
|
|
129
|
+
const fixed = `${sections.join('\n\n')}\n\n## Transcript (tail)\n\n`;
|
|
130
|
+
const body = markdown.trim();
|
|
131
|
+
return (fixed + body.slice(Math.max(0, body.length - (maxChars - fixed.length)))).slice(0, maxChars);
|
|
132
|
+
}
|
|
133
|
+
const fixed = `${sections.join('\n\n')}\n\n## Recent turns\n\n`;
|
|
134
|
+
const markerReserve = 64;
|
|
135
|
+
let remaining = maxChars - fixed.length - markerReserve;
|
|
136
|
+
const picked = [];
|
|
137
|
+
for (let i = session.turns.length - 1; i >= 0 && remaining > 0; i--) {
|
|
138
|
+
const block = turnBlock(session.turns[i]);
|
|
139
|
+
if (block.length + 2 <= remaining) {
|
|
140
|
+
picked.unshift(block);
|
|
141
|
+
remaining -= block.length + 2;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (picked.length === 0) {
|
|
145
|
+
const [header, ...rest] = block.split('\n');
|
|
146
|
+
const prefix = `${header}\n\n[... turn truncated ...]\n`;
|
|
147
|
+
const keep = remaining - prefix.length;
|
|
148
|
+
if (keep > 0) {
|
|
149
|
+
const body = rest.join('\n');
|
|
150
|
+
picked.unshift(prefix + body.slice(body.length - keep));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const omitted = session.turns.length - picked.length;
|
|
156
|
+
const marker = omitted > 0 ? `[... ${omitted} earlier turns omitted ...]\n\n` : '';
|
|
157
|
+
return `${fixed}${marker}${picked.join('\n\n')}`.slice(0, maxChars);
|
|
158
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ApiClient } from '../api-client.js';
|
|
3
|
+
export declare const driveResumeSchema: {
|
|
4
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
5
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
6
|
+
max_chars: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
};
|
|
8
|
+
export declare function driveResume(client: ApiClient, args: {
|
|
9
|
+
session_id?: string;
|
|
10
|
+
platform?: string;
|
|
11
|
+
max_chars?: number;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
content: {
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
}[];
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildHandoff, isEncrypted } from '../session.js';
|
|
3
|
+
import { noProjectResult } from './drive-sessions.js';
|
|
4
|
+
export const driveResumeSchema = {
|
|
5
|
+
session_id: z.string().optional().describe('Session run id from drive_sessions (e.g. codex:abc123); omit to pick the latest session from another harness'),
|
|
6
|
+
platform: z.string().optional().describe('When session_id is omitted, only consider sessions from this harness'),
|
|
7
|
+
max_chars: z.number().int().min(1000).max(50000).optional().describe('Handoff size budget in characters (default 12000)'),
|
|
8
|
+
};
|
|
9
|
+
function text(value) {
|
|
10
|
+
return { content: [{ type: 'text', text: value }] };
|
|
11
|
+
}
|
|
12
|
+
async function pickSession(client, project, platform) {
|
|
13
|
+
const { items } = await client.listRuns({ project, platform, limit: 20 });
|
|
14
|
+
return items.find((r) => r.agent_platform !== client.agentPlatform) ?? items[0];
|
|
15
|
+
}
|
|
16
|
+
export async function driveResume(client, args) {
|
|
17
|
+
let run;
|
|
18
|
+
if (args.session_id) {
|
|
19
|
+
run = await client.getRun(args.session_id);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
if (!client.project)
|
|
23
|
+
return noProjectResult();
|
|
24
|
+
run = await pickSession(client, client.project, args.platform);
|
|
25
|
+
if (!run)
|
|
26
|
+
return text(`No prior sessions for project ${client.project}.`);
|
|
27
|
+
}
|
|
28
|
+
const transcriptPath = run.metadata.transcript_path;
|
|
29
|
+
if (typeof transcriptPath !== 'string' || !run.workspace_id) {
|
|
30
|
+
return text(`Session ${run.run_id} has no uploaded transcript (missing metadata.transcript_path or workspace_id).`);
|
|
31
|
+
}
|
|
32
|
+
const file = await client.readFileByPath(run.workspace_id, transcriptPath);
|
|
33
|
+
if (isEncrypted(file.content)) {
|
|
34
|
+
return text(`This session is encrypted client-side; run \`adrive resume ${run.run_id}\` in a shell to view it.`);
|
|
35
|
+
}
|
|
36
|
+
return text(buildHandoff(file.content, args.max_chars));
|
|
37
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ApiClient } from '../api-client.js';
|
|
3
|
-
declare const AGENT_PLATFORMS: readonly ["claude-code", "codex", "cursor", "langgraph", "crewai", "autogen", "hermes", "openclaw", "unknown"];
|
|
3
|
+
declare const AGENT_PLATFORMS: readonly ["claude-code", "codex", "cursor", "langgraph", "crewai", "autogen", "hermes", "openclaw", "opencode", "gemini-cli", "unknown"];
|
|
4
4
|
export declare const driveSearchSchema: {
|
|
5
5
|
query: z.ZodString;
|
|
6
6
|
workspace_id: z.ZodOptional<z.ZodString>;
|
|
7
7
|
top_k: z.ZodOptional<z.ZodNumber>;
|
|
8
8
|
filter: z.ZodOptional<z.ZodObject<{
|
|
9
|
-
agent_platform: z.ZodOptional<z.ZodEnum<["claude-code", "codex", "cursor", "langgraph", "crewai", "autogen", "hermes", "openclaw", "unknown"]>>;
|
|
9
|
+
agent_platform: z.ZodOptional<z.ZodEnum<["claude-code", "codex", "cursor", "langgraph", "crewai", "autogen", "hermes", "openclaw", "opencode", "gemini-cli", "unknown"]>>;
|
|
10
10
|
agent_id: z.ZodOptional<z.ZodString>;
|
|
11
11
|
path_prefix: z.ZodOptional<z.ZodString>;
|
|
12
12
|
}, "strip", z.ZodTypeAny, {
|
|
13
|
-
agent_platform?: "claude-code" | "cursor" | "codex" | "langgraph" | "crewai" | "autogen" | "hermes" | "openclaw" | "unknown" | undefined;
|
|
13
|
+
agent_platform?: "claude-code" | "cursor" | "codex" | "langgraph" | "crewai" | "autogen" | "hermes" | "openclaw" | "opencode" | "gemini-cli" | "unknown" | undefined;
|
|
14
14
|
agent_id?: string | undefined;
|
|
15
15
|
path_prefix?: string | undefined;
|
|
16
16
|
}, {
|
|
17
|
-
agent_platform?: "claude-code" | "cursor" | "codex" | "langgraph" | "crewai" | "autogen" | "hermes" | "openclaw" | "unknown" | undefined;
|
|
17
|
+
agent_platform?: "claude-code" | "cursor" | "codex" | "langgraph" | "crewai" | "autogen" | "hermes" | "openclaw" | "opencode" | "gemini-cli" | "unknown" | undefined;
|
|
18
18
|
agent_id?: string | undefined;
|
|
19
19
|
path_prefix?: string | undefined;
|
|
20
20
|
}>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ApiClient } from '../api-client.js';
|
|
3
|
+
export declare const driveSessionsSchema: {
|
|
4
|
+
project: z.ZodOptional<z.ZodString>;
|
|
5
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
6
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
};
|
|
8
|
+
export declare function noProjectResult(): {
|
|
9
|
+
content: {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
export declare function driveSessions(client: ApiClient, args: {
|
|
15
|
+
project?: string;
|
|
16
|
+
platform?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
content: {
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}[];
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { relativeTime } from './drive-context.js';
|
|
3
|
+
import { sessionActivityAt } from '../session.js';
|
|
4
|
+
export const driveSessionsSchema = {
|
|
5
|
+
project: z.string().regex(/^[a-z0-9._-]{1,200}$/).optional().describe('Project key; defaults to the project of the current working directory'),
|
|
6
|
+
platform: z.string().optional().describe('Only sessions from this harness (claude-code, codex, opencode, gemini-cli, cursor)'),
|
|
7
|
+
limit: z.number().int().positive().max(50).optional().describe('Max sessions to return (default 10, max 50)'),
|
|
8
|
+
};
|
|
9
|
+
export function noProjectResult() {
|
|
10
|
+
return {
|
|
11
|
+
content: [{
|
|
12
|
+
type: 'text',
|
|
13
|
+
text: 'No project key is known for this session. Pass `project` (e.g. github.com__owner__repo).',
|
|
14
|
+
}],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function sessionTitle(run) {
|
|
18
|
+
const title = run.metadata.title;
|
|
19
|
+
return typeof title === 'string' && title ? title : run.task;
|
|
20
|
+
}
|
|
21
|
+
export async function driveSessions(client, args) {
|
|
22
|
+
const project = args.project ?? client.project;
|
|
23
|
+
if (!project)
|
|
24
|
+
return noProjectResult();
|
|
25
|
+
const { items } = await client.listRuns({ project, platform: args.platform, limit: args.limit ?? 10 });
|
|
26
|
+
if (items.length === 0) {
|
|
27
|
+
return { content: [{ type: 'text', text: `No prior sessions for project ${project}.` }] };
|
|
28
|
+
}
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const lines = items.map((r) => {
|
|
31
|
+
const age = relativeTime(now - sessionActivityAt(r));
|
|
32
|
+
const uploaded = typeof r.metadata.files_uploaded === 'number' ? r.metadata.files_uploaded : r.artifacts_produced.length;
|
|
33
|
+
return `${age} · ${r.agent_platform} · ${sessionTitle(r)} · id=${r.run_id} · files_uploaded=${uploaded}`;
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
content: [{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: `Sessions for ${project} (newest first):\n${lines.join('\n')}\n\nCall drive_resume with session_id to load one.`,
|
|
39
|
+
}],
|
|
40
|
+
};
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adrive/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for AgentDrive — 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",
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
"adrive-mcp": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "dist/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js",
|
|
13
|
+
"./registry": "./dist/registry.js",
|
|
14
|
+
"./session": "./dist/session.js",
|
|
15
|
+
"./project": "./dist/project.js"
|
|
16
|
+
},
|
|
11
17
|
"files": [
|
|
12
18
|
"dist",
|
|
13
19
|
"README.md"
|
|
@@ -15,8 +21,7 @@
|
|
|
15
21
|
"homepage": "https://agentdrive.sh",
|
|
16
22
|
"repository": {
|
|
17
23
|
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/Rakesh1002/agentdrive.git"
|
|
19
|
-
"directory": "packages/mcp-server"
|
|
24
|
+
"url": "git+https://github.com/Rakesh1002/agentdrive-mcp.git"
|
|
20
25
|
},
|
|
21
26
|
"bugs": {
|
|
22
27
|
"url": "https://github.com/Rakesh1002/agentdrive/issues"
|
|
@@ -35,12 +40,6 @@
|
|
|
35
40
|
"publishConfig": {
|
|
36
41
|
"access": "public"
|
|
37
42
|
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsc",
|
|
40
|
-
"dev": "tsc --watch",
|
|
41
|
-
"typecheck": "tsc --noEmit",
|
|
42
|
-
"test": "vitest run"
|
|
43
|
-
},
|
|
44
43
|
"dependencies": {
|
|
45
44
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
46
45
|
"zod": "^3.24.0"
|
|
@@ -49,5 +48,11 @@
|
|
|
49
48
|
"@types/node": "^22.0.0",
|
|
50
49
|
"typescript": "^5.7.0",
|
|
51
50
|
"vitest": "^2.1.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"dev": "tsc --watch",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "vitest run"
|
|
52
57
|
}
|
|
53
|
-
}
|
|
58
|
+
}
|