@ebowwa/coder 0.7.64 → 0.7.65
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/dist/index.js +36168 -32
- package/dist/interfaces/ui/terminal/cli/index.js +34253 -158
- package/dist/interfaces/ui/terminal/native/README.md +53 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
- package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/interfaces/ui/terminal/native/index.js +43 -0
- package/dist/interfaces/ui/terminal/native/index.node +0 -0
- package/dist/interfaces/ui/terminal/native/package.json +34 -0
- package/dist/native/README.md +53 -0
- package/dist/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/native/claude_code_native.dylib +0 -0
- package/dist/native/index.d.ts +0 -480
- package/dist/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.js +43 -1625
- package/dist/native/index.node +0 -0
- package/dist/native/package.json +34 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +33 -19
- package/package.json +3 -2
- package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
- package/packages/src/core/agent-loop/compaction.ts +6 -2
- package/packages/src/core/agent-loop/index.ts +2 -0
- package/packages/src/core/agent-loop/loop-state.ts +1 -1
- package/packages/src/core/agent-loop/turn-executor.ts +4 -0
- package/packages/src/core/agent-loop/types.ts +4 -0
- package/packages/src/core/api-client-impl.ts +283 -173
- package/packages/src/core/cognitive-security/hooks.ts +2 -1
- package/packages/src/core/config/todo +7 -0
- package/packages/src/core/context/__tests__/integration.test.ts +334 -0
- package/packages/src/core/context/compaction.ts +170 -0
- package/packages/src/core/context/constants.ts +58 -0
- package/packages/src/core/context/extraction.ts +85 -0
- package/packages/src/core/context/index.ts +66 -0
- package/packages/src/core/context/summarization.ts +251 -0
- package/packages/src/core/context/token-estimation.ts +98 -0
- package/packages/src/core/context/types.ts +59 -0
- package/packages/src/core/models.ts +81 -4
- package/packages/src/core/normalizers/todo +5 -1
- package/packages/src/core/providers/README.md +230 -0
- package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
- package/packages/src/core/providers/index.ts +419 -0
- package/packages/src/core/providers/types.ts +132 -0
- package/packages/src/core/retry.ts +10 -0
- package/packages/src/ecosystem/tools/index.ts +174 -0
- package/packages/src/index.ts +23 -2
- package/packages/src/interfaces/ui/index.ts +17 -20
- package/packages/src/interfaces/ui/spinner.ts +2 -2
- package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
- package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
- package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
- package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
- package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
- package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
- package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +393 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
- package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
- package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
- package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
- package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
- package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
- package/packages/src/native/index.ts +404 -27
- package/packages/src/native/tui_v2_types.ts +39 -0
- package/packages/src/teammates/coordination.test.ts +279 -0
- package/packages/src/teammates/coordination.ts +646 -0
- package/packages/src/teammates/index.ts +95 -25
- package/packages/src/teammates/integration.test.ts +272 -0
- package/packages/src/teammates/runner.test.ts +235 -0
- package/packages/src/teammates/runner.ts +750 -0
- package/packages/src/teammates/schemas.ts +673 -0
- package/packages/src/types/index.ts +1 -0
- package/packages/src/core/context-compaction.ts +0 -578
- package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
- package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
- package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
- package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
- package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
- package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
- package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
- package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
- package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
- package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
- package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
- package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
- package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
- package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
- package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
- package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
- package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
- package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
- package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
- package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
- package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
- package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
- package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
- package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination Callbacks - Inter-agent communication and progress reporting
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Progress reporting callbacks
|
|
6
|
+
* - File locking/claiming
|
|
7
|
+
* - Status change notifications
|
|
8
|
+
* - Heartbeat system
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { TeammateManager } from "./index.js";
|
|
12
|
+
import type { Teammate, TeammateStatus, TeammateMessage } from "../types/index.js";
|
|
13
|
+
import { writeFileSync, existsSync, rmSync, readFileSync, mkdirSync, readdirSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// TYPES
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
export type CoordinationEventType =
|
|
21
|
+
| "progress"
|
|
22
|
+
| "status_change"
|
|
23
|
+
| "file_claim"
|
|
24
|
+
| "file_release"
|
|
25
|
+
| "heartbeat"
|
|
26
|
+
| "task_start"
|
|
27
|
+
| "task_progress"
|
|
28
|
+
| "task_complete"
|
|
29
|
+
| "task_failed"
|
|
30
|
+
| "blocked"
|
|
31
|
+
| "unblocked";
|
|
32
|
+
|
|
33
|
+
export interface CoordinationEvent {
|
|
34
|
+
type: CoordinationEventType;
|
|
35
|
+
teammateId: string;
|
|
36
|
+
teammateName: string;
|
|
37
|
+
teamName: string;
|
|
38
|
+
timestamp: number;
|
|
39
|
+
payload: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ProgressReport {
|
|
43
|
+
/** Current step description */
|
|
44
|
+
step: string;
|
|
45
|
+
/** Current step number */
|
|
46
|
+
stepNumber: number;
|
|
47
|
+
/** Total steps (if known) */
|
|
48
|
+
totalSteps?: number;
|
|
49
|
+
/** Percentage complete (0-100) */
|
|
50
|
+
percentage?: number;
|
|
51
|
+
/** Files being worked on */
|
|
52
|
+
files?: string[];
|
|
53
|
+
/** Any blockers encountered */
|
|
54
|
+
blockers?: string[];
|
|
55
|
+
/** ETA in seconds */
|
|
56
|
+
eta?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FileClaim {
|
|
60
|
+
/** File path being claimed */
|
|
61
|
+
filePath: string;
|
|
62
|
+
/** Teammate ID claiming the file */
|
|
63
|
+
teammateId: string;
|
|
64
|
+
/** Teammate name */
|
|
65
|
+
teammateName: string;
|
|
66
|
+
/** When the claim was made */
|
|
67
|
+
claimedAt: number;
|
|
68
|
+
/** Optional expiration time */
|
|
69
|
+
expiresAt?: number;
|
|
70
|
+
/** Reason for claim */
|
|
71
|
+
reason?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type CoordinationCallback = (event: CoordinationEvent) => void;
|
|
75
|
+
|
|
76
|
+
export interface CoordinationConfig {
|
|
77
|
+
/** Enable progress reporting */
|
|
78
|
+
enableProgressReporting?: boolean;
|
|
79
|
+
/** Enable file locking */
|
|
80
|
+
enableFileLocking?: boolean;
|
|
81
|
+
/** Enable heartbeat */
|
|
82
|
+
enableHeartbeat?: boolean;
|
|
83
|
+
/** Heartbeat interval in ms (default: 30000) */
|
|
84
|
+
heartbeatInterval?: number;
|
|
85
|
+
/** Callback for coordination events */
|
|
86
|
+
onCoordinationEvent?: CoordinationCallback;
|
|
87
|
+
/** Callback for status changes from other teammates */
|
|
88
|
+
onStatusChange?: (teammateId: string, oldStatus: TeammateStatus, newStatus: TeammateStatus) => void;
|
|
89
|
+
/** Callback when a teammate reports progress */
|
|
90
|
+
onProgress?: (teammateId: string, progress: ProgressReport) => void;
|
|
91
|
+
/** Callback when a file is claimed */
|
|
92
|
+
onFileClaimed?: (claim: FileClaim) => void;
|
|
93
|
+
/** Callback when a file is released */
|
|
94
|
+
onFileReleased?: (filePath: string, teammateId: string) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// COORDINATION MANAGER
|
|
99
|
+
// ============================================
|
|
100
|
+
|
|
101
|
+
export class CoordinationManager {
|
|
102
|
+
private manager: TeammateManager;
|
|
103
|
+
private storagePath: string;
|
|
104
|
+
private claimsPath: string;
|
|
105
|
+
private claims = new Map<string, FileClaim>();
|
|
106
|
+
private heartbeatTimer: Timer | null = null;
|
|
107
|
+
private config: CoordinationConfig;
|
|
108
|
+
private teammateId: string | null = null;
|
|
109
|
+
private teamName: string | null = null;
|
|
110
|
+
|
|
111
|
+
constructor(manager: TeammateManager, config: CoordinationConfig = {}) {
|
|
112
|
+
this.manager = manager;
|
|
113
|
+
this.config = config;
|
|
114
|
+
this.storagePath = join(process.env.HOME || "", ".claude", "teams");
|
|
115
|
+
this.claimsPath = join(this.storagePath, "_coordination", "claims");
|
|
116
|
+
this.ensureClaimsDirectory();
|
|
117
|
+
this.loadExistingClaims();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private ensureClaimsDirectory(): void {
|
|
121
|
+
if (!existsSync(this.claimsPath)) {
|
|
122
|
+
mkdirSync(this.claimsPath, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private loadExistingClaims(): void {
|
|
127
|
+
this.reloadClaims();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reload claims from disk - useful for synchronizing between instances
|
|
132
|
+
*/
|
|
133
|
+
reloadClaims(): void {
|
|
134
|
+
if (!existsSync(this.claimsPath)) return;
|
|
135
|
+
|
|
136
|
+
// Clear current claims (we'll reload from disk)
|
|
137
|
+
this.claims.clear();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const files = readdirSync(this.claimsPath).filter(f => f.endsWith(".json"));
|
|
141
|
+
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
try {
|
|
144
|
+
const content = readFileSync(join(this.claimsPath, file), "utf-8");
|
|
145
|
+
const claim = JSON.parse(content) as FileClaim;
|
|
146
|
+
|
|
147
|
+
// Check if claim has expired
|
|
148
|
+
if (claim.expiresAt && claim.expiresAt < Date.now()) {
|
|
149
|
+
this.deleteClaimFile(claim.filePath);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.claims.set(this.normalizePath(claim.filePath), claim);
|
|
154
|
+
} catch {
|
|
155
|
+
// Skip malformed claim files
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Directory may not exist
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private normalizePath(filePath: string): string {
|
|
164
|
+
return filePath.replace(/\\/g, "/").toLowerCase();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private getClaimFilePath(filePath: string): string {
|
|
168
|
+
const normalized = this.normalizePath(filePath);
|
|
169
|
+
const hash = this.hashPath(normalized);
|
|
170
|
+
return join(this.claimsPath, `${hash}.json`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private hashPath(path: string): string {
|
|
174
|
+
// Simple hash for file path
|
|
175
|
+
let hash = 0;
|
|
176
|
+
for (let i = 0; i < path.length; i++) {
|
|
177
|
+
const char = path.charCodeAt(i);
|
|
178
|
+
hash = ((hash << 5) - hash) + char;
|
|
179
|
+
hash = hash & hash;
|
|
180
|
+
}
|
|
181
|
+
return Math.abs(hash).toString(36);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private deleteClaimFile(filePath: string): void {
|
|
185
|
+
const claimPath = this.getClaimFilePath(filePath);
|
|
186
|
+
if (existsSync(claimPath)) {
|
|
187
|
+
rmSync(claimPath);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================
|
|
192
|
+
// INITIALIZATION
|
|
193
|
+
// ============================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Initialize coordination for a teammate
|
|
197
|
+
*/
|
|
198
|
+
initialize(teammateId: string, teamName: string): void {
|
|
199
|
+
this.teammateId = teammateId;
|
|
200
|
+
this.teamName = teamName;
|
|
201
|
+
|
|
202
|
+
if (this.config.enableHeartbeat) {
|
|
203
|
+
this.startHeartbeat();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Shutdown coordination
|
|
209
|
+
*/
|
|
210
|
+
shutdown(): void {
|
|
211
|
+
if (this.heartbeatTimer) {
|
|
212
|
+
clearInterval(this.heartbeatTimer);
|
|
213
|
+
this.heartbeatTimer = null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Release all claims
|
|
217
|
+
if (this.teammateId) {
|
|
218
|
+
this.releaseAllClaims();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================
|
|
223
|
+
// HEARTBEAT
|
|
224
|
+
// ============================================
|
|
225
|
+
|
|
226
|
+
private startHeartbeat(): void {
|
|
227
|
+
const interval = this.config.heartbeatInterval || 30000;
|
|
228
|
+
|
|
229
|
+
this.heartbeatTimer = setInterval(() => {
|
|
230
|
+
this.sendHeartbeat();
|
|
231
|
+
}, interval);
|
|
232
|
+
|
|
233
|
+
// Send initial heartbeat
|
|
234
|
+
this.sendHeartbeat();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private sendHeartbeat(): void {
|
|
238
|
+
if (!this.teammateId || !this.teamName) return;
|
|
239
|
+
|
|
240
|
+
this.emitEvent({
|
|
241
|
+
type: "heartbeat",
|
|
242
|
+
teammateId: this.teammateId,
|
|
243
|
+
teammateName: this.getTeammateName(),
|
|
244
|
+
teamName: this.teamName,
|
|
245
|
+
timestamp: Date.now(),
|
|
246
|
+
payload: {},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================
|
|
251
|
+
// PROGRESS REPORTING
|
|
252
|
+
// ============================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Report progress to the team
|
|
256
|
+
*/
|
|
257
|
+
reportProgress(progress: ProgressReport): void {
|
|
258
|
+
if (!this.teammateId || !this.teamName) return;
|
|
259
|
+
|
|
260
|
+
this.emitEvent({
|
|
261
|
+
type: "task_progress",
|
|
262
|
+
teammateId: this.teammateId,
|
|
263
|
+
teammateName: this.getTeammateName(),
|
|
264
|
+
teamName: this.teamName,
|
|
265
|
+
timestamp: Date.now(),
|
|
266
|
+
payload: { progress },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Broadcast progress message to teammates
|
|
270
|
+
this.manager.broadcast(
|
|
271
|
+
this.teamName,
|
|
272
|
+
`[PROGRESS] ${progress.step} (${progress.stepNumber}${progress.totalSteps ? `/${progress.totalSteps}` : ""})${progress.percentage ? ` - ${progress.percentage}%` : ""}`,
|
|
273
|
+
this.teammateId
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Report task start
|
|
279
|
+
*/
|
|
280
|
+
reportTaskStart(taskId: string, taskDescription: string): void {
|
|
281
|
+
if (!this.teammateId || !this.teamName) return;
|
|
282
|
+
|
|
283
|
+
this.emitEvent({
|
|
284
|
+
type: "task_start",
|
|
285
|
+
teammateId: this.teammateId,
|
|
286
|
+
teammateName: this.getTeammateName(),
|
|
287
|
+
teamName: this.teamName,
|
|
288
|
+
timestamp: Date.now(),
|
|
289
|
+
payload: { taskId, taskDescription },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
this.manager.broadcast(
|
|
293
|
+
this.teamName,
|
|
294
|
+
`[STARTED] ${taskDescription} (${taskId})`,
|
|
295
|
+
this.teammateId
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Report that you're blocked
|
|
301
|
+
*/
|
|
302
|
+
reportBlocked(reason: string, blockedBy?: string): void {
|
|
303
|
+
if (!this.teammateId || !this.teamName) return;
|
|
304
|
+
|
|
305
|
+
this.emitEvent({
|
|
306
|
+
type: "blocked",
|
|
307
|
+
teammateId: this.teammateId,
|
|
308
|
+
teammateName: this.getTeammateName(),
|
|
309
|
+
teamName: this.teamName,
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
payload: { reason, blockedBy },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this.manager.broadcast(
|
|
315
|
+
this.teamName,
|
|
316
|
+
`[BLOCKED] ${reason}${blockedBy ? ` (waiting on: ${blockedBy})` : ""}`,
|
|
317
|
+
this.teammateId
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Report that you're unblocked
|
|
323
|
+
*/
|
|
324
|
+
reportUnblocked(): void {
|
|
325
|
+
if (!this.teammateId || !this.teamName) return;
|
|
326
|
+
|
|
327
|
+
this.emitEvent({
|
|
328
|
+
type: "unblocked",
|
|
329
|
+
teammateId: this.teammateId,
|
|
330
|
+
teammateName: this.getTeammateName(),
|
|
331
|
+
teamName: this.teamName,
|
|
332
|
+
timestamp: Date.now(),
|
|
333
|
+
payload: {},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
this.manager.broadcast(
|
|
337
|
+
this.teamName,
|
|
338
|
+
`[UNBLOCKED] Resuming work`,
|
|
339
|
+
this.teammateId
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ============================================
|
|
344
|
+
// FILE LOCKING
|
|
345
|
+
// ============================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Check if a specific file is claimed on disk (by another teammate)
|
|
349
|
+
*/
|
|
350
|
+
private checkClaimOnDisk(filePath: string): FileClaim | null {
|
|
351
|
+
const claimPath = this.getClaimFilePath(filePath);
|
|
352
|
+
if (!existsSync(claimPath)) return null;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const content = readFileSync(claimPath, "utf-8");
|
|
356
|
+
const claim = JSON.parse(content) as FileClaim;
|
|
357
|
+
|
|
358
|
+
// Check if expired
|
|
359
|
+
if (claim.expiresAt && claim.expiresAt < Date.now()) {
|
|
360
|
+
this.deleteClaimFile(filePath);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Only return if it's claimed by someone else
|
|
365
|
+
if (claim.teammateId !== this.teammateId) {
|
|
366
|
+
return claim;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Claim exclusive access to a file
|
|
376
|
+
*/
|
|
377
|
+
claimFile(filePath: string, reason?: string, expiresIn?: number): boolean {
|
|
378
|
+
if (!this.config.enableFileLocking) return true;
|
|
379
|
+
if (!this.teammateId) return false;
|
|
380
|
+
|
|
381
|
+
const normalized = this.normalizePath(filePath);
|
|
382
|
+
|
|
383
|
+
// Check in-memory claims first
|
|
384
|
+
const existing = this.claims.get(normalized);
|
|
385
|
+
|
|
386
|
+
// Check if already claimed by someone else in memory
|
|
387
|
+
if (existing && existing.teammateId !== this.teammateId) {
|
|
388
|
+
// Check if expired
|
|
389
|
+
if (existing.expiresAt && existing.expiresAt < Date.now()) {
|
|
390
|
+
this.claims.delete(normalized);
|
|
391
|
+
this.deleteClaimFile(filePath);
|
|
392
|
+
} else {
|
|
393
|
+
return false; // Claimed by another
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Also check on disk in case another process claimed it
|
|
398
|
+
const diskClaim = this.checkClaimOnDisk(filePath);
|
|
399
|
+
if (diskClaim) {
|
|
400
|
+
// Update our memory with the disk claim
|
|
401
|
+
this.claims.set(normalized, diskClaim);
|
|
402
|
+
return false; // Claimed by another on disk
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const claim: FileClaim = {
|
|
406
|
+
filePath,
|
|
407
|
+
teammateId: this.teammateId,
|
|
408
|
+
teammateName: this.getTeammateName(),
|
|
409
|
+
claimedAt: Date.now(),
|
|
410
|
+
expiresAt: expiresIn ? Date.now() + expiresIn : undefined,
|
|
411
|
+
reason,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
this.claims.set(normalized, claim);
|
|
415
|
+
|
|
416
|
+
// Persist claim
|
|
417
|
+
const claimPath = this.getClaimFilePath(filePath);
|
|
418
|
+
writeFileSync(claimPath, JSON.stringify(claim, null, 2));
|
|
419
|
+
|
|
420
|
+
this.emitEvent({
|
|
421
|
+
type: "file_claim",
|
|
422
|
+
teammateId: this.teammateId,
|
|
423
|
+
teammateName: this.getTeammateName(),
|
|
424
|
+
teamName: this.teamName || "",
|
|
425
|
+
timestamp: Date.now(),
|
|
426
|
+
payload: { claim },
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Release a file claim
|
|
434
|
+
*/
|
|
435
|
+
releaseFile(filePath: string): void {
|
|
436
|
+
if (!this.teammateId) return;
|
|
437
|
+
|
|
438
|
+
const normalized = this.normalizePath(filePath);
|
|
439
|
+
const claim = this.claims.get(normalized);
|
|
440
|
+
|
|
441
|
+
if (claim && claim.teammateId === this.teammateId) {
|
|
442
|
+
this.claims.delete(normalized);
|
|
443
|
+
this.deleteClaimFile(filePath);
|
|
444
|
+
|
|
445
|
+
this.emitEvent({
|
|
446
|
+
type: "file_release",
|
|
447
|
+
teammateId: this.teammateId,
|
|
448
|
+
teammateName: this.getTeammateName(),
|
|
449
|
+
teamName: this.teamName || "",
|
|
450
|
+
timestamp: Date.now(),
|
|
451
|
+
payload: { filePath },
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Release all file claims for this teammate
|
|
458
|
+
*/
|
|
459
|
+
releaseAllClaims(): void {
|
|
460
|
+
if (!this.teammateId) return;
|
|
461
|
+
|
|
462
|
+
const myClaims = Array.from(this.claims.entries())
|
|
463
|
+
.filter(([_, claim]) => claim.teammateId === this.teammateId);
|
|
464
|
+
|
|
465
|
+
for (const [normalized, _] of myClaims) {
|
|
466
|
+
this.claims.delete(normalized);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Clean up claim files on disk
|
|
470
|
+
try {
|
|
471
|
+
const files = readdirSync(this.claimsPath).filter(f => f.endsWith(".json"));
|
|
472
|
+
|
|
473
|
+
for (const file of files) {
|
|
474
|
+
try {
|
|
475
|
+
const content = readFileSync(join(this.claimsPath, file), "utf-8");
|
|
476
|
+
const claim = JSON.parse(content) as FileClaim;
|
|
477
|
+
if (claim.teammateId === this.teammateId) {
|
|
478
|
+
rmSync(join(this.claimsPath, file));
|
|
479
|
+
}
|
|
480
|
+
} catch {
|
|
481
|
+
// Skip
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
// Directory may not exist
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Check if a file is claimed by another teammate
|
|
491
|
+
*/
|
|
492
|
+
isFileClaimed(filePath: string): boolean {
|
|
493
|
+
const normalized = this.normalizePath(filePath);
|
|
494
|
+
|
|
495
|
+
// First check in-memory claims
|
|
496
|
+
const claim = this.claims.get(normalized);
|
|
497
|
+
|
|
498
|
+
if (claim) {
|
|
499
|
+
// Check if expired
|
|
500
|
+
if (claim.expiresAt && claim.expiresAt < Date.now()) {
|
|
501
|
+
this.claims.delete(normalized);
|
|
502
|
+
this.deleteClaimFile(filePath);
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Return true only if claimed by someone else
|
|
507
|
+
if (claim.teammateId !== this.teammateId) {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Also check disk for claims by other processes
|
|
513
|
+
const diskClaim = this.checkClaimOnDisk(filePath);
|
|
514
|
+
if (diskClaim) {
|
|
515
|
+
// Update memory with disk claim
|
|
516
|
+
this.claims.set(normalized, diskClaim);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get the claim on a file (if any)
|
|
525
|
+
*/
|
|
526
|
+
getFileClaim(filePath: string): FileClaim | undefined {
|
|
527
|
+
const normalized = this.normalizePath(filePath);
|
|
528
|
+
const claim = this.claims.get(normalized);
|
|
529
|
+
|
|
530
|
+
if (!claim) return undefined;
|
|
531
|
+
|
|
532
|
+
// Check if expired
|
|
533
|
+
if (claim.expiresAt && claim.expiresAt < Date.now()) {
|
|
534
|
+
this.claims.delete(normalized);
|
|
535
|
+
this.deleteClaimFile(filePath);
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return claim;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get all files claimed by a specific teammate
|
|
544
|
+
*/
|
|
545
|
+
getTeammateClaims(teammateId: string): FileClaim[] {
|
|
546
|
+
return Array.from(this.claims.values())
|
|
547
|
+
.filter(claim => claim.teammateId === teammateId);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get all active claims
|
|
552
|
+
*/
|
|
553
|
+
getAllClaims(): FileClaim[] {
|
|
554
|
+
return Array.from(this.claims.values());
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ============================================
|
|
558
|
+
// EVENT EMISSION
|
|
559
|
+
// ============================================
|
|
560
|
+
|
|
561
|
+
private emitEvent(event: CoordinationEvent): void {
|
|
562
|
+
if (this.config.onCoordinationEvent) {
|
|
563
|
+
this.config.onCoordinationEvent(event);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Emit specific callbacks
|
|
567
|
+
switch (event.type) {
|
|
568
|
+
case "status_change":
|
|
569
|
+
if (this.config.onStatusChange) {
|
|
570
|
+
const { teammateId, oldStatus, newStatus } = event.payload as {
|
|
571
|
+
teammateId: string;
|
|
572
|
+
oldStatus: TeammateStatus;
|
|
573
|
+
newStatus: TeammateStatus;
|
|
574
|
+
};
|
|
575
|
+
this.config.onStatusChange(teammateId, oldStatus, newStatus);
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
|
|
579
|
+
case "task_progress":
|
|
580
|
+
if (this.config.onProgress) {
|
|
581
|
+
const { progress } = event.payload as { progress: ProgressReport };
|
|
582
|
+
this.config.onProgress(event.teammateId, progress);
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
|
|
586
|
+
case "file_claim":
|
|
587
|
+
if (this.config.onFileClaimed) {
|
|
588
|
+
const { claim } = event.payload as { claim: FileClaim };
|
|
589
|
+
this.config.onFileClaimed(claim);
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
|
|
593
|
+
case "file_release":
|
|
594
|
+
if (this.config.onFileReleased) {
|
|
595
|
+
const { filePath } = event.payload as { filePath: string };
|
|
596
|
+
this.config.onFileReleased(filePath, event.teammateId);
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ============================================
|
|
603
|
+
// HELPERS
|
|
604
|
+
// ============================================
|
|
605
|
+
|
|
606
|
+
private getTeammateName(): string {
|
|
607
|
+
if (!this.teammateId) return "unknown";
|
|
608
|
+
const teammate = this.manager.getTeammate(this.teammateId);
|
|
609
|
+
return teammate?.name || this.teammateId;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ============================================
|
|
614
|
+
// COORDINATION HELPER FUNCTIONS
|
|
615
|
+
// ============================================
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Create a coordination-aware message
|
|
619
|
+
*/
|
|
620
|
+
export function createCoordinationMessage(
|
|
621
|
+
type: CoordinationEventType,
|
|
622
|
+
content: string
|
|
623
|
+
): string {
|
|
624
|
+
return `[COORD:${type.toUpperCase()}] ${content}`;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Parse a coordination message
|
|
629
|
+
*/
|
|
630
|
+
export function parseCoordinationMessage(message: string): {
|
|
631
|
+
isCoordination: boolean;
|
|
632
|
+
type?: CoordinationEventType;
|
|
633
|
+
content?: string;
|
|
634
|
+
} {
|
|
635
|
+
const match = message.match(/^\[COORD:([A-Z_]+)\]\s*(.*)$/);
|
|
636
|
+
if (!match) {
|
|
637
|
+
return { isCoordination: false };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const typeStr = match[1]!.toLowerCase() as CoordinationEventType;
|
|
641
|
+
return {
|
|
642
|
+
isCoordination: true,
|
|
643
|
+
type: typeStr,
|
|
644
|
+
content: match[2]!,
|
|
645
|
+
};
|
|
646
|
+
}
|