carto-md 2.0.0 → 2.0.2

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.
@@ -0,0 +1,34 @@
1
+ # Carto V2 Benchmark Results
2
+
3
+ Generated: 2026-05-28T17:58:28.019Z
4
+ Platform: Node v20.20.1 · 8 CPUs · 8192.0 MB RAM · darwin arm64
5
+
6
+ | Repo | Source Files | Indexed | First Run | Second Run | DB Size | Routes | Import Edges |
7
+ |------|-------------|---------|-----------|------------|---------|--------|--------------|
8
+ | prisma | 3303 | 3303 | 1.6s | 178ms | 2.2 MB | 10 | 3590 |
9
+ | supabase | 6818 | 6746 | 4.9s | 725ms | 4.3 MB | 90 | 5754 |
10
+ | vscode | 10565 | 10565 | 9.7s | 1.2s | 10.6 MB | 11 | 19769 |
11
+ | zed | 1837 | 1837 | 2.7s | 83ms | 4.7 MB | 12 | 2176 |
12
+
13
+ ## Domains Detected
14
+
15
+ **prisma:** DATABASE(1044) · CORE(752) · EVENTS(36) · AUTH(8)
16
+ **supabase:** CORE(4243) · AUTH(412) · DATABASE(387) · PAYMENTS(51) · EVENTS(17) · NOTIFICATIONS(12) · TRPC(3)
17
+ **vscode:** CORE(5330) · AUTH(850) · EVENTS(446) · DATABASE(419) · NOTIFICATIONS(5)
18
+ **zed:** CORE(1424) · DATABASE(110) · AUTH(75) · EVENTS(55) · PAYMENTS(53) · NOTIFICATIONS(10) · TRPC(2)
19
+
20
+ ## MCP Query Latency
21
+
22
+ | Repo | get_structure | get_routes | get_domains_list |
23
+ |------|--------------|------------|-----------------|
24
+ | prisma | 0ms | 0ms | 1ms |
25
+ | supabase | 1ms | 0ms | 3ms |
26
+ | vscode | 1ms | 0ms | 3ms |
27
+ | zed | 0ms | 0ms | 1ms |
28
+
29
+ ## Target Assessment
30
+
31
+ - **prisma**: ✅ All targets met
32
+ - **supabase**: ✅ All targets met
33
+ - **vscode**: ✅ All targets met
34
+ - **zed**: ✅ All targets met
package/CONTRIBUTING.md CHANGED
@@ -215,7 +215,7 @@ cd carto
215
215
  npm install
216
216
  node src/cli/index.js init # test in any project
217
217
  node src/cli/index.js serve # test MCP server
218
- npm test # run test suite (30 tests)
218
+ npm test # run test suite (35 tests)
219
219
  node test/correctness.js # run correctness tests (31 tests)
220
220
  node test/benchmark.js # run benchmarks against real repos
221
221
  ```
@@ -232,7 +232,7 @@ node test/benchmark.js # run benchmarks against real repos
232
232
  - [ ] Extension added to `CODE_EXTS` and `detectLanguage()` in `sync-v2.js`
233
233
  - [ ] No changes to merger logic (unless explicitly fixing a merger bug)
234
234
  - [ ] No network calls added
235
- - [ ] `npm test` passes (30/30)
235
+ - [ ] `npm test` passes (35/35)
236
236
  - [ ] `node test/correctness.js` passes (31/31)
237
237
 
238
238
  ---
package/README.md CHANGED
@@ -162,12 +162,70 @@ Custom domains via `carto.config.json`:
162
162
  | `carto sync` | Full re-index (skips unchanged files via mtime+size cache) |
163
163
  | `carto watch` | Incremental live re-index on every file save (<50ms) |
164
164
  | `carto serve` | Start MCP server for Kiro / Cursor / Claude |
165
+ | `carto agent` | Start ACP agent mode (for Zed / JetBrains / VS Code) |
165
166
  | `carto impact <file>` | Blast radius: risk level, affected files, routes at risk |
166
167
  | `carto check` | Cross-domain violations, high-risk uncommitted changes, domain health |
167
168
  | `carto remove` | Remove AGENTS.md and .carto/ from project |
168
169
 
169
170
  ---
170
171
 
172
+ ## ACP Agent (Zed / JetBrains / VS Code)
173
+
174
+ Carto works as a full **ACP agent** — not just a passive tool server, but an active coding agent with architectural awareness.
175
+
176
+ ```
177
+ User: "Add rate limiting to /api/users"
178
+
179
+ Carto auto-queries its own SQLite:
180
+ - Blast radius of relevant files
181
+ - Domain context (AUTH)
182
+ - Similar patterns in codebase
183
+
184
+ Builds rich prompt with structural context
185
+
186
+ Sends to LLM (your API key) → streams answer + diffs back to editor
187
+ ```
188
+
189
+ ### Setup in Zed
190
+
191
+ Add to `~/.config/zed/settings.json`:
192
+
193
+ ```json
194
+ {
195
+ "agent_servers": {
196
+ "Carto": {
197
+ "command": "carto",
198
+ "args": ["agent"]
199
+ }
200
+ }
201
+ }
202
+ ```
203
+
204
+ ### BYOK (Bring Your Own Key)
205
+
206
+ Carto supports any LLM provider — configure in your editor:
207
+
208
+ | Provider | Models |
209
+ |----------|--------|
210
+ | Anthropic | Claude Sonnet 4, Haiku |
211
+ | OpenAI | GPT-4o, GPT-4o-mini, o1, o3 |
212
+ | Google Gemini | Gemini 2.5 Pro, 2.5 Flash |
213
+ | Ollama | Any local model (free) |
214
+ | OpenRouter | Any model via single API |
215
+ | Groq | Ultra-fast inference |
216
+ | Together AI | Open-source models |
217
+ | Azure OpenAI | Enterprise deployments |
218
+
219
+ ### What makes it different
220
+
221
+ Other agents are smart but blind. Carto sees the architecture:
222
+ - Auto-indexes your project on first session (1-10s depending on size)
223
+ - Injects structural context into every LLM call (blast radius, domains, routes)
224
+ - 12 internal tools the LLM can call during reasoning
225
+ - Zero cost to you beyond your own API key
226
+
227
+ ---
228
+
171
229
  ## V1 → V2 migration
172
230
 
173
231
  Run `carto sync` — it auto-migrates your existing `graph-cache.json` and `map.json` into SQLite and renames them to `.bak`. No manual steps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Structural intelligence layer for AI coding tools. Indexes your codebase into SQLite — routes, models, import graph, blast radius, domains — and exposes 16 MCP tools for Kiro, Cursor, and Claude.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
@@ -12,6 +12,7 @@
12
12
  "test:benchmark": "node test/benchmark.js"
13
13
  },
14
14
  "dependencies": {
15
+ "@agentclientprotocol/sdk": "^0.22.1",
15
16
  "@babel/parser": "^7.29.3",
16
17
  "@modelcontextprotocol/sdk": "^1.0.0",
17
18
  "better-sqlite3": "11.7.0",
@@ -39,6 +40,8 @@
39
40
  "agents",
40
41
  "AGENTS.md",
41
42
  "AI",
43
+ "acp",
44
+ "agent-client-protocol",
42
45
  "context",
43
46
  "codebase",
44
47
  "developer-tools",
@@ -51,6 +54,7 @@
51
54
  "blast-radius",
52
55
  "import-graph",
53
56
  "kiro",
54
- "claude"
57
+ "claude",
58
+ "zed"
55
59
  ]
56
60
  }
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ const { Readable, Writable } = require('stream');
4
+ const acp = require('@agentclientprotocol/sdk');
5
+ const { SessionManager } = require('./session');
6
+ const { buildSystemPrompt, buildContextBlock } = require('./prompt');
7
+ const { CARTO_TOOLS, executeTool } = require('./tools');
8
+ const { ProviderRegistry } = require('./providers');
9
+
10
+ const MAX_ITERATIONS = 25;
11
+
12
+ class CartoAgent {
13
+ constructor(connection) {
14
+ this.connection = connection;
15
+ this.sessions = new SessionManager();
16
+ this.providers = new ProviderRegistry();
17
+ }
18
+
19
+ async initialize(_params) {
20
+ return {
21
+ protocolVersion: acp.PROTOCOL_VERSION,
22
+ agentCapabilities: {
23
+ loadSession: false,
24
+ promptCapabilities: { image: false, audio: false, embeddedContext: true },
25
+ },
26
+ agentInfo: {
27
+ name: 'carto',
28
+ title: 'Carto',
29
+ version: require('../../package.json').version,
30
+ },
31
+ authMethods: [],
32
+ };
33
+ }
34
+
35
+ async authenticate(_params) {
36
+ return {};
37
+ }
38
+
39
+ async newSession(params) {
40
+ const session = this.sessions.create(params.cwd || process.cwd());
41
+ return { sessionId: session.id };
42
+ }
43
+
44
+ async setSessionMode(_params) {
45
+ return {};
46
+ }
47
+
48
+ async prompt(params) {
49
+ const session = this.sessions.get(params.sessionId);
50
+ if (!session) throw new Error(`Session ${params.sessionId} not found`);
51
+
52
+ session.abortController?.abort();
53
+ session.abortController = new AbortController();
54
+ const signal = session.abortController.signal;
55
+
56
+ try {
57
+ await this._runAgentLoop(params, session, signal);
58
+ } catch (err) {
59
+ if (signal.aborted) return { stopReason: 'cancelled' };
60
+ throw err;
61
+ }
62
+
63
+ session.abortController = null;
64
+ return { stopReason: 'end_turn' };
65
+ }
66
+
67
+ async cancel(params) {
68
+ const session = this.sessions.get(params.sessionId);
69
+ if (session) session.abortController?.abort();
70
+ }
71
+
72
+ // Provider methods
73
+ async unstable_listProviders(_params) {
74
+ return { providers: this.providers.list() };
75
+ }
76
+
77
+ async unstable_setProvider(params) {
78
+ this.providers.set(params);
79
+ return {};
80
+ }
81
+
82
+ async unstable_disableProvider(_params) {
83
+ this.providers.disable();
84
+ return {};
85
+ }
86
+
87
+ // Session list/load stubs
88
+ async listSessions(_params) { return { sessions: [] }; }
89
+ async closeSession(_params) { return {}; }
90
+
91
+ // ─── Agent Loop ──────────────────────────────────────────────────────────
92
+
93
+ async _runAgentLoop(params, session, signal) {
94
+ // 1. Ensure project is indexed
95
+ const indexMsg = await session.ensureIndexed();
96
+ if (indexMsg) {
97
+ await this.connection.sessionUpdate({
98
+ sessionId: session.id,
99
+ update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: indexMsg } },
100
+ });
101
+ }
102
+
103
+ // 2. Extract user message text
104
+ const userText = (params.prompt || [])
105
+ .filter(b => b.type === 'text')
106
+ .map(b => b.text)
107
+ .join('\n');
108
+
109
+ // 3. Build context from Carto intelligence
110
+ const contextBlock = buildContextBlock(session.carto, session.workingDir);
111
+
112
+ // 4. Build messages array
113
+ const systemPrompt = buildSystemPrompt(contextBlock);
114
+ const messages = [
115
+ { role: 'system', content: systemPrompt },
116
+ ...session.history,
117
+ { role: 'user', content: userText },
118
+ ];
119
+ session.history.push({ role: 'user', content: userText });
120
+
121
+ // 5. Get the active provider
122
+ const provider = this.providers.getActive();
123
+ if (!provider) {
124
+ const msg = 'No LLM provider configured. Please set a provider in your editor settings (API key + model).';
125
+ await this.connection.sessionUpdate({
126
+ sessionId: session.id,
127
+ update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: msg } },
128
+ });
129
+ session.history.push({ role: 'assistant', content: msg });
130
+ return;
131
+ }
132
+
133
+ // 6. Agent loop — iterate until no more tool calls or max iterations
134
+ let iterations = 0;
135
+ while (iterations++ < MAX_ITERATIONS) {
136
+ if (signal.aborted) throw new Error('cancelled');
137
+
138
+ const response = await provider.chat(messages, CARTO_TOOLS, signal);
139
+
140
+ // Stream text content
141
+ let assistantText = '';
142
+ let toolCalls = [];
143
+
144
+ for (const block of response.content) {
145
+ if (block.type === 'text' && block.text) {
146
+ assistantText += block.text;
147
+ await this.connection.sessionUpdate({
148
+ sessionId: session.id,
149
+ update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: block.text } },
150
+ });
151
+ } else if (block.type === 'tool_use') {
152
+ toolCalls.push(block);
153
+ }
154
+ }
155
+
156
+ // Add assistant message to history
157
+ messages.push({ role: 'assistant', content: response.content });
158
+
159
+ if (toolCalls.length === 0) {
160
+ session.history.push({ role: 'assistant', content: assistantText });
161
+ break;
162
+ }
163
+
164
+ // Execute tool calls
165
+ const toolResults = [];
166
+ for (const tc of toolCalls) {
167
+ // Report tool call to editor
168
+ await this.connection.sessionUpdate({
169
+ sessionId: session.id,
170
+ update: {
171
+ sessionUpdate: 'tool_call',
172
+ toolCallId: tc.id,
173
+ title: tc.name,
174
+ kind: 'other',
175
+ status: 'pending',
176
+ rawInput: tc.input,
177
+ },
178
+ });
179
+
180
+ // Execute
181
+ const result = await executeTool(tc.name, tc.input, session);
182
+
183
+ // Report completion
184
+ await this.connection.sessionUpdate({
185
+ sessionId: session.id,
186
+ update: {
187
+ sessionUpdate: 'tool_call_update',
188
+ toolCallId: tc.id,
189
+ status: 'completed',
190
+ content: [{ type: 'content', content: { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) } }],
191
+ rawOutput: result,
192
+ },
193
+ });
194
+
195
+ toolResults.push({ type: 'tool_result', tool_use_id: tc.id, content: typeof result === 'string' ? result : JSON.stringify(result) });
196
+ }
197
+
198
+ // Add tool results to messages
199
+ messages.push({ role: 'user', content: toolResults });
200
+ }
201
+
202
+ if (iterations > MAX_ITERATIONS) {
203
+ await this.connection.sessionUpdate({
204
+ sessionId: session.id,
205
+ update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: '\n\n⚠️ Reached maximum iterations (25). Stopping.' } },
206
+ });
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * startAgent() — Entry point. Connects ACP over stdin/stdout.
213
+ */
214
+ function startAgent() {
215
+ const input = Writable.toWeb(process.stdout);
216
+ const output = Readable.toWeb(process.stdin);
217
+ const stream = acp.ndJsonStream(input, output);
218
+ new acp.AgentSideConnection((conn) => new CartoAgent(conn), stream);
219
+ }
220
+
221
+ module.exports = { startAgent, CartoAgent };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const SYSTEM_PROMPT_TEMPLATE = `You are Carto, an AI coding agent with deep architectural awareness.
4
+
5
+ You understand this project's structure before writing a single line:
6
+ - Every file's blast radius (what breaks if this changes)
7
+ - All API routes and which files define them
8
+ - Domain clusters (AUTH, PAYMENTS, DATABASE, etc.)
9
+ - The full import graph and cross-domain dependencies
10
+
11
+ RULES:
12
+ 1. Always check blast radius before making changes to high-impact files
13
+ 2. Reference real file names and routes — never hallucinate paths
14
+ 3. Make minimal, focused changes — read first, then write
15
+ 4. Warn the user if a change crosses domain boundaries
16
+ 5. After changes, confirm what was changed and what it affects
17
+ 6. Use existing patterns (check get_similar_patterns before writing new code)
18
+ 7. If unsure, read the relevant files — don't guess
19
+
20
+ You have access to Carto tools that give you structural intelligence about this codebase. Use them proactively.
21
+
22
+ PROJECT CONTEXT:
23
+ {context}`;
24
+
25
+ /**
26
+ * buildContextBlock(carto, workingDir)
27
+ * Generates a structural context summary from the indexed project.
28
+ */
29
+ function buildContextBlock(carto, workingDir) {
30
+ if (!carto) return 'Project not yet indexed.';
31
+
32
+ try {
33
+ const lines = [];
34
+ const structure = carto.getStructure();
35
+
36
+ if (structure.stack && structure.stack.length > 0) {
37
+ lines.push(`Stack: ${structure.stack.join(', ')}`);
38
+ }
39
+
40
+ if (structure.meta) {
41
+ const m = structure.meta;
42
+ lines.push(`Size: ${m.totalFiles || 0} files, ${m.totalRoutes || 0} routes, ${m.totalImportEdges || 0} import edges`);
43
+ }
44
+
45
+ const domains = carto.getDomainsList();
46
+ if (domains.length > 0) {
47
+ lines.push(`Domains: ${domains.map(d => `${d.name} (${d.fileCount} files)`).join(', ')}`);
48
+ }
49
+
50
+ const highImpact = carto.getHighImpactFiles(5);
51
+ if (highImpact && highImpact.length > 0) {
52
+ lines.push(`High-impact files: ${highImpact.map(f => f.file || f).join(', ')}`);
53
+ }
54
+
55
+ return lines.join('\n') || 'Project indexed but no structural data available.';
56
+ } catch {
57
+ return 'Project indexed.';
58
+ }
59
+ }
60
+
61
+ /**
62
+ * buildSystemPrompt(contextBlock)
63
+ * Assembles the full system prompt with injected context.
64
+ */
65
+ function buildSystemPrompt(contextBlock) {
66
+ return SYSTEM_PROMPT_TEMPLATE.replace('{context}', contextBlock);
67
+ }
68
+
69
+ module.exports = { buildSystemPrompt, buildContextBlock };
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const { URL } = require('url');
5
+
6
+ /**
7
+ * AnthropicProvider — Handles Anthropic Claude API (Messages API).
8
+ * Supports tool use and streaming-ready structure.
9
+ */
10
+ class AnthropicProvider {
11
+ constructor(apiKey, baseUrl, model) {
12
+ this.apiKey = apiKey;
13
+ this.baseUrl = (baseUrl || 'https://api.anthropic.com').replace(/\/$/, '');
14
+ this.model = model;
15
+ }
16
+
17
+ /**
18
+ * chat(messages, tools, signal)
19
+ * Calls the Anthropic Messages API.
20
+ * Returns { content: [{ type: 'text', text } | { type: 'tool_use', id, name, input }] }
21
+ */
22
+ async chat(messages, tools, signal) {
23
+ // Separate system message from conversation
24
+ let system = '';
25
+ const conversationMessages = [];
26
+
27
+ for (const msg of messages) {
28
+ if (msg.role === 'system') {
29
+ system += (typeof msg.content === 'string' ? msg.content : '') + '\n';
30
+ } else {
31
+ conversationMessages.push(this._formatMessage(msg));
32
+ }
33
+ }
34
+
35
+ const body = {
36
+ model: this.model,
37
+ max_tokens: 8192,
38
+ system: system.trim(),
39
+ messages: conversationMessages,
40
+ };
41
+
42
+ if (tools && tools.length > 0) {
43
+ body.tools = tools.map(t => ({
44
+ name: t.name,
45
+ description: t.description,
46
+ input_schema: t.input_schema,
47
+ }));
48
+ }
49
+
50
+ const data = await this._request('/v1/messages', body, signal);
51
+ return this._parseResponse(data);
52
+ }
53
+
54
+ _formatMessage(msg) {
55
+ if (typeof msg.content === 'string') {
56
+ return { role: msg.role, content: msg.content };
57
+ }
58
+
59
+ if (Array.isArray(msg.content)) {
60
+ if (msg.role === 'assistant') {
61
+ // Pass through content blocks (text + tool_use)
62
+ return { role: 'assistant', content: msg.content };
63
+ }
64
+ if (msg.role === 'user') {
65
+ // Convert tool_result blocks to Anthropic format
66
+ const blocks = msg.content.map(b => {
67
+ if (b.type === 'tool_result') {
68
+ return { type: 'tool_result', tool_use_id: b.tool_use_id, content: b.content };
69
+ }
70
+ return b;
71
+ });
72
+ return { role: 'user', content: blocks };
73
+ }
74
+ }
75
+
76
+ return { role: msg.role, content: msg.content };
77
+ }
78
+
79
+ _parseResponse(data) {
80
+ if (!data.content) return { content: [{ type: 'text', text: 'No response from model.' }] };
81
+ // Anthropic already returns content in the format we need
82
+ return { content: data.content };
83
+ }
84
+
85
+ _request(endpoint, body, signal) {
86
+ return new Promise((resolve, reject) => {
87
+ const url = new URL(this.baseUrl + endpoint);
88
+
89
+ const options = {
90
+ hostname: url.hostname,
91
+ port: url.port || 443,
92
+ path: url.pathname + url.search,
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'x-api-key': this.apiKey,
97
+ 'anthropic-version': '2023-06-01',
98
+ },
99
+ };
100
+
101
+ const req = https.request(options, (res) => {
102
+ let data = '';
103
+ res.on('data', chunk => { data += chunk; });
104
+ res.on('end', () => {
105
+ try {
106
+ const parsed = JSON.parse(data);
107
+ if (parsed.error) reject(new Error(parsed.error.message || JSON.stringify(parsed.error)));
108
+ else resolve(parsed);
109
+ } catch { reject(new Error(`Invalid JSON response: ${data.slice(0, 200)}`)); }
110
+ });
111
+ });
112
+
113
+ req.on('error', reject);
114
+
115
+ if (signal) {
116
+ signal.addEventListener('abort', () => { req.destroy(); reject(new Error('cancelled')); }, { once: true });
117
+ }
118
+
119
+ req.write(JSON.stringify(body));
120
+ req.end();
121
+ });
122
+ }
123
+ }
124
+
125
+ module.exports = { AnthropicProvider };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const { OpenAIProvider } = require('./openai');
4
+ const { AnthropicProvider } = require('./anthropic');
5
+
6
+ const SUPPORTED_PROVIDERS = [
7
+ { id: 'anthropic', name: 'Anthropic', defaultModel: 'claude-sonnet-4-20250514', models: ['claude-sonnet-4-20250514', 'claude-haiku-3-20250414'] },
8
+ { id: 'openai', name: 'OpenAI', defaultModel: 'gpt-4o', models: ['gpt-4o', 'gpt-4o-mini', 'o1', 'o3'] },
9
+ { id: 'gemini', name: 'Google Gemini', defaultModel: 'gemini-2.5-flash', models: ['gemini-2.5-pro', 'gemini-2.5-flash'] },
10
+ { id: 'ollama', name: 'Ollama (local)', defaultModel: 'llama3.1', models: [] },
11
+ { id: 'openrouter', name: 'OpenRouter', defaultModel: 'anthropic/claude-sonnet-4', models: [] },
12
+ { id: 'azure', name: 'Azure OpenAI', defaultModel: 'gpt-4o', models: [] },
13
+ { id: 'groq', name: 'Groq', defaultModel: 'llama-3.3-70b-versatile', models: [] },
14
+ { id: 'together', name: 'Together AI', defaultModel: 'meta-llama/Llama-3-70b-chat-hf', models: [] },
15
+ ];
16
+
17
+ const DEFAULT_BASE_URLS = {
18
+ anthropic: 'https://api.anthropic.com',
19
+ openai: 'https://api.openai.com/v1',
20
+ gemini: 'https://generativelanguage.googleapis.com/v1beta/openai',
21
+ ollama: 'http://localhost:11434/v1',
22
+ openrouter: 'https://openrouter.ai/api/v1',
23
+ groq: 'https://api.groq.com/openai/v1',
24
+ together: 'https://api.together.xyz/v1',
25
+ };
26
+
27
+ class ProviderRegistry {
28
+ constructor() {
29
+ this._active = null;
30
+ this._config = null;
31
+ }
32
+
33
+ /**
34
+ * list() — Returns supported providers for ACP providers/list.
35
+ */
36
+ list() {
37
+ return SUPPORTED_PROVIDERS.map(p => ({
38
+ id: p.id,
39
+ name: p.name,
40
+ defaultModel: p.defaultModel,
41
+ models: p.models,
42
+ }));
43
+ }
44
+
45
+ /**
46
+ * set(params) — Configures the active provider from ACP providers/set.
47
+ * params: { providerId, apiKey, baseUrl?, model? }
48
+ */
49
+ set(params) {
50
+ const { providerId, apiKey, baseUrl, model } = params;
51
+ const providerDef = SUPPORTED_PROVIDERS.find(p => p.id === providerId);
52
+ if (!providerDef) throw new Error(`Unknown provider: ${providerId}`);
53
+
54
+ const resolvedModel = model || providerDef.defaultModel;
55
+ const resolvedUrl = baseUrl || DEFAULT_BASE_URLS[providerId] || '';
56
+
57
+ this._config = { providerId, apiKey, baseUrl: resolvedUrl, model: resolvedModel };
58
+
59
+ if (providerId === 'anthropic') {
60
+ this._active = new AnthropicProvider(apiKey, resolvedUrl, resolvedModel);
61
+ } else {
62
+ // All others use OpenAI-compatible API
63
+ this._active = new OpenAIProvider(apiKey, resolvedUrl, resolvedModel);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * disable() — Clears the active provider.
69
+ */
70
+ disable() {
71
+ this._active = null;
72
+ this._config = null;
73
+ }
74
+
75
+ /**
76
+ * getActive() — Returns the active provider instance or null.
77
+ */
78
+ getActive() {
79
+ return this._active;
80
+ }
81
+ }
82
+
83
+ module.exports = { ProviderRegistry, SUPPORTED_PROVIDERS };
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { URL } = require('url');
6
+
7
+ /**
8
+ * OpenAIProvider — Handles OpenAI and all OpenAI-compatible APIs.
9
+ * Works with: OpenAI, Gemini, Ollama, OpenRouter, Together, Groq, Azure, LM Studio, vLLM.
10
+ */
11
+ class OpenAIProvider {
12
+ constructor(apiKey, baseUrl, model) {
13
+ this.apiKey = apiKey;
14
+ this.baseUrl = baseUrl.replace(/\/$/, '');
15
+ this.model = model;
16
+ }
17
+
18
+ /**
19
+ * chat(messages, tools, signal)
20
+ * Calls the OpenAI-compatible /chat/completions endpoint.
21
+ * Returns { content: [{ type: 'text', text } | { type: 'tool_use', id, name, input }] }
22
+ */
23
+ async chat(messages, tools, signal) {
24
+ const body = {
25
+ model: this.model,
26
+ messages: this._formatMessages(messages),
27
+ stream: false,
28
+ };
29
+
30
+ if (tools && tools.length > 0) {
31
+ body.tools = tools.map(t => ({
32
+ type: 'function',
33
+ function: { name: t.name, description: t.description, parameters: t.input_schema },
34
+ }));
35
+ }
36
+
37
+ const data = await this._request('/chat/completions', body, signal);
38
+ return this._parseResponse(data);
39
+ }
40
+
41
+ _formatMessages(messages) {
42
+ const formatted = [];
43
+ for (const msg of messages) {
44
+ if (typeof msg.content === 'string') {
45
+ formatted.push({ role: msg.role, content: msg.content });
46
+ } else if (Array.isArray(msg.content)) {
47
+ // Handle tool_use blocks (assistant) and tool_result blocks (user)
48
+ if (msg.role === 'assistant') {
49
+ const text = msg.content.filter(b => b.type === 'text').map(b => b.text).join('');
50
+ const toolCalls = msg.content.filter(b => b.type === 'tool_use').map(b => ({
51
+ id: b.id,
52
+ type: 'function',
53
+ function: { name: b.name, arguments: JSON.stringify(b.input) },
54
+ }));
55
+ const entry = { role: 'assistant' };
56
+ if (text) entry.content = text;
57
+ if (toolCalls.length > 0) entry.tool_calls = toolCalls;
58
+ formatted.push(entry);
59
+ } else if (msg.role === 'user') {
60
+ // Tool results
61
+ for (const block of msg.content) {
62
+ if (block.type === 'tool_result') {
63
+ formatted.push({ role: 'tool', tool_call_id: block.tool_use_id, content: block.content });
64
+ } else if (block.type === 'text') {
65
+ formatted.push({ role: 'user', content: block.text });
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return formatted;
72
+ }
73
+
74
+ _parseResponse(data) {
75
+ const choice = data.choices && data.choices[0];
76
+ if (!choice) return { content: [{ type: 'text', text: 'No response from model.' }] };
77
+
78
+ const msg = choice.message;
79
+ const content = [];
80
+
81
+ if (msg.content) {
82
+ content.push({ type: 'text', text: msg.content });
83
+ }
84
+
85
+ if (msg.tool_calls) {
86
+ for (const tc of msg.tool_calls) {
87
+ let input = {};
88
+ try { input = JSON.parse(tc.function.arguments); } catch {}
89
+ content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input });
90
+ }
91
+ }
92
+
93
+ return { content };
94
+ }
95
+
96
+ _request(endpoint, body, signal) {
97
+ return new Promise((resolve, reject) => {
98
+ const url = new URL(this.baseUrl + endpoint);
99
+ const isHttps = url.protocol === 'https:';
100
+ const mod = isHttps ? https : http;
101
+
102
+ const options = {
103
+ hostname: url.hostname,
104
+ port: url.port || (isHttps ? 443 : 80),
105
+ path: url.pathname + url.search,
106
+ method: 'POST',
107
+ headers: {
108
+ 'Content-Type': 'application/json',
109
+ 'Authorization': `Bearer ${this.apiKey}`,
110
+ },
111
+ };
112
+
113
+ const req = mod.request(options, (res) => {
114
+ let data = '';
115
+ res.on('data', chunk => { data += chunk; });
116
+ res.on('end', () => {
117
+ try {
118
+ const parsed = JSON.parse(data);
119
+ if (parsed.error) reject(new Error(parsed.error.message || JSON.stringify(parsed.error)));
120
+ else resolve(parsed);
121
+ } catch { reject(new Error(`Invalid JSON response: ${data.slice(0, 200)}`)); }
122
+ });
123
+ });
124
+
125
+ req.on('error', reject);
126
+
127
+ if (signal) {
128
+ signal.addEventListener('abort', () => { req.destroy(); reject(new Error('cancelled')); }, { once: true });
129
+ }
130
+
131
+ req.write(JSON.stringify(body));
132
+ req.end();
133
+ });
134
+ }
135
+ }
136
+
137
+ module.exports = { OpenAIProvider };
@@ -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 };
@@ -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 };
@@ -16,6 +16,9 @@ function formatSections({ routes, models, frontend, structure, warnings, fileMap
16
16
  sections.push(`- ${icon} ${entry.name}${suffix}`);
17
17
  }
18
18
  } else {
19
+ if (process.env.CARTO_DEBUG) {
20
+ console.warn('[CARTO] Warning: structure data was empty when formatting AGENTS.md');
21
+ }
19
22
  sections.push('_No structure data available._');
20
23
  }
21
24
 
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ /**
6
+ * IGNORE_DIRS — names skipped at the top level when listing project structure.
7
+ *
8
+ * This is intentionally a *small* set tuned for the "Project Structure (auto)"
9
+ * block in AGENTS.md. It is NOT the same as the recursive file-discovery
10
+ * ignore lists (e.g. JS_IGNORE / PYTHON_IGNORE in src/detector/files.js or
11
+ * IGNORE_DIRS in src/store/sync-v2.js) — those filter what gets indexed.
12
+ *
13
+ * Top-level structure should still surface things like `dist/`, `build/`,
14
+ * `coverage/` (so users see what their project actually contains), but it
15
+ * should hide noise (`node_modules`, `.git`, `.carto`, etc.) and the file
16
+ * the structure block is about to be merged into (`AGENTS.md`).
17
+ *
18
+ * Anchored on the original V1 set in src/sync.js so existing AGENTS.md
19
+ * outputs do not drift after the V1 → V2 cleanup.
20
+ */
21
+ const IGNORE_DIRS = new Set([
22
+ 'node_modules',
23
+ '.git',
24
+ '__pycache__',
25
+ '.venv',
26
+ 'venv',
27
+ '.idea',
28
+ '.vscode',
29
+ '.carto',
30
+ 'AGENTS.md'
31
+ ]);
32
+
33
+ /**
34
+ * scanStructure(basePath) → Array<{ name: string, type: 'dir' | 'file' }>
35
+ *
36
+ * Lists the immediate children of `basePath` (one level deep, no recursion).
37
+ * Filters out IGNORE_DIRS. Sorts: directories before files; alphabetical
38
+ * within each group.
39
+ *
40
+ * Symlinks are reported by Dirent as neither dir nor file → treated as 'file'.
41
+ * Failures (missing dir, EACCES, etc.) return an empty array silently — the
42
+ * formatter handles the empty case.
43
+ */
44
+ async function scanStructure(basePath) {
45
+ const entries = [];
46
+ let items;
47
+ try {
48
+ items = await fs.promises.readdir(basePath, { withFileTypes: true });
49
+ } catch {
50
+ return entries;
51
+ }
52
+
53
+ for (const item of items) {
54
+ if (IGNORE_DIRS.has(item.name)) continue;
55
+ entries.push({
56
+ name: item.name,
57
+ type: item.isDirectory() ? 'dir' : 'file'
58
+ });
59
+ }
60
+
61
+ entries.sort((a, b) => {
62
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
63
+ return a.name.localeCompare(b.name);
64
+ });
65
+
66
+ return entries;
67
+ }
68
+
69
+ module.exports = { scanStructure, IGNORE_DIRS };
@@ -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();
@@ -13,6 +13,7 @@ const { clusterByDomain } = require('../agents/domains');
13
13
  const { clusterByGraph } = require('../agents/leiden');
14
14
  const { formatSections, formatDomainFile } = require('../agents/formatter');
15
15
  const { mergeIntoAgentsMd } = require('../agents/merger');
16
+ const { scanStructure } = require('../agents/scan-structure');
16
17
  const { WorkerPool, POOL_SIZE } = require('../engine/worker-pool');
17
18
  const { parseCartoIgnore } = require('../security/ignore');
18
19
 
@@ -420,7 +421,7 @@ async function runSyncV2(config) {
420
421
 
421
422
  // 8. Generate outputs (only if files changed)
422
423
  if (toProcess.length > 0) {
423
- generateOutputs(store, config, projectRoot, store.getImportGraph());
424
+ await generateOutputs(store, config, projectRoot, store.getImportGraph());
424
425
  }
425
426
 
426
427
  console.log(`[CARTO] Indexed ${toProcess.length} files (${cached} cached) in ${elapsed}ms`);
@@ -461,12 +462,13 @@ function buildFileDataFromStore(store) {
461
462
  /**
462
463
  * Generate backward-compatible outputs (AGENTS.md, context files, map.json)
463
464
  */
464
- function generateOutputs(store, config, projectRoot, importGraph) {
465
+ async function generateOutputs(store, config, projectRoot, importGraph) {
465
466
  const structure = store.getStructure();
466
467
  const routes = store.getRoutes();
467
468
  const models = store.getModels();
468
469
  const envVars = store.getEnvVars();
469
470
  const domains = store.getDomainsList();
471
+ const topLevelStructure = await scanStructure(projectRoot);
470
472
 
471
473
  // Write domain context files — lazy: only write if stale or missing
472
474
  const contextDir = path.join(projectRoot, '.carto', 'context');
@@ -518,7 +520,7 @@ function generateOutputs(store, config, projectRoot, importGraph) {
518
520
  routes,
519
521
  models,
520
522
  frontend: { fetches: [], storageKeys: [] },
521
- structure: [],
523
+ structure: topLevelStructure,
522
524
  warnings: [],
523
525
  fileMap: [],
524
526
  functions: {},
package/src/sync.js CHANGED
@@ -13,8 +13,7 @@ const { buildStackLine } = require('./extractors/stack');
13
13
  const { loadHashes, saveHashes, computeChangedFiles } = require('./cache/file-hash');
14
14
  const { loadGraphCache, saveGraphCache, buildEmptyCache } = require('./cache/graph-cache');
15
15
  const { applyIncrementalUpdate, recomputeGraphMetrics } = require('./engine/incremental');
16
-
17
- const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto', 'AGENTS.md']);
16
+ const { scanStructure } = require('./agents/scan-structure');
18
17
 
19
18
  // Load plugins once at module load
20
19
  const plugins = loadLanguagePlugins();
@@ -29,22 +28,6 @@ async function safeReadFile(filePath, warnings) {
29
28
  }
30
29
  }
31
30
 
32
- async function scanStructure(basePath) {
33
- const entries = [];
34
- try {
35
- const items = await fs.promises.readdir(basePath, { withFileTypes: true });
36
- for (const item of items) {
37
- if (IGNORE_DIRS.has(item.name)) continue;
38
- entries.push({ name: item.name, type: item.isDirectory() ? 'dir' : 'file' });
39
- }
40
- entries.sort((a, b) => {
41
- if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
42
- return a.name.localeCompare(b.name);
43
- });
44
- } catch {}
45
- return entries;
46
- }
47
-
48
31
  /**
49
32
  * runFullSync(config)
50
33
  *