session-collab-mcp 0.4.6 → 0.5.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/migrations/0004_symbols.sql +18 -0
- package/migrations/0005_references.sql +19 -0
- package/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/constants.ts +154 -19
- package/src/db/queries.ts +349 -34
- package/src/db/sqlite-adapter.ts +22 -3
- package/src/db/types.ts +60 -0
- package/src/mcp/server.ts +17 -1
- package/src/mcp/tools/claim.ts +209 -40
- package/src/mcp/tools/lsp.ts +705 -0
package/src/db/sqlite-adapter.ts
CHANGED
|
@@ -39,7 +39,8 @@ class SqlitePreparedStatement implements PreparedStatement {
|
|
|
39
39
|
|
|
40
40
|
constructor(
|
|
41
41
|
private db: Database.Database,
|
|
42
|
-
private sql: string
|
|
42
|
+
private sql: string,
|
|
43
|
+
private onWrite?: () => void
|
|
43
44
|
) {}
|
|
44
45
|
|
|
45
46
|
bind(...values: unknown[]): PreparedStatement {
|
|
@@ -65,6 +66,8 @@ class SqlitePreparedStatement implements PreparedStatement {
|
|
|
65
66
|
async run(): Promise<{ meta: { changes: number } }> {
|
|
66
67
|
const stmt = this.db.prepare(this.sql);
|
|
67
68
|
const result = stmt.run(...this.bindings);
|
|
69
|
+
// Trigger checkpoint after write
|
|
70
|
+
this.onWrite?.();
|
|
68
71
|
return {
|
|
69
72
|
meta: { changes: result.changes },
|
|
70
73
|
};
|
|
@@ -88,12 +91,25 @@ class SqliteDatabase implements DatabaseAdapter {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
this.db = new Database(dbPath);
|
|
94
|
+
|
|
95
|
+
// Multi-process SQLite configuration
|
|
91
96
|
this.db.pragma('journal_mode = WAL');
|
|
92
97
|
this.db.pragma('foreign_keys = ON');
|
|
98
|
+
this.db.pragma('busy_timeout = 5000'); // Wait up to 5s for locks
|
|
99
|
+
this.db.pragma('synchronous = NORMAL'); // Ensure durability
|
|
100
|
+
this.db.pragma('wal_autocheckpoint = 100'); // Checkpoint every 100 pages
|
|
101
|
+
|
|
102
|
+
// Checkpoint on open to see latest data from other processes
|
|
103
|
+
this.db.pragma('wal_checkpoint(PASSIVE)');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Force checkpoint to make changes visible to other processes
|
|
107
|
+
checkpoint(): void {
|
|
108
|
+
this.db.pragma('wal_checkpoint(PASSIVE)');
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
prepare(sql: string): PreparedStatement {
|
|
96
|
-
return new SqlitePreparedStatement(this.db, sql);
|
|
112
|
+
return new SqlitePreparedStatement(this.db, sql, () => this.checkpoint());
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
async batch(statements: PreparedStatement[]): Promise<QueryResult<unknown>[]> {
|
|
@@ -107,7 +123,10 @@ class SqliteDatabase implements DatabaseAdapter {
|
|
|
107
123
|
};
|
|
108
124
|
});
|
|
109
125
|
});
|
|
110
|
-
|
|
126
|
+
const results = transaction();
|
|
127
|
+
// Checkpoint after batch write
|
|
128
|
+
this.checkpoint();
|
|
129
|
+
return results;
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
// Initialize database schema
|
package/src/db/types.ts
CHANGED
|
@@ -120,6 +120,62 @@ export interface ClaimFile {
|
|
|
120
120
|
is_pattern: number; // 0 or 1, SQLite boolean
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Symbol types for fine-grained conflict detection
|
|
124
|
+
export type SymbolType = 'function' | 'class' | 'method' | 'variable' | 'block' | 'other';
|
|
125
|
+
|
|
126
|
+
export interface ClaimSymbol {
|
|
127
|
+
id: number;
|
|
128
|
+
claim_id: string;
|
|
129
|
+
file_path: string;
|
|
130
|
+
symbol_name: string;
|
|
131
|
+
symbol_type: SymbolType;
|
|
132
|
+
created_at: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Input format for claiming symbols
|
|
136
|
+
export interface SymbolClaim {
|
|
137
|
+
file: string;
|
|
138
|
+
symbols: string[];
|
|
139
|
+
symbol_type?: SymbolType;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Symbol reference for impact tracking
|
|
143
|
+
export interface SymbolReference {
|
|
144
|
+
id: number;
|
|
145
|
+
source_file: string;
|
|
146
|
+
source_symbol: string;
|
|
147
|
+
ref_file: string;
|
|
148
|
+
ref_line: number | null;
|
|
149
|
+
ref_context: string | null;
|
|
150
|
+
session_id: string;
|
|
151
|
+
created_at: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Input format for storing references
|
|
155
|
+
export interface ReferenceInput {
|
|
156
|
+
source_file: string;
|
|
157
|
+
source_symbol: string;
|
|
158
|
+
references: Array<{
|
|
159
|
+
file: string;
|
|
160
|
+
line: number;
|
|
161
|
+
context?: string;
|
|
162
|
+
}>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Impact analysis result
|
|
166
|
+
export interface ImpactInfo {
|
|
167
|
+
symbol: string;
|
|
168
|
+
file: string;
|
|
169
|
+
affected_claims: Array<{
|
|
170
|
+
claim_id: string;
|
|
171
|
+
session_name: string | null;
|
|
172
|
+
intent: string;
|
|
173
|
+
affected_symbols: string[];
|
|
174
|
+
}>;
|
|
175
|
+
reference_count: number;
|
|
176
|
+
affected_files: string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
123
179
|
export interface Message {
|
|
124
180
|
id: string;
|
|
125
181
|
from_session_id: string;
|
|
@@ -152,4 +208,8 @@ export interface ConflictInfo {
|
|
|
152
208
|
intent: string;
|
|
153
209
|
scope: ClaimScope;
|
|
154
210
|
created_at: string;
|
|
211
|
+
// Symbol-level conflict info (optional)
|
|
212
|
+
symbol_name?: string;
|
|
213
|
+
symbol_type?: SymbolType;
|
|
214
|
+
conflict_level: 'file' | 'symbol';
|
|
155
215
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// MCP Server implementation for Session Collaboration
|
|
2
|
+
// Last edited: 2025-12-29 by session-b
|
|
2
3
|
|
|
3
4
|
import type { DatabaseAdapter } from '../db/sqlite-adapter.js';
|
|
4
5
|
import {
|
|
@@ -18,6 +19,7 @@ import { sessionTools, handleSessionTool } from './tools/session';
|
|
|
18
19
|
import { claimTools, handleClaimTool } from './tools/claim';
|
|
19
20
|
import { messageTools, handleMessageTool } from './tools/message';
|
|
20
21
|
import { decisionTools, handleDecisionTool } from './tools/decision';
|
|
22
|
+
import { lspTools, handleLspTool } from './tools/lsp';
|
|
21
23
|
import type { AuthContext } from '../auth/types';
|
|
22
24
|
import { VERSION, SERVER_NAME, SERVER_INSTRUCTIONS } from '../constants.js';
|
|
23
25
|
|
|
@@ -31,7 +33,7 @@ const CAPABILITIES: McpCapabilities = {
|
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
// Combine all tools
|
|
34
|
-
const ALL_TOOLS: McpTool[] = [...sessionTools, ...claimTools, ...messageTools, ...decisionTools];
|
|
36
|
+
const ALL_TOOLS: McpTool[] = [...sessionTools, ...claimTools, ...messageTools, ...decisionTools, ...lspTools];
|
|
35
37
|
|
|
36
38
|
export class McpServer {
|
|
37
39
|
private authContext?: AuthContext;
|
|
@@ -104,6 +106,13 @@ export class McpServer {
|
|
|
104
106
|
result = await handleMessageTool(this.db, name, args);
|
|
105
107
|
} else if (name.startsWith('collab_decision_')) {
|
|
106
108
|
result = await handleDecisionTool(this.db, name, args);
|
|
109
|
+
} else if (
|
|
110
|
+
name === 'collab_analyze_symbols' ||
|
|
111
|
+
name === 'collab_validate_symbols' ||
|
|
112
|
+
name === 'collab_store_references' ||
|
|
113
|
+
name === 'collab_impact_analysis'
|
|
114
|
+
) {
|
|
115
|
+
result = await handleLspTool(this.db, name, args);
|
|
107
116
|
} else {
|
|
108
117
|
result = createToolResult(`Unknown tool: ${name}`, true);
|
|
109
118
|
}
|
|
@@ -147,6 +156,13 @@ export async function handleMcpRequest(
|
|
|
147
156
|
return await handleMessageTool(db, name, args);
|
|
148
157
|
} else if (name.startsWith('collab_decision_')) {
|
|
149
158
|
return await handleDecisionTool(db, name, args);
|
|
159
|
+
} else if (
|
|
160
|
+
name === 'collab_analyze_symbols' ||
|
|
161
|
+
name === 'collab_validate_symbols' ||
|
|
162
|
+
name === 'collab_store_references' ||
|
|
163
|
+
name === 'collab_impact_analysis'
|
|
164
|
+
) {
|
|
165
|
+
return await handleLspTool(db, name, args);
|
|
150
166
|
} else {
|
|
151
167
|
return createToolResult(`Unknown tool: ${name}`, true);
|
|
152
168
|
}
|
package/src/mcp/tools/claim.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { DatabaseAdapter } from '../../db/sqlite-adapter.js';
|
|
4
4
|
import type { McpTool, McpToolResult } from '../protocol';
|
|
5
5
|
import { createToolResult } from '../protocol';
|
|
6
|
-
import type { ClaimScope, SessionConfig } from '../../db/types';
|
|
6
|
+
import type { ClaimScope, SessionConfig, SymbolClaim } from '../../db/types';
|
|
7
7
|
import { DEFAULT_SESSION_CONFIG } from '../../db/types';
|
|
8
8
|
import { createClaim, getClaim, listClaims, checkConflicts, releaseClaim, getSession } from '../../db/queries';
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ export const claimTools: McpTool[] = [
|
|
|
11
11
|
{
|
|
12
12
|
name: 'collab_claim',
|
|
13
13
|
description:
|
|
14
|
-
'Declare files you are about to modify.
|
|
14
|
+
'Declare files or specific symbols (functions/classes) you are about to modify. Use symbols for fine-grained claims that allow other sessions to work on different parts of the same file.',
|
|
15
15
|
inputSchema: {
|
|
16
16
|
type: 'object',
|
|
17
17
|
properties: {
|
|
@@ -22,11 +22,32 @@ export const claimTools: McpTool[] = [
|
|
|
22
22
|
files: {
|
|
23
23
|
type: 'array',
|
|
24
24
|
items: { type: 'string' },
|
|
25
|
-
description: "File paths to claim. Supports glob patterns like 'src/api/*'",
|
|
25
|
+
description: "File paths to claim. Supports glob patterns like 'src/api/*'. Use this for whole-file claims.",
|
|
26
|
+
},
|
|
27
|
+
symbols: {
|
|
28
|
+
type: 'array',
|
|
29
|
+
items: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
file: { type: 'string', description: 'File path containing the symbols' },
|
|
33
|
+
symbols: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
description: 'Symbol names (function, class, method names) to claim',
|
|
37
|
+
},
|
|
38
|
+
symbol_type: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
enum: ['function', 'class', 'method', 'variable', 'block', 'other'],
|
|
41
|
+
description: 'Type of symbols being claimed (default: function)',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ['file', 'symbols'],
|
|
45
|
+
},
|
|
46
|
+
description: 'Symbol-level claims for fine-grained conflict detection. Use this instead of files when you only need to modify specific functions/classes.',
|
|
26
47
|
},
|
|
27
48
|
intent: {
|
|
28
49
|
type: 'string',
|
|
29
|
-
description: 'What you plan to do with these files',
|
|
50
|
+
description: 'What you plan to do with these files/symbols',
|
|
30
51
|
},
|
|
31
52
|
scope: {
|
|
32
53
|
type: 'string',
|
|
@@ -34,13 +55,13 @@ export const claimTools: McpTool[] = [
|
|
|
34
55
|
description: 'Estimated scope: small(<30min), medium(30min-2hr), large(>2hr)',
|
|
35
56
|
},
|
|
36
57
|
},
|
|
37
|
-
required: ['session_id', '
|
|
58
|
+
required: ['session_id', 'intent'],
|
|
38
59
|
},
|
|
39
60
|
},
|
|
40
61
|
{
|
|
41
62
|
name: 'collab_check',
|
|
42
63
|
description:
|
|
43
|
-
'Check if files are being worked on by other sessions. ALWAYS call this before
|
|
64
|
+
'Check if files or symbols are being worked on by other sessions. ALWAYS call this before modifying files. Supports symbol-level checking for fine-grained conflict detection.',
|
|
44
65
|
inputSchema: {
|
|
45
66
|
type: 'object',
|
|
46
67
|
properties: {
|
|
@@ -49,6 +70,22 @@ export const claimTools: McpTool[] = [
|
|
|
49
70
|
items: { type: 'string' },
|
|
50
71
|
description: 'File paths to check',
|
|
51
72
|
},
|
|
73
|
+
symbols: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
items: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
file: { type: 'string', description: 'File path containing the symbols' },
|
|
79
|
+
symbols: {
|
|
80
|
+
type: 'array',
|
|
81
|
+
items: { type: 'string' },
|
|
82
|
+
description: 'Symbol names to check for conflicts',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['file', 'symbols'],
|
|
86
|
+
},
|
|
87
|
+
description: 'Symbol-level check. If provided, only checks for conflicts with these specific symbols.',
|
|
88
|
+
},
|
|
52
89
|
session_id: {
|
|
53
90
|
type: 'string',
|
|
54
91
|
description: 'Your session ID (to exclude your own claims from results)',
|
|
@@ -121,6 +158,7 @@ export async function handleClaimTool(
|
|
|
121
158
|
case 'collab_claim': {
|
|
122
159
|
const sessionId = args.session_id as string | undefined;
|
|
123
160
|
const files = args.files as string[] | undefined;
|
|
161
|
+
const symbols = args.symbols as SymbolClaim[] | undefined;
|
|
124
162
|
const intent = args.intent as string | undefined;
|
|
125
163
|
const scope = (args.scope as ClaimScope) ?? 'medium';
|
|
126
164
|
|
|
@@ -131,12 +169,18 @@ export async function handleClaimTool(
|
|
|
131
169
|
true
|
|
132
170
|
);
|
|
133
171
|
}
|
|
134
|
-
|
|
172
|
+
|
|
173
|
+
// Either files or symbols must be provided
|
|
174
|
+
const hasFiles = files && Array.isArray(files) && files.length > 0;
|
|
175
|
+
const hasSymbols = symbols && Array.isArray(symbols) && symbols.length > 0;
|
|
176
|
+
|
|
177
|
+
if (!hasFiles && !hasSymbols) {
|
|
135
178
|
return createToolResult(
|
|
136
|
-
JSON.stringify({ error: 'INVALID_INPUT', message: 'files
|
|
179
|
+
JSON.stringify({ error: 'INVALID_INPUT', message: 'Either files or symbols must be provided' }),
|
|
137
180
|
true
|
|
138
181
|
);
|
|
139
182
|
}
|
|
183
|
+
|
|
140
184
|
if (!intent || typeof intent !== 'string' || intent.trim() === '') {
|
|
141
185
|
return createToolResult(
|
|
142
186
|
JSON.stringify({ error: 'INVALID_INPUT', message: 'intent is required' }),
|
|
@@ -156,41 +200,56 @@ export async function handleClaimTool(
|
|
|
156
200
|
);
|
|
157
201
|
}
|
|
158
202
|
|
|
203
|
+
// Build file list from both files and symbols
|
|
204
|
+
const allFiles = new Set<string>(files ?? []);
|
|
205
|
+
if (hasSymbols) {
|
|
206
|
+
for (const sc of symbols!) {
|
|
207
|
+
allFiles.add(sc.file);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const fileList = Array.from(allFiles);
|
|
211
|
+
|
|
159
212
|
// Check for conflicts before creating claim
|
|
160
|
-
const conflicts = await checkConflicts(db,
|
|
213
|
+
const conflicts = await checkConflicts(db, fileList, sessionId, symbols);
|
|
161
214
|
|
|
162
215
|
// Create the claim
|
|
163
216
|
const { claim } = await createClaim(db, {
|
|
164
217
|
session_id: sessionId,
|
|
165
|
-
files,
|
|
218
|
+
files: fileList,
|
|
166
219
|
intent,
|
|
167
220
|
scope,
|
|
221
|
+
symbols: hasSymbols ? symbols : undefined,
|
|
168
222
|
});
|
|
169
223
|
|
|
170
224
|
if (conflicts.length > 0) {
|
|
171
|
-
// Group conflicts by
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
225
|
+
// Group conflicts by type (file vs symbol)
|
|
226
|
+
const fileConflicts = conflicts.filter((c) => c.conflict_level === 'file');
|
|
227
|
+
const symbolConflicts = conflicts.filter((c) => c.conflict_level === 'symbol');
|
|
228
|
+
|
|
229
|
+
const conflictDetails = {
|
|
230
|
+
file_level: fileConflicts.map((c) => ({
|
|
231
|
+
session_name: c.session_name,
|
|
232
|
+
file: c.file_path,
|
|
233
|
+
intent: c.intent,
|
|
234
|
+
})),
|
|
235
|
+
symbol_level: symbolConflicts.map((c) => ({
|
|
236
|
+
session_name: c.session_name,
|
|
237
|
+
file: c.file_path,
|
|
238
|
+
symbol: c.symbol_name,
|
|
239
|
+
symbol_type: c.symbol_type,
|
|
240
|
+
intent: c.intent,
|
|
241
|
+
})),
|
|
242
|
+
};
|
|
186
243
|
|
|
187
244
|
return createToolResult(
|
|
188
245
|
JSON.stringify(
|
|
189
246
|
{
|
|
190
247
|
claim_id: claim.id,
|
|
191
248
|
status: 'created_with_conflicts',
|
|
249
|
+
files: fileList,
|
|
250
|
+
symbols: hasSymbols ? symbols : undefined,
|
|
192
251
|
conflicts: conflictDetails,
|
|
193
|
-
warning: `⚠️ ${
|
|
252
|
+
warning: `⚠️ Conflicts detected: ${fileConflicts.length} file-level, ${symbolConflicts.length} symbol-level. Coordinate before proceeding.`,
|
|
194
253
|
},
|
|
195
254
|
null,
|
|
196
255
|
2
|
|
@@ -202,16 +261,20 @@ export async function handleClaimTool(
|
|
|
202
261
|
JSON.stringify({
|
|
203
262
|
claim_id: claim.id,
|
|
204
263
|
status: 'created',
|
|
205
|
-
files,
|
|
264
|
+
files: fileList,
|
|
265
|
+
symbols: hasSymbols ? symbols : undefined,
|
|
206
266
|
intent,
|
|
207
267
|
scope,
|
|
208
|
-
message:
|
|
268
|
+
message: hasSymbols
|
|
269
|
+
? 'Symbol-level claim created. Other sessions can work on different symbols in the same file.'
|
|
270
|
+
: 'Claim created successfully. Other sessions will be warned about these files.',
|
|
209
271
|
})
|
|
210
272
|
);
|
|
211
273
|
}
|
|
212
274
|
|
|
213
275
|
case 'collab_check': {
|
|
214
276
|
const files = args.files as string[] | undefined;
|
|
277
|
+
const symbols = args.symbols as SymbolClaim[] | undefined;
|
|
215
278
|
const sessionId = args.session_id as string | undefined;
|
|
216
279
|
|
|
217
280
|
// Input validation
|
|
@@ -246,21 +309,89 @@ export async function handleClaimTool(
|
|
|
246
309
|
}
|
|
247
310
|
}
|
|
248
311
|
|
|
249
|
-
const
|
|
312
|
+
const hasSymbols = symbols && Array.isArray(symbols) && symbols.length > 0;
|
|
313
|
+
const conflicts = await checkConflicts(db, files, sessionId, hasSymbols ? symbols : undefined);
|
|
314
|
+
|
|
315
|
+
// Separate file-level and symbol-level conflicts
|
|
316
|
+
const fileConflicts = conflicts.filter((c) => c.conflict_level === 'file');
|
|
317
|
+
const symbolConflicts = conflicts.filter((c) => c.conflict_level === 'symbol');
|
|
318
|
+
|
|
319
|
+
// Build per-file status (considering both file and symbol conflicts)
|
|
320
|
+
const blockedFiles = new Set<string>();
|
|
321
|
+
const blockedSymbols = new Map<string, Set<string>>(); // file -> symbols
|
|
322
|
+
|
|
323
|
+
for (const c of conflicts) {
|
|
324
|
+
if (c.conflict_level === 'file') {
|
|
325
|
+
blockedFiles.add(c.file_path);
|
|
326
|
+
} else if (c.conflict_level === 'symbol' && c.symbol_name) {
|
|
327
|
+
const existing = blockedSymbols.get(c.file_path) ?? new Set();
|
|
328
|
+
existing.add(c.symbol_name);
|
|
329
|
+
blockedSymbols.set(c.file_path, existing);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// For symbol-level check, determine safe symbols
|
|
334
|
+
let safeSymbols: Array<{ file: string; symbols: string[] }> = [];
|
|
335
|
+
let blockedSymbolsList: Array<{ file: string; symbols: string[] }> = [];
|
|
336
|
+
|
|
337
|
+
if (hasSymbols) {
|
|
338
|
+
for (const sc of symbols!) {
|
|
339
|
+
const blocked = blockedSymbols.get(sc.file) ?? new Set();
|
|
340
|
+
const safe = sc.symbols.filter((s) => !blocked.has(s));
|
|
341
|
+
const blockedList = sc.symbols.filter((s) => blocked.has(s));
|
|
342
|
+
|
|
343
|
+
if (safe.length > 0) {
|
|
344
|
+
safeSymbols.push({ file: sc.file, symbols: safe });
|
|
345
|
+
}
|
|
346
|
+
if (blockedList.length > 0) {
|
|
347
|
+
blockedSymbolsList.push({ file: sc.file, symbols: blockedList });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const safeFiles = files.filter((f) => !blockedFiles.has(f) && !blockedSymbols.has(f));
|
|
353
|
+
const blockedFilesList = files.filter((f) => blockedFiles.has(f));
|
|
354
|
+
|
|
355
|
+
// Determine recommendation for Claude's auto-decision
|
|
356
|
+
type Recommendation = 'proceed_all' | 'proceed_safe_only' | 'abort';
|
|
357
|
+
let recommendation: Recommendation;
|
|
358
|
+
let canEdit: boolean;
|
|
359
|
+
|
|
360
|
+
const hasSafeContent = safeFiles.length > 0 || safeSymbols.length > 0;
|
|
361
|
+
|
|
362
|
+
if (conflicts.length === 0) {
|
|
363
|
+
recommendation = 'proceed_all';
|
|
364
|
+
canEdit = true;
|
|
365
|
+
} else if (hasSafeContent) {
|
|
366
|
+
recommendation = 'proceed_safe_only';
|
|
367
|
+
canEdit = true;
|
|
368
|
+
} else {
|
|
369
|
+
recommendation = 'abort';
|
|
370
|
+
canEdit = false;
|
|
371
|
+
}
|
|
250
372
|
|
|
251
373
|
if (conflicts.length === 0) {
|
|
252
374
|
return createToolResult(
|
|
253
375
|
JSON.stringify({
|
|
254
376
|
has_conflicts: false,
|
|
255
377
|
safe: true,
|
|
256
|
-
|
|
378
|
+
can_edit: true,
|
|
379
|
+
recommendation: 'proceed_all',
|
|
380
|
+
file_status: {
|
|
381
|
+
safe: files,
|
|
382
|
+
blocked: [],
|
|
383
|
+
},
|
|
384
|
+
symbol_status: hasSymbols ? { safe: symbols, blocked: [] } : undefined,
|
|
385
|
+
message: hasSymbols
|
|
386
|
+
? 'All symbols are safe to edit. Proceed.'
|
|
387
|
+
: 'All files are safe to edit. Proceed.',
|
|
257
388
|
has_in_progress_todo: hasInProgressTodo,
|
|
258
389
|
todos_status: todosStatus,
|
|
259
390
|
})
|
|
260
391
|
);
|
|
261
392
|
}
|
|
262
393
|
|
|
263
|
-
// Group by session for clearer output
|
|
394
|
+
// Group conflicts by session for clearer output
|
|
264
395
|
const bySession = new Map<string, typeof conflicts>();
|
|
265
396
|
for (const c of conflicts) {
|
|
266
397
|
const key = c.session_id;
|
|
@@ -269,22 +400,60 @@ export async function handleClaimTool(
|
|
|
269
400
|
bySession.set(key, existing);
|
|
270
401
|
}
|
|
271
402
|
|
|
272
|
-
const conflictDetails = Array.from(bySession.entries()).map(([sessId, items]) =>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
403
|
+
const conflictDetails = Array.from(bySession.entries()).map(([sessId, items]) => {
|
|
404
|
+
const fileItems = items.filter((i) => i.conflict_level === 'file');
|
|
405
|
+
const symbolItems = items.filter((i) => i.conflict_level === 'symbol');
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
session_id: sessId,
|
|
409
|
+
session_name: items[0].session_name,
|
|
410
|
+
intent: items[0].intent,
|
|
411
|
+
scope: items[0].scope,
|
|
412
|
+
files: fileItems.map((i) => i.file_path),
|
|
413
|
+
symbols: symbolItems.map((i) => ({
|
|
414
|
+
file: i.file_path,
|
|
415
|
+
symbol: i.symbol_name,
|
|
416
|
+
type: i.symbol_type,
|
|
417
|
+
})),
|
|
418
|
+
started_at: items[0].created_at,
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Build actionable message based on recommendation
|
|
423
|
+
let message: string;
|
|
424
|
+
if (recommendation === 'proceed_safe_only') {
|
|
425
|
+
if (hasSymbols && safeSymbols.length > 0) {
|
|
426
|
+
const safeDesc = safeSymbols.map((s) => `${s.file}:[${s.symbols.join(',')}]`).join(', ');
|
|
427
|
+
const blockedDesc = blockedSymbolsList.map((s) => `${s.file}:[${s.symbols.join(',')}]`).join(', ');
|
|
428
|
+
message = `Edit ONLY these safe symbols: ${safeDesc}. Skip blocked: ${blockedDesc}.`;
|
|
429
|
+
} else {
|
|
430
|
+
message = `Edit ONLY these safe files: [${safeFiles.join(', ')}]. Skip blocked files: [${blockedFilesList.join(', ')}].`;
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
message = hasSymbols
|
|
434
|
+
? `All requested symbols are blocked. Coordinate with other session(s) or wait.`
|
|
435
|
+
: `All ${files.length} file(s) are blocked. Coordinate with other session(s) or wait.`;
|
|
436
|
+
}
|
|
280
437
|
|
|
281
438
|
return createToolResult(
|
|
282
439
|
JSON.stringify(
|
|
283
440
|
{
|
|
284
441
|
has_conflicts: true,
|
|
285
442
|
safe: false,
|
|
443
|
+
can_edit: canEdit,
|
|
444
|
+
recommendation,
|
|
445
|
+
file_status: {
|
|
446
|
+
safe: safeFiles,
|
|
447
|
+
blocked: blockedFilesList,
|
|
448
|
+
},
|
|
449
|
+
symbol_status: hasSymbols
|
|
450
|
+
? {
|
|
451
|
+
safe: safeSymbols,
|
|
452
|
+
blocked: blockedSymbolsList,
|
|
453
|
+
}
|
|
454
|
+
: undefined,
|
|
286
455
|
conflicts: conflictDetails,
|
|
287
|
-
|
|
456
|
+
message,
|
|
288
457
|
has_in_progress_todo: hasInProgressTodo,
|
|
289
458
|
todos_status: todosStatus,
|
|
290
459
|
},
|