carto-md 2.0.0 → 2.0.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 +58 -0
- package/acp-strategy.md +480 -0
- package/package.json +6 -2
- package/src/acp/agent.js +221 -0
- package/src/acp/prompt.js +69 -0
- package/src/acp/providers/anthropic.js +125 -0
- package/src/acp/providers/index.js +83 -0
- package/src/acp/providers/openai.js +137 -0
- package/src/acp/session.js +71 -0
- package/src/acp/tools.js +150 -0
- package/src/cli/agent.js +13 -0
- package/src/cli/index.js +3 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { Carto } = require('../engine/carto');
|
|
7
|
+
|
|
8
|
+
class Session {
|
|
9
|
+
constructor(id, workingDir) {
|
|
10
|
+
this.id = id;
|
|
11
|
+
this.workingDir = workingDir;
|
|
12
|
+
this.history = [];
|
|
13
|
+
this.abortController = null;
|
|
14
|
+
this.carto = null;
|
|
15
|
+
this._indexed = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ensureIndexed() — Auto-indexes the project if .carto/carto.db is missing.
|
|
20
|
+
* Returns a status message if indexing was performed, null otherwise.
|
|
21
|
+
*/
|
|
22
|
+
async ensureIndexed() {
|
|
23
|
+
if (this._indexed) return null;
|
|
24
|
+
|
|
25
|
+
const dbPath = path.join(this.workingDir, '.carto', 'carto.db');
|
|
26
|
+
this.carto = new Carto();
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(dbPath)) {
|
|
29
|
+
// DB exists — just load the index (fast path, <10ms)
|
|
30
|
+
await this.carto.index(this.workingDir);
|
|
31
|
+
this._indexed = true;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// No DB — run full indexing
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
await this.carto.index(this.workingDir, { useWorkers: true });
|
|
38
|
+
this._indexed = true;
|
|
39
|
+
|
|
40
|
+
const meta = this.carto.getMeta();
|
|
41
|
+
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
|
42
|
+
const domains = this.carto.getDomainsList();
|
|
43
|
+
const routes = this.carto.getRoutes();
|
|
44
|
+
|
|
45
|
+
return `✓ Indexed project in ${duration}s — ${meta.totalFiles || 0} files, ${routes.length} routes, ${domains.length} domains detected.\n\n`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class SessionManager {
|
|
50
|
+
constructor() {
|
|
51
|
+
this._sessions = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
create(workingDir) {
|
|
55
|
+
const id = crypto.randomBytes(16).toString('hex');
|
|
56
|
+
const dir = workingDir || process.cwd();
|
|
57
|
+
const session = new Session(id, dir);
|
|
58
|
+
this._sessions.set(id, session);
|
|
59
|
+
return session;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get(id) {
|
|
63
|
+
return this._sessions.get(id) || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
delete(id) {
|
|
67
|
+
this._sessions.delete(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { Session, SessionManager };
|
package/src/acp/tools.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carto tools exposed to the LLM during the agent loop.
|
|
5
|
+
* These wrap the Carto engine's query methods as tool definitions
|
|
6
|
+
* compatible with OpenAI/Anthropic function calling format.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const CARTO_TOOLS = [
|
|
10
|
+
{
|
|
11
|
+
name: 'get_blast_radius',
|
|
12
|
+
description: 'Get all files, routes, and domains affected by changing a specific file. Use before making changes to understand impact.',
|
|
13
|
+
input_schema: { type: 'object', properties: { file: { type: 'string', description: 'Relative file path from project root' } }, required: ['file'] },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'get_context',
|
|
17
|
+
description: 'Get full structural context for a file: domain, blast radius, import neighbors, routes, models, env vars, and cross-domain dependencies.',
|
|
18
|
+
input_schema: { type: 'object', properties: { file: { type: 'string', description: 'Relative file path from project root' } }, required: ['file'] },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'get_structure',
|
|
22
|
+
description: 'Get project structure: import graph summary, entry points, high impact files, tech stack, and domains.',
|
|
23
|
+
input_schema: { type: 'object', properties: {}, required: [] },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'get_domain',
|
|
27
|
+
description: 'Get all routes, models, functions, and context for a specific domain (e.g. AUTH, PAYMENTS, DATABASE, CORE).',
|
|
28
|
+
input_schema: { type: 'object', properties: { domain: { type: 'string', description: 'Domain name e.g. AUTH, PAYMENTS' } }, required: ['domain'] },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'get_routes',
|
|
32
|
+
description: 'Get all API routes in this project including REST, tRPC, and webhooks.',
|
|
33
|
+
input_schema: { type: 'object', properties: {}, required: [] },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'get_change_plan',
|
|
37
|
+
description: 'Given a natural-language intent, returns files to touch, domains affected, blast radius, and similar patterns.',
|
|
38
|
+
input_schema: { type: 'object', properties: { intent: { type: 'string', description: 'What you want to change, e.g. "add rate limiting to /api/users"' } }, required: ['intent'] },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'get_similar_patterns',
|
|
42
|
+
description: 'Find structurally similar files — same domain, same route shape, or shared dependencies. Use to find conventions before writing new code.',
|
|
43
|
+
input_schema: { type: 'object', properties: { file: { type: 'string', description: 'Relative file path' }, limit: { type: 'number', description: 'Max results (default 5)' } }, required: ['file'] },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'get_neighbors',
|
|
47
|
+
description: 'Get import graph neighbors of a file — files it imports and files that import it.',
|
|
48
|
+
input_schema: { type: 'object', properties: { file: { type: 'string', description: 'Relative file path' }, hops: { type: 'number', description: 'Hops to traverse (default 1, max 3)' } }, required: ['file'] },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'get_cross_domain',
|
|
52
|
+
description: 'Get all import edges that cross domain boundaries. Use to detect unexpected coupling.',
|
|
53
|
+
input_schema: { type: 'object', properties: {}, required: [] },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'get_high_impact_files',
|
|
57
|
+
description: 'Get the files with the highest blast radius — most other files depend on them.',
|
|
58
|
+
input_schema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of files (default 10)' } }, required: [] },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'search_routes',
|
|
62
|
+
description: 'Search API routes by path or method. Case-insensitive.',
|
|
63
|
+
input_schema: { type: 'object', properties: { query: { type: 'string', description: 'Search query e.g. "auth", "POST"' } }, required: ['query'] },
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'get_models',
|
|
67
|
+
description: 'Get all data models (Prisma, Zod, TypeScript interfaces, etc.), optionally filtered by domain.',
|
|
68
|
+
input_schema: { type: 'object', properties: { domain: { type: 'string', description: 'Optional domain filter' } }, required: [] },
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* executeTool(name, input, session)
|
|
74
|
+
* Executes a Carto tool and returns the result as a string.
|
|
75
|
+
*/
|
|
76
|
+
function executeTool(name, input, session) {
|
|
77
|
+
const carto = session.carto;
|
|
78
|
+
if (!carto) return 'Project not indexed. Cannot execute tool.';
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
switch (name) {
|
|
82
|
+
case 'get_blast_radius': {
|
|
83
|
+
const result = carto.getBlastRadius(input.file);
|
|
84
|
+
if (!result) return `File not found: ${input.file}`;
|
|
85
|
+
return JSON.stringify(result, null, 2);
|
|
86
|
+
}
|
|
87
|
+
case 'get_context': {
|
|
88
|
+
const result = carto.getContextForFile(input.file);
|
|
89
|
+
if (!result) return `File not found: ${input.file}`;
|
|
90
|
+
return JSON.stringify(result, null, 2);
|
|
91
|
+
}
|
|
92
|
+
case 'get_structure': {
|
|
93
|
+
const s = carto.getStructure();
|
|
94
|
+
return JSON.stringify({
|
|
95
|
+
stack: s.stack,
|
|
96
|
+
domains: s.domains,
|
|
97
|
+
entryPoints: s.entryPoints,
|
|
98
|
+
highImpact: (s.highImpact || []).slice(0, 10),
|
|
99
|
+
meta: s.meta,
|
|
100
|
+
}, null, 2);
|
|
101
|
+
}
|
|
102
|
+
case 'get_domain': {
|
|
103
|
+
const result = carto.getDomain(input.domain);
|
|
104
|
+
if (!result) return `Domain not found: ${input.domain}`;
|
|
105
|
+
return JSON.stringify(result, null, 2);
|
|
106
|
+
}
|
|
107
|
+
case 'get_routes': {
|
|
108
|
+
return JSON.stringify(carto.getRoutes(), null, 2);
|
|
109
|
+
}
|
|
110
|
+
case 'get_change_plan': {
|
|
111
|
+
// Synthesize a change plan from structural data
|
|
112
|
+
const routes = carto.searchRoutes(input.intent);
|
|
113
|
+
const structure = carto.getStructure();
|
|
114
|
+
const domains = carto.getDomainsList();
|
|
115
|
+
return JSON.stringify({ matchingRoutes: routes, domains, highImpact: (structure.highImpact || []).slice(0, 5) }, null, 2);
|
|
116
|
+
}
|
|
117
|
+
case 'get_similar_patterns': {
|
|
118
|
+
// Find files in same domain with similar structure
|
|
119
|
+
const ctx = carto.getContextForFile(input.file);
|
|
120
|
+
if (!ctx) return `File not found: ${input.file}`;
|
|
121
|
+
const domain = carto.getDomain(ctx.domain);
|
|
122
|
+
const limit = input.limit || 5;
|
|
123
|
+
const similar = (domain && domain.files || []).filter(f => f !== input.file).slice(0, limit);
|
|
124
|
+
return JSON.stringify({ domain: ctx.domain, similarFiles: similar }, null, 2);
|
|
125
|
+
}
|
|
126
|
+
case 'get_neighbors': {
|
|
127
|
+
const result = carto.getNeighbors(input.file, input.hops || 1);
|
|
128
|
+
return JSON.stringify(result, null, 2);
|
|
129
|
+
}
|
|
130
|
+
case 'get_cross_domain': {
|
|
131
|
+
return JSON.stringify(carto.getCrossDomainDeps().slice(0, 50), null, 2);
|
|
132
|
+
}
|
|
133
|
+
case 'get_high_impact_files': {
|
|
134
|
+
return JSON.stringify(carto.getHighImpactFiles(input.limit || 10), null, 2);
|
|
135
|
+
}
|
|
136
|
+
case 'search_routes': {
|
|
137
|
+
return JSON.stringify(carto.searchRoutes(input.query), null, 2);
|
|
138
|
+
}
|
|
139
|
+
case 'get_models': {
|
|
140
|
+
return JSON.stringify(carto.getModels(input.domain), null, 2);
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return `Unknown tool: ${name}`;
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return `Tool error (${name}): ${err.message}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { CARTO_TOOLS, executeTool };
|
package/src/cli/agent.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { startAgent } = require('../acp/agent');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `carto agent` — Starts Carto in ACP mode (stdin/stdout).
|
|
7
|
+
* Editors like Zed and JetBrains spawn this as a subprocess.
|
|
8
|
+
*/
|
|
9
|
+
function run() {
|
|
10
|
+
startAgent();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { run };
|
package/src/cli/index.js
CHANGED
|
@@ -15,6 +15,7 @@ Commands:
|
|
|
15
15
|
check Report cross-domain deps, high-risk uncommitted changes, domain health
|
|
16
16
|
remove Remove AGENTS.md and .carto/ from this project
|
|
17
17
|
serve Start MCP server for AI tool integration
|
|
18
|
+
agent Start ACP agent mode (for Zed, JetBrains, VS Code)
|
|
18
19
|
|
|
19
20
|
Options:
|
|
20
21
|
--help, -h Show this help message
|
|
@@ -58,6 +59,8 @@ if (command === 'init') {
|
|
|
58
59
|
require('./remove').run(process.cwd());
|
|
59
60
|
} else if (command === 'serve') {
|
|
60
61
|
require('./serve').run(process.cwd());
|
|
62
|
+
} else if (command === 'agent') {
|
|
63
|
+
require('./agent').run();
|
|
61
64
|
} else {
|
|
62
65
|
console.error(`[CARTO] Unknown command: ${command}`);
|
|
63
66
|
printUsage();
|