summaryrefslogblamecommitdiff
path: root/makima/frontend/src/lib/api.ts
blob: 466a79472c913189d5e183047e18cc799a5c91b4 (plain) (tree)
1
2
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288

                                      


































                                                                                 






























































                                                                                     
                                                          
                                                        
                                                                           
                                                                                



                                               










                                  






                                                        

                                                        






                                       

                                                                  
 





                              
                  



                                                                     

                    



                                                








                                

                         
                  





                                                                     









                                    

                                                                                   

                       
                                 
                    

                                                      





                                    

                       




















                                                    

 


                                                                
                 




                              

                              
                   
                          
                               









                               













                                               




                                
                                    



                                                              
                                                          






                                                                
                                                                






                                                                                
                                                           
                   











                                                                
                                                                 
                  

                               





                                                                 






                                                                
                                                                 





                                                                



                                   
                  
                   

                              
                          



                                        


                                      



                                                                      
                   
                               






                                                                   
































                                                                                          
                                                                             






                                                                                             
                                                                                        










                                                                
                                                                                      
                   













                                                                    
















                                                                                                      
































































                                                                                
























                                                                                





                                                     

                                                      






                                 

                                                                  

                                                                    







                       

                                      






































                                                               


                        











                                                

                                                                     





























                                                               

                                                                    







































































































































































                                                                                           









































































































                                                                                       

























































































                                                                                       




































































                                                                                      







                                                                                  










































                                                                                      





































































                                                                                                 






















                                                                                        














































































































































































































































































































































































                                                                                                     




                                                                                
                                                                   
                                                                  




                                                                                   




                                                                             


                                   




                                                                            
                                               
                                                   
                    

 

















                                                                                

                                                       



                                                 
                                        

 













                                                    
   

                                                                








                                                                               
















                                     

                                                   

                         

                                                      

                                                                                 











                             

                                                   



                                                      



                                                                                   

                                                                                 




























                                                         





                                                                                               

                                                                                 






                                        



                                               






































































































                                                                                

                                                                                          


                                          

                           


                                                                           
                                               






























































































































































































































































                                                                                  





















































                                                                                








































                                                                                












                                                                                

                                                 

                                                                                                                                                 










                                                                                     







































                                                                          









































































                                                                                       

                                                                                












































































































































































































































































































































































































                                                                                                

















































































































                                                                                               











                                                                                






































































































                                                                                                                                                      





                                                                                
                                                                          















                                      













                                  










                                      



















                                      

                                                                           
                                   
                                  











































                                            








                                            



                                     
                                                                     
                                             





























































































































                                                                                     

















                                                                                













                                                                           










                                                                              

                                                   














                                                      

                                                     













                                                  

                                                     







                                     

































































































                                                                                        

                                                                                

                                                                             

                                                    







                                                                                 
                                       









                                                                                                 



























































                                                                                          





















































































































































































































































































































































































































































































































































































































































































































































































                                                                                               
import { supabase } from "./supabase";

const API_CONFIG = {
  local: {
    http: "http://localhost:8080",
    ws: "ws://localhost:8080",
  },
  production: {
    http: "https://api.makima.jp",
    ws: "wss://api.makima.jp",
  },
} as const;

type Environment = "local" | "production";

function detectEnvironment(): Environment {
  // Check if explicitly set via env var
  const envOverride = import.meta.env.VITE_API_ENV as Environment | undefined;
  if (envOverride && (envOverride === "local" || envOverride === "production")) {
    return envOverride;
  }

  // Auto-detect based on hostname
  if (typeof window !== "undefined") {
    const hostname = window.location.hostname;
    if (hostname === "localhost" || hostname === "127.0.0.1") {
      return "local";
    }
  }

  return "production";
}

const env = detectEnvironment();

export const API_BASE = API_CONFIG[env].http;
export const WS_BASE = API_CONFIG[env].ws;

// =============================================================================
// Authentication helpers
// =============================================================================

/** Storage key for API key */
const API_KEY_STORAGE_KEY = "makima_api_key";

/** Get stored API key from localStorage */
export function getStoredApiKey(): string | null {
  if (typeof window === "undefined") return null;
  return localStorage.getItem(API_KEY_STORAGE_KEY);
}

/** Store API key in localStorage */
export function setStoredApiKey(key: string): void {
  if (typeof window === "undefined") return;
  localStorage.setItem(API_KEY_STORAGE_KEY, key);
}

/** Remove stored API key */
export function clearStoredApiKey(): void {
  if (typeof window === "undefined") return;
  localStorage.removeItem(API_KEY_STORAGE_KEY);
}

/** Get auth headers for API requests */
async function getAuthHeaders(): Promise<HeadersInit> {
  const headers: HeadersInit = {
    "Content-Type": "application/json",
  };

  // Try Supabase session first
  if (supabase) {
    const { data: { session } } = await supabase.auth.getSession();
    if (session?.access_token) {
      headers["Authorization"] = `Bearer ${session.access_token}`;
      return headers;
    }
  }

  // Fall back to API key if available
  const apiKey = getStoredApiKey();
  if (apiKey) {
    headers["X-Makima-API-Key"] = apiKey;
  }

  return headers;
}

/** Fetch with authentication headers */
async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
  const authHeaders = await getAuthHeaders();
  const mergedHeaders = {
    ...authHeaders,
    ...options.headers,
  };

  return fetch(url, {
    ...options,
    headers: mergedHeaders,
  });
}
export const LISTEN_ENDPOINT = `${WS_BASE}/api/v1/listen`;
export const SPEAK_ENDPOINT = `${WS_BASE}/api/v1/speak`;
export const FILE_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/files/subscribe`;
export const TASK_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/mesh/tasks/subscribe`;

export function getEnvironment(): Environment {
  return env;
}

// File API types
export interface TranscriptEntry {
  id: string;
  speaker: string;
  start: number;
  end: number;
  text: string;
  isFinal: boolean;
}

// Chart types for visualization
export type ChartType = "line" | "bar" | "pie" | "area";

// Body element types for structured content
export type BodyElement =
  | { type: "heading"; level: number; text: string }
  | { type: "paragraph"; text: string }
  | { type: "code"; language?: string; content: string }
  | { type: "list"; ordered: boolean; items: string[] }
  | {
      type: "chart";
      chartType: ChartType;
      title?: string;
      data: Record<string, unknown>[];
      config?: Record<string, unknown>;
    }
  | { type: "image"; src: string; alt?: string; caption?: string }
  | { type: "markdown"; content: string };

export interface FileSummary {
  id: string;
  name: string;
  description: string | null;
  transcriptCount: number;
  duration: number | null;
  version: number;
  /** Path to linked repository file (e.g., "README.md") */
  repoFilePath: string | null;
  /** Sync status: 'none', 'synced', 'modified', 'conflict' */
  repoSyncStatus: 'none' | 'synced' | 'modified' | 'conflict' | null;
  createdAt: string;
  updatedAt: string;
  // Contract info (joined from contracts table)
  contractId: string | null;
  contractName: string | null;
  contractPhase: ContractPhase | null;
}

export interface FileDetail {
  id: string;
  ownerId: string;
  name: string;
  description: string | null;
  transcript: TranscriptEntry[];
  location: string | null;
  summary: string | null;
  body: BodyElement[];
  version: number;
  /** Path to linked repository file (e.g., "README.md") */
  repoFilePath: string | null;
  /** When file was last synced from repository */
  repoSyncedAt: string | null;
  /** Sync status: 'none', 'synced', 'modified', 'conflict' */
  repoSyncStatus: 'none' | 'synced' | 'modified' | 'conflict' | null;
  createdAt: string;
  updatedAt: string;
}

export interface FileListResponse {
  files: FileSummary[];
  total: number;
}

export interface CreateFileRequest {
  /** Contract this file belongs to (required - files must belong to a contract) */
  contractId: string;
  name?: string;
  description?: string;
  transcript?: TranscriptEntry[];
  location?: string;
  /** Initial body elements (e.g., from a template) */
  body?: BodyElement[];
}

export interface UpdateFileRequest {
  name?: string;
  description?: string;
  transcript?: TranscriptEntry[];
  summary?: string;
  body?: BodyElement[];
  version?: number;
}

// Conflict error types
export interface ConflictErrorResponse {
  code: "VERSION_CONFLICT";
  message: string;
  expectedVersion: number;
  actualVersion: number;
}

export class VersionConflictError extends Error {
  expectedVersion: number;
  actualVersion: number;

  constructor(conflict: ConflictErrorResponse) {
    super(conflict.message);
    this.name = "VersionConflictError";
    this.expectedVersion = conflict.expectedVersion;
    this.actualVersion = conflict.actualVersion;
  }
}

// Available LLM models
export type LlmModel = "claude-sonnet" | "claude-opus" | "groq";

// Chat API types
export interface ChatMessage {
  role: "user" | "assistant";
  content: string;
}

export interface ChatRequest {
  message: string;
  model?: LlmModel;
  history?: ChatMessage[];
  focusedElementIndex?: number;
}

export interface ToolCallInfo {
  name: string;
  result: {
    success: boolean;
    message: string;
  };
}

// User question types for interactive LLM tool
export interface UserQuestion {
  id: string;
  question: string;
  options: string[];
  allowMultiple: boolean;
  allowCustom: boolean;
}

export interface UserAnswer {
  id: string;
  answers: string[];
}

export interface ChatResponse {
  response: string;
  toolCalls: ToolCallInfo[];
  updatedBody: BodyElement[];
  updatedSummary: string | null;
  pendingQuestions?: UserQuestion[];
}

// File API functions
export async function listFiles(): Promise<FileListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/files`);
  if (!res.ok) {
    throw new Error(`Failed to list files: ${res.statusText}`);
  }
  return res.json();
}

export async function getFile(id: string): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get file: ${res.statusText}`);
  }
  return res.json();
}

export async function createFile(data: CreateFileRequest): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files`, {
    method: "POST",
    body: JSON.stringify(data),
  });
  if (!res.ok) {
    throw new Error(`Failed to create file: ${res.statusText}`);
  }
  return res.json();
}

export async function updateFile(
  id: string,
  data: UpdateFileRequest
): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to update file: ${res.statusText}`);
  }
  return res.json();
}

export async function deleteFile(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to delete file: ${res.statusText}`);
  }
}

// Chat API function
export async function chatWithFile(
  id: string,
  message: string,
  model?: LlmModel,
  history?: ChatMessage[],
  focusedElementIndex?: number
): Promise<ChatResponse> {
  const body: ChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  if (focusedElementIndex !== undefined) {
    body.focusedElementIndex = focusedElementIndex;
  }
  const res = await authFetch(`${API_BASE}/api/v1/files/${id}/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// Version history types
export type VersionSource = "user" | "llm" | "system";

export interface FileVersion {
  version: number;
  name: string;
  description: string | null;
  summary: string | null;
  body: BodyElement[];
  source: VersionSource;
  createdAt: string;
  changeDescription?: string;
}

export interface FileVersionSummary {
  version: number;
  source: VersionSource;
  createdAt: string;
  changeDescription?: string;
}

export interface FileVersionListResponse {
  versions: FileVersionSummary[];
  total: number;
}

export interface RestoreVersionRequest {
  targetVersion: number;
}

// Version history API functions
export async function listFileVersions(fileId: string): Promise<FileVersionListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions`);
  if (!res.ok) {
    throw new Error(`Failed to list versions: ${res.statusText}`);
  }
  return res.json();
}

export async function getFileVersion(fileId: string, version: number): Promise<FileVersion> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/${version}`);
  if (!res.ok) {
    throw new Error(`Failed to get version: ${res.statusText}`);
  }
  return res.json();
}

export async function restoreFileVersion(
  fileId: string,
  targetVersion: number,
  currentVersion: number
): Promise<FileDetail> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/restore`, {
    method: "POST",
    body: JSON.stringify({ targetVersion, currentVersion }),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to restore version: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Sync a file from its linked repository file.
 * Triggers an async operation - the file will be updated when the daemon responds.
 * Returns 202 Accepted if the sync started successfully.
 */
export async function syncFileFromRepo(fileId: string): Promise<{ message: string; fileId: string }> {
  const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/sync-from-repo`, {
    method: "POST",
  });

  if (!res.ok) {
    const error = await res.json();
    throw new Error(error.message || `Failed to sync file: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// LLM Tool Definitions for Version History
// =============================================================================
// These types define the tools available to the LLM for version history access.
// The backend should implement handlers for these tools.

/**
 * Tool: read_version
 * Allows the LLM to read the content of a specific historical version.
 * This is read-only - it does not modify the document.
 */
export interface ReadVersionToolInput {
  version: number;
}

export interface ReadVersionToolOutput {
  success: boolean;
  version: number;
  body: BodyElement[];
  summary: string | null;
  source: VersionSource;
  createdAt: string;
  changeDescription?: string;
  message: string;
}

/**
 * Tool: list_versions
 * Allows the LLM to list all available versions of the document.
 */
export interface ListVersionsToolOutput {
  success: boolean;
  versions: FileVersionSummary[];
  currentVersion: number;
  message: string;
}

/**
 * Tool: restore_version
 * Allows the LLM to restore the document to a specific historical version.
 * This creates a new version with the content from the target version.
 */
export interface RestoreVersionToolInput {
  targetVersion: number;
  reason?: string;
}

export interface RestoreVersionToolOutput {
  success: boolean;
  previousVersion: number;
  newVersion: number;
  restoredFromVersion: number;
  message: string;
}

// LLM Tool type definitions for the backend
export type LlmVersionTool =
  | { name: "read_version"; input: ReadVersionToolInput }
  | { name: "list_versions"; input: Record<string, never> }
  | { name: "restore_version"; input: RestoreVersionToolInput };

export type LlmVersionToolResult =
  | { name: "read_version"; result: ReadVersionToolOutput }
  | { name: "list_versions"; result: ListVersionsToolOutput }
  | { name: "restore_version"; result: RestoreVersionToolOutput };

// =============================================================================
// Mesh/Task Types for Claude Code Orchestration
// =============================================================================

export type TaskStatus =
  | "pending"
  | "initializing"
  | "starting"
  | "running"
  | "paused"
  | "blocked"
  | "done"
  | "failed"
  | "merged";

export type MergeMode = "pr" | "auto" | "manual";

/** Action to perform when a task completes successfully */
export type CompletionAction = "none" | "branch" | "merge" | "pr";

export type DaemonStatus = "connected" | "disconnected" | "unhealthy";

export interface TaskSummary {
  id: string;
  /** Contract this task belongs to */
  contractId: string | null;
  /** Contract name (joined from contracts table) */
  contractName: string | null;
  /** Contract phase (joined from contracts table) */
  contractPhase: ContractPhase | null;
  /** Contract status (joined from contracts table) */
  contractStatus: ContractStatus | null;
  parentTaskId: string | null;
  depth: number;
  name: string;
  status: TaskStatus;
  priority: number;
  progressSummary: string | null;
  subtaskCount: number;
  /** Whether this is a supervisor task (contract orchestrator) */
  isSupervisor: boolean;
  /** Whether this task is hidden from the UI (user dismissed it) */
  hidden: boolean;
  version: number;
  createdAt: string;
  updatedAt: string;
}

export interface Task {
  id: string;
  ownerId: string;
  /** Contract this task belongs to */
  contractId: string | null;
  parentTaskId: string | null;
  depth: number;
  name: string;
  description: string | null;
  status: TaskStatus;
  priority: number;
  plan: string;

  // Daemon/container info
  daemonId: string | null;
  containerId: string | null;
  overlayPath: string | null;

  // Repository info
  repositoryUrl: string | null;
  baseBranch: string | null;
  targetBranch: string | null;

  // Merge settings
  mergeMode: MergeMode | null;
  prUrl: string | null;

  // Completion action settings
  /** Path to user's local repository for completion actions */
  targetRepoPath: string | null;
  /** Action on completion: "none", "branch", "merge", "pr" */
  completionAction: CompletionAction | null;

  // Progress tracking
  progressSummary: string | null;
  lastOutput: string | null;
  errorMessage: string | null;

  // Timestamps
  startedAt: string | null;
  completedAt: string | null;
  version: number;
  createdAt: string;
  updatedAt: string;

  // Supervisor flag
  isSupervisor: boolean;
}

export interface TaskWithSubtasks extends Task {
  subtasks: TaskSummary[];
}

export interface TaskListResponse {
  tasks: TaskSummary[];
  total: number;
}

export interface CreateTaskRequest {
  /** Contract this task belongs to (optional - can be standalone) */
  contractId?: string;
  name: string;
  description?: string;
  plan: string;
  parentTaskId?: string;
  priority?: number;
  repositoryUrl?: string;
  baseBranch?: string;
  targetBranch?: string;
  mergeMode?: MergeMode;
  /** Path to user's local repository for completion actions */
  targetRepoPath?: string;
  /** Action on completion: "none", "branch", "merge", "pr" */
  completionAction?: CompletionAction;
}

export interface UpdateTaskRequest {
  name?: string;
  description?: string;
  plan?: string;
  status?: TaskStatus;
  priority?: number;
  progressSummary?: string;
  lastOutput?: string;
  errorMessage?: string;
  mergeMode?: MergeMode;
  prUrl?: string;
  /** Path to user's local repository for completion actions */
  targetRepoPath?: string;
  /** Action on completion: "none", "branch", "merge", "pr" */
  completionAction?: CompletionAction;
  /** Whether this task is hidden from the UI (user dismissed it) */
  hidden?: boolean;
  version?: number;
}

export interface TaskEvent {
  id: string;
  taskId: string;
  eventType: string;
  previousStatus: string | null;
  newStatus: string | null;
  eventData: Record<string, unknown> | null;
  createdAt: string;
}

export interface TaskEventListResponse {
  events: TaskEvent[];
  total: number;
}

export interface Daemon {
  id: string;
  ownerId: string;
  connectionId: string;
  hostname: string | null;
  machineId: string | null;
  maxConcurrentTasks: number;
  currentTaskCount: number;
  status: DaemonStatus;
  lastHeartbeatAt: string;
  connectedAt: string;
  disconnectedAt: string | null;
}

export interface DaemonListResponse {
  daemons: Daemon[];
  total: number;
}

// Mesh API functions
export async function listTasks(): Promise<TaskListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`);
  if (!res.ok) {
    throw new Error(`Failed to list tasks: ${res.statusText}`);
  }
  return res.json();
}

export async function getTask(id: string): Promise<TaskWithSubtasks> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get task: ${res.statusText}`);
  }
  return res.json();
}

export async function createTask(data: CreateTaskRequest): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`, {
    method: "POST",
    body: JSON.stringify(data),
  });
  if (!res.ok) {
    throw new Error(`Failed to create task: ${res.statusText}`);
  }
  return res.json();
}

export async function updateTask(
  id: string,
  data: UpdateTaskRequest
): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to update task: ${res.statusText}`);
  }
  return res.json();
}

export async function deleteTask(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to delete task: ${res.statusText}`);
  }
}

export async function startTask(id: string): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/start`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to start task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export async function stopTask(id: string): Promise<Task> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/stop`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to stop task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export interface SendMessageResponse {
  success: boolean;
  taskId: string;
  messageLength: number;
}

/**
 * Send a message to a running task's stdin.
 * This can be used to provide input to Claude Code when it's waiting for user input,
 * or to inject context/instructions into a running task.
 */
export async function sendTaskMessage(
  taskId: string,
  message: string
): Promise<SendMessageResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/message`, {
    method: "POST",
    body: JSON.stringify({ message }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to send message: ${errorText || res.statusText}`);
  }
  return res.json();
}

export interface RetryCompletionResponse {
  success: boolean;
  taskId: string;
  action: string;
  targetRepoPath: string;
  message: string;
}

/**
 * Retry completion action for a completed task.
 * This allows retrying a completion action (push branch, merge, create PR)
 * after filling in the target_repo_path if it wasn't set when the task completed.
 */
export async function retryCompletionAction(
  taskId: string
): Promise<RetryCompletionResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/retry-completion`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to retry completion action: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Git Actions for Tasks (Manual)
// =============================================================================

/** Response from export patch */
export interface ExportPatchResponse {
  success: boolean;
  taskId: string;
  fileName: string;
  filePath?: string;
  patchSize?: number;
  message: string;
}

/**
 * Export a task's changes as a patch file.
 * The patch will be saved to the contract's patch directory.
 */
export async function exportTaskPatch(
  taskId: string,
  fileName?: string
): Promise<ExportPatchResponse> {
  const body: Record<string, unknown> = {};
  if (fileName) {
    body.fileName = fileName;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/export-patch`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to export patch: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Response from push branch */
export interface PushBranchResponse {
  success: boolean;
  taskId: string;
  branchName: string;
  remote?: string;
  message: string;
}

/**
 * Push a task's changes to a remote branch.
 * Creates a branch if it doesn't exist and pushes the commits.
 */
export async function pushTaskBranch(
  taskId: string,
  branchName?: string
): Promise<PushBranchResponse> {
  const body: Record<string, unknown> = {};
  if (branchName) {
    body.branchName = branchName;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/push-branch`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to push branch: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Response from create PR */
export interface CreatePRResponse {
  success: boolean;
  taskId: string;
  prUrl?: string;
  prNumber?: number;
  branchName?: string;
  message: string;
}

/**
 * Create a pull request for a task's changes.
 * First pushes the branch if needed, then creates the PR.
 */
export async function createTaskPR(
  taskId: string,
  title?: string,
  body?: string
): Promise<CreatePRResponse> {
  const reqBody: Record<string, unknown> = {};
  if (title) {
    reqBody.title = title;
  }
  if (body) {
    reqBody.body = body;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/create-pr`, {
    method: "POST",
    body: JSON.stringify(reqBody),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to create PR: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** A suggested directory from a connected daemon */
export interface DaemonDirectory {
  /** Path to the directory */
  path: string;
  /** Display label for the directory */
  label: string;
  /** Type of directory: "working", "makima", "worktrees" */
  directoryType: string;
  /** Daemon hostname this directory is from */
  hostname: string | null;
  /** Whether the directory already exists (for validation) */
  exists?: boolean;
}

export interface DaemonDirectoriesResponse {
  directories: DaemonDirectory[];
}

/**
 * Get suggested directories from connected daemons.
 * These can be used as target_repo_path suggestions for completion actions.
 */
export async function getDaemonDirectories(): Promise<DaemonDirectoriesResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/directories`);
  if (!res.ok) {
    throw new Error(`Failed to get daemon directories: ${res.statusText}`);
  }
  return res.json();
}

/** Request to clone a worktree */
export interface CloneWorktreeRequest {
  targetDir: string;
}

/** Response from clone worktree */
export interface CloneWorktreeResponse {
  status: string;
  taskId: string;
  targetDir: string;
}

/**
 * Clone a task's worktree to a target directory.
 */
export async function cloneWorktree(
  taskId: string,
  targetDir: string
): Promise<CloneWorktreeResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/clone`, {
    method: "POST",
    body: JSON.stringify({ targetDir }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to clone worktree: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Request to check if target exists */
export interface CheckTargetExistsRequest {
  targetDir: string;
}

/** Response from check target exists */
export interface CheckTargetExistsResponse {
  status: string;
  taskId: string;
  targetDir: string;
}

/**
 * Check if a target directory exists.
 */
export async function checkTargetExists(
  taskId: string,
  targetDir: string
): Promise<CheckTargetExistsResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/check-target`, {
    method: "POST",
    body: JSON.stringify({ targetDir }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to check target: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Task Recovery (Daemon Failover)
// =============================================================================

/** Request to reassign a task to a new daemon */
export interface ReassignTaskRequest {
  targetDaemonId?: string;
  includeContext?: boolean;
}

/** Response from reassigning a task */
export interface ReassignTaskResponse {
  task: Task;
  daemonId: string;
  oldTaskId: string;
  contextIncluded: boolean;
  contextEntries: number;
}

/**
 * Reassign a task to a new daemon after daemon disconnect.
 * Creates a new task with conversation context, deletes the old one.
 */
export async function reassignTask(
  taskId: string,
  options?: ReassignTaskRequest
): Promise<ReassignTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/reassign`, {
    method: "POST",
    body: JSON.stringify(options || {}),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to reassign task: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Request to continue a task */
export interface ContinueTaskRequest {
  targetDaemonId?: string;
}

/** Response from continuing a task */
export interface ContinueTaskResponse {
  task: Task;
  daemonId: string;
  contextEntries: number;
}

/**
 * Continue a task after daemon disconnect by restarting it with conversation context.
 * Unlike reassign, this keeps the same task ID.
 */
export async function continueTask(
  taskId: string,
  options?: ContinueTaskRequest
): Promise<ContinueTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/continue`, {
    method: "POST",
    body: JSON.stringify(options || {}),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to continue task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export async function listSubtasks(taskId: string): Promise<TaskListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/subtasks`);
  if (!res.ok) {
    throw new Error(`Failed to list subtasks: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Task Branching
// =============================================================================

/** Request to branch a task */
export interface BranchTaskRequest {
  /** Message to send to the new branched task */
  message: string;
  /** Optional name for the new task (defaults to "Branch of {original task name}") */
  name?: string;
  /** Whether to include conversation history from the source task */
  includeConversation?: boolean;
}

/** Response from branching a task */
export interface BranchTaskResponse {
  /** The newly created task */
  task: Task;
  /** Number of conversation messages copied to the new task */
  messageCount: number;
  /** ID of the daemon assigned to the new task (null if not yet assigned) */
  daemonId: string | null;
}

/**
 * Branch a task to create a new task with the same state.
 * Copies the worktree and optionally the conversation history.
 */
export async function branchTask(
  taskId: string,
  request: BranchTaskRequest
): Promise<BranchTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/branch`, {
    method: "POST",
    body: JSON.stringify(request),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to branch task: ${errorText || res.statusText}`);
  }
  return res.json();
}

export async function listTaskEvents(
  taskId: string
): Promise<TaskEventListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/events`);
  if (!res.ok) {
    throw new Error(`Failed to list task events: ${res.statusText}`);
  }
  return res.json();
}

/** A single output entry from a Claude Code task */
export interface TaskOutputEntry {
  id: string;
  taskId: string;
  /** Message type: "assistant", "tool_use", "tool_result", "result", "system", "error", "raw" */
  messageType: string;
  /** Main text content */
  content: string;
  /** Tool name if tool_use message */
  toolName?: string;
  /** Tool input JSON if tool_use message */
  toolInput?: Record<string, unknown>;
  /** Whether tool result was an error */
  isError?: boolean;
  /** Cost in USD if result message */
  costUsd?: number;
  /** Duration in ms if result message */
  durationMs?: number;
  /** Timestamp when this output was recorded */
  createdAt: string;
}

/** Response from the task output endpoint */
export interface TaskOutputResponse {
  entries: TaskOutputEntry[];
  total: number;
  taskId: string;
}

/**
 * Get task output history.
 * Retrieves all recorded output from a task's Claude Code process.
 * Use this to fetch missed output when subscribing late or reconnecting.
 */
export async function getTaskOutput(
  taskId: string
): Promise<TaskOutputResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/output`);
  if (!res.ok) {
    throw new Error(`Failed to get task output: ${res.statusText}`);
  }
  return res.json();
}

export async function listDaemons(): Promise<DaemonListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons`);
  if (!res.ok) {
    throw new Error(`Failed to list daemons: ${res.statusText}`);
  }
  return res.json();
}

export async function getDaemon(id: string): Promise<Daemon> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get daemon: ${res.statusText}`);
  }
  return res.json();
}

/** Response from the restart daemon endpoint */
export interface RestartDaemonResponse {
  success: boolean;
  daemonId: string;
  message: string;
}

/**
 * Restart a connected daemon.
 * Sends a restart command to the daemon, which will gracefully terminate
 * and restart. Any running tasks will be interrupted.
 */
export async function restartDaemon(id: string): Promise<RestartDaemonResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${id}/restart`, {
    method: "POST",
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => ({}));
    throw new Error(errorData.message || `Failed to restart daemon: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Mesh Chat Types for Task Orchestration
// =============================================================================

export interface MeshChatMessage {
  role: "user" | "assistant";
  content: string;
}

export interface MeshChatRequest {
  message: string;
  model?: LlmModel;
  history?: MeshChatMessage[];
}

export interface MeshToolCallInfo {
  name: string;
  result: {
    success: boolean;
    message: string;
  };
}

export interface MeshChatResponse {
  response: string;
  toolCalls: MeshToolCallInfo[];
  pendingQuestions?: UserQuestion[];
}

// Mesh Chat API functions

// Top-level mesh chat (no specific task context)
export async function chatWithMesh(
  message: string,
  model?: LlmModel,
  history?: MeshChatMessage[]
): Promise<MeshChatResponse> {
  const body: MeshChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// Task-scoped mesh chat
export async function chatWithTask(
  taskId: string,
  message: string,
  model?: LlmModel,
  history?: MeshChatMessage[]
): Promise<MeshChatResponse> {
  const body: MeshChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Mesh Chat History Types
// =============================================================================

export type MeshChatContextType = "mesh" | "task" | "subtask";

export interface MeshChatContext {
  type: MeshChatContextType;
  taskId?: string;
  parentTaskId?: string;
}

export interface MeshChatMessageRecord {
  id: string;
  conversationId: string;
  role: "user" | "assistant" | "error";
  content: string;
  contextType: MeshChatContextType;
  contextTaskId: string | null;
  toolCalls: MeshToolCallInfo[] | null;
  pendingQuestions: UserQuestion[] | null;
  createdAt: string;
}

export interface MeshChatHistoryResponse {
  conversationId: string;
  messages: MeshChatMessageRecord[];
}

export interface MeshChatWithContextRequest {
  message: string;
  model?: LlmModel;
  contextType?: MeshChatContextType;
  contextTaskId?: string;
}

// =============================================================================
// Mesh Chat History API Functions
// =============================================================================

/**
 * Get the current chat history from the database
 */
export async function getMeshChatHistory(): Promise<MeshChatHistoryResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`);
  if (!res.ok) {
    throw new Error(`Failed to get chat history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Clear chat history (archives current conversation, starts new one)
 */
export async function clearMeshChatHistory(): Promise<{ success: boolean; conversationId: string }> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to clear chat history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Chat with mesh using context (new approach with DB history)
 */
export async function chatWithMeshContext(
  message: string,
  context: MeshChatContext,
  model?: LlmModel
): Promise<MeshChatResponse> {
  const body: MeshChatWithContextRequest = {
    message,
    contextType: context.type,
  };

  if (model) {
    body.model = model;
  }

  // Set contextTaskId based on context type
  if (context.type === "task" && context.taskId) {
    body.contextTaskId = context.taskId;
  } else if (context.type === "subtask" && context.taskId) {
    body.contextTaskId = context.taskId;
  }

  // Use top-level endpoint (it now loads history from DB)
  const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// API Key Management
// =============================================================================

export interface ApiKeyInfo {
  id: string;
  prefix: string;
  name: string | null;
  lastUsedAt: string | null;
  createdAt: string;
}

export interface CreateApiKeyResponse {
  id: string;
  key: string;
  prefix: string;
  name: string | null;
  createdAt: string;
}

export interface RefreshApiKeyResponse {
  id: string;
  key: string;
  prefix: string;
  name: string | null;
  createdAt: string;
  previousKeyRevoked: boolean;
}

export interface RevokeApiKeyResponse {
  message: string;
  revokedKeyPrefix: string;
}

/**
 * Get information about the current active API key.
 */
export async function getApiKey(): Promise<ApiKeyInfo | null> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`);
  if (res.status === 404) {
    return null;
  }
  if (!res.ok) {
    throw new Error(`Failed to get API key: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Create a new API key.
 */
export async function createApiKey(name?: string): Promise<CreateApiKeyResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, {
    method: "POST",
    body: JSON.stringify({ name }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to create API key: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Refresh (rotate) the current API key.
 */
export async function refreshApiKey(name?: string): Promise<RefreshApiKeyResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys/refresh`, {
    method: "POST",
    body: JSON.stringify({ name }),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to refresh API key: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Revoke the current API key.
 */
export async function revokeApiKey(): Promise<RevokeApiKeyResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, {
    method: "DELETE",
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to revoke API key: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// User Account Management
// =============================================================================

export interface ChangePasswordRequest {
  currentPassword: string;
  newPassword: string;
}

export interface ChangePasswordResponse {
  success: boolean;
  message: string;
}

export interface ChangeEmailRequest {
  password: string;
  newEmail: string;
}

export interface ChangeEmailResponse {
  success: boolean;
  message: string;
  verificationSent: boolean;
}

export interface DeleteAccountRequest {
  password: string;
  confirmation: string;
}

export interface DeleteAccountResponse {
  success: boolean;
  message: string;
}

/**
 * Change the current user's password.
 * Requires current password verification.
 */
export async function changePassword(
  currentPassword: string,
  newPassword: string
): Promise<ChangePasswordResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/users/me/password`, {
    method: "PUT",
    body: JSON.stringify({ currentPassword, newPassword }),
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => null);
    const errorMessage = errorData?.message || res.statusText;
    throw new Error(errorMessage);
  }
  return res.json();
}

/**
 * Change the current user's email address.
 * Requires password verification.
 */
export async function changeEmail(
  password: string,
  newEmail: string
): Promise<ChangeEmailResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/users/me/email`, {
    method: "PUT",
    body: JSON.stringify({ password, newEmail }),
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => null);
    const errorMessage = errorData?.message || res.statusText;
    throw new Error(errorMessage);
  }
  return res.json();
}

/**
 * Delete the current user's account.
 * Requires password verification and email confirmation.
 */
export async function deleteAccount(
  password: string,
  confirmation: string
): Promise<DeleteAccountResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/users/me`, {
    method: "DELETE",
    body: JSON.stringify({ password, confirmation }),
  });
  if (!res.ok) {
    const errorData = await res.json().catch(() => null);
    const errorMessage = errorData?.message || res.statusText;
    throw new Error(errorMessage);
  }
  return res.json();
}

// =============================================================================
// Contract Types for Workflow Management
// =============================================================================

/** Contract type determines the workflow and required documents */
export type ContractType = "simple" | "specification" | "execute";
export type ContractPhase = "research" | "specify" | "plan" | "execute" | "review";
export type ContractStatus = "active" | "completed" | "archived";
export type RepositorySourceType = "remote" | "local" | "managed";
export type RepositoryStatus = "ready" | "pending" | "creating" | "failed";

/** Get valid phases for a contract type */
export function getValidPhases(contractType: ContractType): ContractPhase[] {
  if (contractType === "simple") {
    return ["plan", "execute"];
  }
  if (contractType === "execute") {
    return ["execute"];
  }
  return ["research", "specify", "plan", "execute", "review"];
}

/** Get default initial phase for a contract type */
export function getDefaultPhase(contractType: ContractType): ContractPhase {
  if (contractType === "simple") return "plan";
  if (contractType === "execute") return "execute";
  return "research";
}

// =============================================================================
// Contract Type Templates
// =============================================================================

/** Contract type template returned by the API */
export interface ContractTypeTemplate {
  /** Template identifier (e.g., "simple", "specification") */
  id: string;
  /** Display name */
  name: string;
  /** Description of the contract type workflow */
  description: string;
  /** Ordered list of phases for this contract type */
  phases: ContractPhase[];
  /** Default starting phase */
  defaultPhase: ContractPhase;
  /** Whether this is a built-in type (always available) */
  isBuiltin: boolean;
  /** Optional mapping from phase ID to display name */
  phaseNames?: Record<string, string>;
}

/** Response from list contract types endpoint */
export interface ListContractTypesResponse {
  contractTypes: ContractTypeTemplate[];
}

/** Phase definition for custom templates */
export interface PhaseDefinition {
  id: string;
  name: string;
  order: number;
}

/** Deliverable definition for custom templates */
export interface DeliverableDefinition {
  id: string;
  name: string;
  priority: "required" | "recommended" | "optional";
}

/**
 * List available contract types.
 * Returns built-in types only (simple, specification, execute).
 */
export async function listContractTypes(): Promise<ListContractTypesResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/contract-types`);
  if (!res.ok) {
    throw new Error(`Failed to list contract types: ${res.statusText}`);
  }
  return res.json();
}

export interface ContractRepository {
  id: string;
  contractId: string;
  name: string;
  repositoryUrl: string | null;
  localPath: string | null;
  sourceType: RepositorySourceType;
  status: RepositoryStatus;
  isPrimary: boolean;
  createdAt: string;
  updatedAt: string;
}

export interface ContractSummary {
  id: string;
  name: string;
  description: string | null;
  /** Contract type: "simple" or "specification" */
  contractType: ContractType;
  phase: ContractPhase;
  status: ContractStatus;
  /** Supervisor task ID for contract orchestration */
  supervisorTaskId: string | null;
  /** When true, tasks won't auto-push or create PRs - use patch files instead */
  localOnly: boolean;
  fileCount: number;
  taskCount: number;
  repositoryCount: number;
  version: number;
  createdAt: string;
}

export interface Contract {
  id: string;
  ownerId: string;
  name: string;
  description: string | null;
  /** Contract type: "simple" or "specification" */
  contractType: ContractType;
  phase: ContractPhase;
  status: ContractStatus;
  /** Supervisor task ID for contract orchestration */
  supervisorTaskId: string | null;
  /** Whether tasks for this contract should run in autonomous loop mode */
  autonomousLoop: boolean;
  /** Whether to wait for user confirmation before progressing to the next phase */
  phaseGuard: boolean;
  /** When true, tasks won't auto-push or create PRs - use patch files instead */
  localOnly: boolean;
  version: number;
  createdAt: string;
  updatedAt: string;
}

export interface ContractWithRelations extends Contract {
  repositories: ContractRepository[];
  files: FileSummary[];
  tasks: TaskSummary[];
}

export interface ContractEvent {
  id: string;
  contractId: string;
  eventType: string;
  previousPhase: string | null;
  newPhase: string | null;
  eventData: Record<string, unknown> | null;
  createdAt: string;
}

export interface ContractListResponse {
  contracts: ContractSummary[];
  total: number;
}

export interface CreateContractRequest {
  name: string;
  description?: string;
  /** Contract type: "simple" (default), "specification", "execute", or custom template name */
  contractType?: ContractType | string;
  /** UUID of a custom template to use. If provided, takes precedence over contractType. */
  templateId?: string;
  /** Initial phase to start in (defaults based on contract type or template) */
  initialPhase?: ContractPhase | string;
  /** When true, tasks won't auto-push or create PRs - use patch files instead */
  localOnly?: boolean;
}

export interface UpdateContractRequest {
  name?: string;
  description?: string;
  phase?: ContractPhase;
  status?: ContractStatus;
  /** Enable or disable autonomous loop mode */
  autonomousLoop?: boolean;
  /** Enable or disable phase guard mode */
  phaseGuard?: boolean;
  version?: number;
}

export interface AddRemoteRepositoryRequest {
  name: string;
  repositoryUrl: string;
  isPrimary?: boolean;
}

export interface AddLocalRepositoryRequest {
  name: string;
  localPath: string;
  isPrimary?: boolean;
}

export interface CreateManagedRepositoryRequest {
  name: string;
  isPrimary?: boolean;
}

export interface ChangePhaseRequest {
  phase: ContractPhase;
}

// =============================================================================
// Contract API Functions
// =============================================================================

/**
 * List all contracts.
 */
export async function listContracts(): Promise<ContractListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts`);
  if (!res.ok) {
    throw new Error(`Failed to list contracts: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get a contract with all its relations.
 */
export async function getContract(id: string): Promise<ContractWithRelations> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get contract: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Create a new contract.
 */
export async function createContract(
  data: CreateContractRequest
): Promise<ContractSummary> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts`, {
    method: "POST",
    body: JSON.stringify(data),
  });
  if (!res.ok) {
    throw new Error(`Failed to create contract: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Update a contract.
 */
export async function updateContract(
  id: string,
  data: UpdateContractRequest
): Promise<ContractSummary> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });

  if (res.status === 409) {
    const conflict = (await res.json()) as ConflictErrorResponse;
    throw new VersionConflictError(conflict);
  }

  if (!res.ok) {
    throw new Error(`Failed to update contract: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Delete a contract.
 */
export async function deleteContract(id: string): Promise<void> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to delete contract: ${res.statusText}`);
  }
}

/**
 * Change contract phase.
 * @param confirmed - When true, confirms the phase change even if phase_guard is enabled.
 *                    Defaults to true for explicit user actions like drag-and-drop.
 */
export async function changeContractPhase(
  id: string,
  phase: ContractPhase,
  confirmed: boolean = true
): Promise<ContractSummary> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/phase`, {
    method: "POST",
    body: JSON.stringify({ phase, confirmed }),
  });
  if (!res.ok) {
    throw new Error(`Failed to change phase: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get contract event history.
 */
export async function getContractEvents(
  id: string
): Promise<ContractEvent[]> {
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${id}/events`);
  if (!res.ok) {
    throw new Error(`Failed to get events: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Contract Repository Management
// =============================================================================

/**
 * Add a remote repository to a contract.
 */
export async function addRemoteRepository(
  contractId: string,
  data: AddRemoteRepositoryRequest
): Promise<ContractRepository> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/remote`,
    {
      method: "POST",
      body: JSON.stringify(data),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to add remote repository: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Add a local repository to a contract.
 */
export async function addLocalRepository(
  contractId: string,
  data: AddLocalRepositoryRequest
): Promise<ContractRepository> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/local`,
    {
      method: "POST",
      body: JSON.stringify(data),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to add local repository: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Create a managed repository (daemon will create it).
 */
export async function createManagedRepository(
  contractId: string,
  data: CreateManagedRepositoryRequest
): Promise<ContractRepository> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/managed`,
    {
      method: "POST",
      body: JSON.stringify(data),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to create managed repository: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Delete a repository from a contract.
 */
export async function deleteContractRepository(
  contractId: string,
  repoId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}`,
    {
      method: "DELETE",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to delete repository: ${res.statusText}`);
  }
}

/**
 * Set a repository as primary.
 */
export async function setRepositoryPrimary(
  contractId: string,
  repoId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/repositories/${repoId}/primary`,
    {
      method: "PUT",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to set repository as primary: ${res.statusText}`);
  }
}

// =============================================================================
// Contract Task Association
// =============================================================================

/**
 * Add a task to a contract.
 */
export async function addTaskToContract(
  contractId: string,
  taskId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`,
    {
      method: "POST",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to add task to contract: ${res.statusText}`);
  }
}

/**
 * Remove a task from a contract.
 */
export async function removeTaskFromContract(
  contractId: string,
  taskId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/tasks/${taskId}`,
    {
      method: "DELETE",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to remove task from contract: ${res.statusText}`);
  }
}

// =============================================================================
// Contract Chat Types and API
// =============================================================================

export interface ContractChatRequest {
  message: string;
  model?: LlmModel;
  history?: ChatMessage[];
}

export interface ContractToolCallInfo {
  name: string;
  result: {
    success: boolean;
    message: string;
  };
}

export interface ContractChatResponse {
  response: string;
  toolCalls: ContractToolCallInfo[];
  pendingQuestions?: UserQuestion[];
}

/**
 * Chat with a contract using LLM-powered management tools.
 */
export async function chatWithContract(
  contractId: string,
  message: string,
  model?: LlmModel,
  history?: ChatMessage[]
): Promise<ContractChatResponse> {
  const body: ContractChatRequest = { message };
  if (model) {
    body.model = model;
  }
  if (history && history.length > 0) {
    body.history = history;
  }
  const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/chat`, {
    method: "POST",
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Contract chat failed: ${errorText || res.statusText}`);
  }
  return res.json();
}

// Contract chat history types
export interface ContractChatMessage {
  id: string;
  conversationId: string;
  role: "user" | "assistant" | "error";
  content: string;
  toolCalls?: unknown;
  pendingQuestions?: unknown;
  createdAt: string;
}

export interface ContractChatHistoryResponse {
  contractId: string;
  conversationId: string;
  messages: ContractChatMessage[];
}

/** Get contract chat history */
export async function getContractChatHistory(
  contractId: string
): Promise<ContractChatHistoryResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/chat/history`
  );
  if (!res.ok) {
    throw new Error(`Failed to fetch contract chat history: ${res.statusText}`);
  }
  return res.json();
}

/** Clear contract chat history (starts a new conversation) */
export async function clearContractChatHistory(
  contractId: string
): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/chat/history`,
    { method: "DELETE" }
  );
  if (!res.ok) {
    throw new Error(`Failed to clear contract chat history: ${res.statusText}`);
  }
}

// =============================================================================
// Contract Discussion Types and API
// =============================================================================

export interface DiscussContractRequest {
  message: string;
  model?: LlmModel;
  history?: ChatMessage[];
  transcriptContext?: string;
}

export interface CreatedContractInfo {
  id: string;
  name: string;
  description: string | null;
  contractType: string;
  initialPhase: string;
}

export interface DiscussContractResponse {
  response: string;
  toolCalls: ContractToolCallInfo[];
  createdContract?: CreatedContractInfo;
  pendingQuestions?: UserQuestion[];
}

/**
 * Discuss a potential contract with Makima.
 * This is an ephemeral conversation that can result in contract creation.
 */
export async function discussContract(
  message: string,
  model?: LlmModel,
  history?: ChatMessage[],
  transcriptContext?: string
): Promise<DiscussContractResponse> {
  const body: DiscussContractRequest = { message };
  if (model) body.model = model;
  if (history && history.length > 0) body.history = history;
  if (transcriptContext) body.transcriptContext = transcriptContext;

  const res = await authFetch(`${API_BASE}/api/v1/contracts/discuss`, {
    method: "POST",
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Discussion failed: ${errorText || res.statusText}`);
  }

  return res.json();
}

// =============================================================================
// Template Types and API
// =============================================================================

export interface TemplateSummary {
  id: string;
  name: string;
  phase: ContractPhase;
  description: string;
  elementCount: number;
}

export interface FileTemplate {
  id: string;
  name: string;
  phase: ContractPhase;
  description: string;
  suggestedBody: BodyElement[];
}

export interface ListTemplatesResponse {
  templates: TemplateSummary[];
}

export async function listTemplates(
  phase?: ContractPhase
): Promise<ListTemplatesResponse> {
  const params = phase ? `?phase=${phase}` : "";
  const res = await authFetch(`${API_BASE}/api/v1/templates${params}`);
  if (!res.ok) {
    throw new Error(`Failed to list templates: ${res.statusText}`);
  }
  return res.json();
}

export async function getTemplate(id: string): Promise<FileTemplate> {
  const res = await authFetch(`${API_BASE}/api/v1/templates/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get template: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Supervisor Question Types and Functions
// =============================================================================

export interface PendingQuestion {
  questionId: string;
  taskId: string;
  contractId: string;
  question: string;
  choices: string[];
  context: string | null;
  createdAt: string;
  /** Whether multiple choices can be selected */
  multiSelect?: boolean;
  /** Question type - "general" for regular questions, "phase_confirmation" for phase transitions, "contract_complete" for contract completion */
  questionType?: "general" | "phase_confirmation" | "contract_complete";
  /** Phase confirmation specific data (when questionType is "phase_confirmation") */
  phaseConfirmation?: {
    currentPhase: ContractPhase;
    nextPhase: ContractPhase;
    contractName?: string;
    summary?: string;
    deliverables?: Array<{
      name: string;
      completed: boolean;
    }>;
  };
}

export interface AnswerQuestionRequest {
  response: string;
}

export interface AnswerQuestionResponse {
  success: boolean;
}

/**
 * Get all pending supervisor questions for the current user.
 */
export async function listPendingQuestions(): Promise<PendingQuestion[]> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/questions`);
  if (!res.ok) {
    throw new Error(`Failed to list questions: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Answer a pending supervisor question.
 */
export async function answerQuestion(
  questionId: string,
  response: string
): Promise<AnswerQuestionResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/mesh/questions/${questionId}/answer`,
    {
      method: "POST",
      body: JSON.stringify({ response }),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to answer question: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Repository History Types and API
// =============================================================================

export interface RepositoryHistoryEntry {
  id: string;
  ownerId: string;
  name: string;
  repositoryUrl: string | null;
  localPath: string | null;
  sourceType: "remote" | "local";
  useCount: number;
  lastUsedAt: string;
  createdAt: string;
}

export interface RepositoryHistoryListResponse {
  entries: RepositoryHistoryEntry[];
  total: number;
}

/**
 * List all repository history entries.
 * Returns entries ordered by use_count DESC, last_used_at DESC.
 */
export async function listRepositoryHistory(): Promise<RepositoryHistoryListResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/settings/repository-history`);
  if (!res.ok) {
    throw new Error(`Failed to list repository history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get repository suggestions based on history.
 * Optionally filter by source type and search query.
 */
export async function getRepositorySuggestions(
  sourceType?: "remote" | "local",
  query?: string,
  limit?: number
): Promise<RepositoryHistoryListResponse> {
  const params = new URLSearchParams();
  if (sourceType) params.append("source_type", sourceType);
  if (query) params.append("query", query);
  if (limit) params.append("limit", limit.toString());

  const queryString = params.toString();
  const url = `${API_BASE}/api/v1/settings/repository-history/suggestions${
    queryString ? `?${queryString}` : ""
  }`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get repository suggestions: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Delete a repository history entry.
 */
export async function deleteRepositoryHistory(id: string): Promise<void> {
  const res = await authFetch(
    `${API_BASE}/api/v1/settings/repository-history/${id}`,
    {
      method: "DELETE",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to delete repository history: ${res.statusText}`);
  }
}

// =============================================================================
// History Types
// =============================================================================

/** History event from the timeline */
export interface HistoryEvent {
  id: string;
  ownerId: string;
  contractId: string | null;
  taskId: string | null;
  eventType: string;
  eventSubtype: string | null;
  phase: string | null;
  eventData: Record<string, unknown>;
  createdAt: string;
}

/** Response for contract history endpoint */
export interface ContractHistoryResponse {
  contractId: string;
  entries: HistoryEvent[];
  totalCount: number;
  cursor: string | null;
}

/** Tool call info in conversation messages */
export interface ToolCallInfo {
  id: string;
  name: string;
  input: Record<string, unknown>;
}

/** Conversation message with optional tool calls */
export interface ConversationMessage {
  id: string;
  role: string;
  content: string;
  timestamp: string;
  toolCalls?: ToolCallInfo[];
  toolName?: string;
  toolInput?: Record<string, unknown>;
  toolResult?: string;
  isError?: boolean;
  tokenCount?: number;
  costUsd?: number;
}

/** Reference to a spawned task in supervisor conversation */
export interface TaskReference {
  taskId: string;
  taskName: string;
  status: string;
  createdAt: string;
  completedAt: string | null;
}

/** Response for supervisor conversation endpoint */
export interface SupervisorConversationResponse {
  contractId: string;
  supervisorTaskId: string;
  phase: string;
  lastActivity: string;
  pendingTaskIds: string[];
  messages: ConversationMessage[];
  spawnedTasks: TaskReference[];
}

/** Response for task conversation endpoint */
export interface TaskConversationResponse {
  taskId: string;
  taskName: string;
  status: string;
  messages: ConversationMessage[];
  totalTokens: number | null;
  totalCost: number | null;
}

/** Query filters for timeline endpoint */
export interface TimelineQueryFilters {
  contractId?: string;
  taskId?: string;
  includeSubtasks?: boolean;
  from?: string;
  to?: string;
  limit?: number;
}

/** Query filters for contract history endpoint */
export interface HistoryQueryFilters {
  phase?: string;
  eventTypes?: string;
  from?: string;
  to?: string;
  limit?: number;
}

/** Task checkpoint */
export interface TaskCheckpoint {
  id: string;
  taskId: string;
  checkpointNumber: number;
  commitSha: string;
  branchName: string;
  message: string;
  filesChanged: Array<{ path: string; action: string }>;
  linesAdded: number;
  linesRemoved: number;
  createdAt: string;
}

// =============================================================================
// History API Functions
// =============================================================================

/**
 * Get contract history timeline.
 */
export async function getContractHistory(
  contractId: string,
  filters?: HistoryQueryFilters
): Promise<ContractHistoryResponse> {
  const params = new URLSearchParams();
  if (filters?.phase) params.append("phase", filters.phase);
  if (filters?.eventTypes) params.append("event_types", filters.eventTypes);
  if (filters?.from) params.append("from", filters.from);
  if (filters?.to) params.append("to", filters.to);
  if (filters?.limit) params.append("limit", filters.limit.toString());

  const query = params.toString();
  const url = `${API_BASE}/api/v1/contracts/${contractId}/history${query ? `?${query}` : ""}`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get contract history: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get supervisor conversation history.
 */
export async function getSupervisorConversation(
  contractId: string
): Promise<SupervisorConversationResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation`
  );
  if (!res.ok) {
    throw new Error(`Failed to get supervisor conversation: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get task conversation history.
 */
export async function getTaskConversation(
  taskId: string,
  options?: { includeToolCalls?: boolean; includeToolResults?: boolean; limit?: number }
): Promise<TaskConversationResponse> {
  const params = new URLSearchParams();
  if (options?.includeToolCalls !== undefined)
    params.append("include_tool_calls", options.includeToolCalls.toString());
  if (options?.includeToolResults !== undefined)
    params.append("include_tool_results", options.includeToolResults.toString());
  if (options?.limit) params.append("limit", options.limit.toString());

  const query = params.toString();
  const url = `${API_BASE}/api/v1/mesh/tasks/${taskId}/conversation${query ? `?${query}` : ""}`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get task conversation: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get unified timeline for current user.
 */
export async function getTimeline(
  filters?: TimelineQueryFilters
): Promise<ContractHistoryResponse> {
  const params = new URLSearchParams();
  if (filters?.contractId) params.append("contract_id", filters.contractId);
  if (filters?.taskId) params.append("task_id", filters.taskId);
  if (filters?.includeSubtasks !== undefined)
    params.append("include_subtasks", filters.includeSubtasks.toString());
  if (filters?.from) params.append("from", filters.from);
  if (filters?.to) params.append("to", filters.to);
  if (filters?.limit) params.append("limit", filters.limit.toString());

  const query = params.toString();
  const url = `${API_BASE}/api/v1/timeline${query ? `?${query}` : ""}`;

  const res = await authFetch(url);
  if (!res.ok) {
    throw new Error(`Failed to get timeline: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get task checkpoints.
 */
export async function getTaskCheckpoints(taskId: string): Promise<TaskCheckpoint[]> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints`);
  if (!res.ok) {
    throw new Error(`Failed to get task checkpoints: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Resume/Rewind/Fork Types
// =============================================================================

/** Request to rewind a task to a checkpoint */
export interface RewindTaskRequest {
  checkpointId?: string;
  checkpointSha?: string;
  preserveMode: "discard" | "create_branch" | "stash";
  branchName?: string;
}

/** Response from task rewind */
export interface RewindTaskResponse {
  taskId: string;
  rewindedTo: {
    checkpointNumber: number;
    sha: string;
    message: string;
  };
  preservedAs?: {
    stateType: string;
    reference: string;
  };
}

/** Request to fork a task from a checkpoint */
export interface ForkTaskRequest {
  forkFromType: "checkpoint" | "timestamp" | "message_id";
  forkFromValue: string;
  newTaskName: string;
  newTaskPlan: string;
  includeConversation?: boolean;
  createBranch?: boolean;
  branchName?: string;
}

/** Response from task fork */
export interface ForkTaskResponse {
  newTaskId: string;
  sourceTaskId: string;
  forkPoint: {
    forkType: string;
    checkpoint?: TaskCheckpoint;
    timestamp: string;
  };
  branchName?: string;
  conversationIncluded: boolean;
  messageCount?: number;
}

/** Request to resume supervisor */
export interface ResumeSupervisorRequest {
  resumeMode: "continue" | "restart_phase" | "from_checkpoint";
  checkpointId?: string;
  additionalContext?: string;
}

/** Response from supervisor resume */
export interface ResumeSupervisorResponse {
  supervisorTaskId: string;
  daemonId: string | null;
  contractId: string;
  phase: string;
  status: string;
  conversationMessageCount: number;
}

/** Request to rewind supervisor conversation */
export interface RewindConversationRequest {
  toMessageId?: string;
  byMessageCount?: number;
  rewindCode?: boolean;
}

/** Response from conversation rewind */
export interface RewindConversationResponse {
  contractId: string;
  messagesRemoved: number;
  newMessageCount: number;
}

// =============================================================================
// Resume/Rewind/Fork API Functions
// =============================================================================

/**
 * Rewind a task to a checkpoint.
 */
export async function rewindTask(
  taskId: string,
  request: RewindTaskRequest
): Promise<RewindTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/rewind`, {
    method: "POST",
    body: JSON.stringify(request),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to rewind task: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Fork a task from a checkpoint.
 */
export async function forkTask(
  taskId: string,
  request: ForkTaskRequest
): Promise<ForkTaskResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/fork`, {
    method: "POST",
    body: JSON.stringify(request),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to fork task: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Resume a supervisor.
 */
export async function resumeSupervisor(
  contractId: string,
  request: ResumeSupervisorRequest
): Promise<ResumeSupervisorResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/supervisor/resume`,
    {
      method: "POST",
      body: JSON.stringify(request),
    }
  );
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to resume supervisor: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Rewind supervisor conversation.
 */
export async function rewindSupervisorConversation(
  contractId: string,
  request: RewindConversationRequest
): Promise<RewindConversationResponse> {
  const res = await authFetch(
    `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation/rewind`,
    {
      method: "POST",
      body: JSON.stringify(request),
    }
  );
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to rewind conversation: ${errorText || res.statusText}`);
  }
  return res.json();
}

/**
 * Resume task from a checkpoint.
 */
export async function resumeFromCheckpoint(
  taskId: string,
  checkpointId: string,
  request: { taskName?: string; plan: string; includeConversation?: boolean }
): Promise<{ taskId: string }> {
  const res = await authFetch(
    `${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints/${checkpointId}/resume`,
    {
      method: "POST",
      body: JSON.stringify(request),
    }
  );
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to resume from checkpoint: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Supervisor/Autopilot Control Functions
// =============================================================================

/**
 * Start a contract's supervisor task (enable autopilot mode).
 * This is a convenience wrapper around startTask.
 */
export async function startSupervisor(supervisorTaskId: string): Promise<Task> {
  return startTask(supervisorTaskId);
}

/**
 * Stop a contract's supervisor task (pause autopilot mode).
 * This is a convenience wrapper around stopTask.
 */
export async function stopSupervisor(supervisorTaskId: string): Promise<Task> {
  return stopTask(supervisorTaskId);
}

/** Status of the supervisor/autopilot for a contract */
export interface SupervisorStatus {
  supervisorTaskId: string | null;
  status: "not_configured" | "pending" | "starting" | "running" | "paused" | "done" | "failed";
  daemonId: string | null;
  canStart: boolean;
  canStop: boolean;
  canResume: boolean;
}

/**
 * Get the supervisor status for a contract.
 */
export function getSupervisorStatus(
  contract: ContractWithRelations
): SupervisorStatus {
  const supervisorTaskId = contract.supervisorTaskId;

  if (!supervisorTaskId) {
    return {
      supervisorTaskId: null,
      status: "not_configured",
      daemonId: null,
      canStart: false,
      canStop: false,
      canResume: false,
    };
  }

  // Find the supervisor task in the contract's tasks
  const supervisorTask = contract.tasks.find(
    (t) => t.id === supervisorTaskId && t.isSupervisor
  );

  if (!supervisorTask) {
    return {
      supervisorTaskId,
      status: "pending",
      daemonId: null,
      canStart: true,
      canStop: false,
      canResume: false,
    };
  }

  // Map task status to supervisor status
  let status: SupervisorStatus["status"];
  let canStart = false;
  let canStop = false;
  let canResume = false;

  switch (supervisorTask.status) {
    case "pending":
      status = "pending";
      canStart = true;
      break;
    case "initializing":
    case "starting":
      status = "starting";
      canStop = true;
      break;
    case "running":
      status = "running";
      canStop = true;
      break;
    case "paused":
    case "blocked":
      status = "paused";
      canResume = true;
      canStop = true;
      break;
    case "done":
    case "merged":
      status = "done";
      break;
    case "failed":
      status = "failed";
      canResume = true;
      break;
    default:
      status = "pending";
      canStart = true;
  }

  return {
    supervisorTaskId,
    status,
    daemonId: null, // Task summary doesn't have daemon_id, would need full task
    canStart,
    canStop,
    canResume,
  };
}

// =============================================================================
// Task Dismiss (Hide) Functions
// =============================================================================

/**
 * Dismiss (hide) a completed standalone task from the UI.
 * This marks the task as hidden so it won't appear in the task list.
 */
export async function dismissTask(taskId: string): Promise<Task> {
  return updateTask(taskId, { hidden: true });
}

// =============================================================================
// Worktree Info Types and Functions
// =============================================================================

/** File status in the worktree (git status) */
export type FileStatus = "M" | "A" | "D" | "R" | "C" | "U" | "?" | "modified" | "added" | "deleted" | "renamed" | "copied" | "unmerged" | "untracked";

/** A single changed file in the worktree */
export interface WorktreeFile {
  /** File path relative to worktree root */
  path: string;
  /** Git status code (M=modified, A=added, D=deleted, R=renamed, C=copied, U=unmerged, ?=untracked) */
  status: FileStatus;
  /** Lines added (0 if deleted or unavailable) */
  linesAdded: number;
  /** Lines removed (0 if added or unavailable) */
  linesRemoved: number;
}

/** Statistics about worktree changes */
export interface WorktreeStats {
  /** Number of files changed */
  filesChanged: number;
  /** Total lines inserted */
  insertions: number;
  /** Total lines deleted */
  deletions: number;
}

/** Worktree information for a task */
export interface WorktreeInfo {
  /** Task ID */
  taskId: string;
  /** Path to the worktree directory */
  worktreePath: string | null;
  /** Whether the worktree exists on the daemon */
  exists: boolean;
  /** Aggregate statistics */
  stats: WorktreeStats;
  /** Changed files list */
  files: WorktreeFile[];
  /** Current branch name */
  branch: string | null;
  /** Current HEAD commit SHA */
  headSha: string | null;
}

/**
 * Get worktree information for a task.
 * Returns changed files, stats, and metadata about the worktree.
 */
export async function getWorktreeInfo(taskId: string): Promise<WorktreeInfo> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/worktree-info`);
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to get worktree info: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Patch Types and Functions
// =============================================================================

/** Summary of a patch file (contract file of type "patch") */
export interface PatchSummary {
  /** Patch/file ID */
  id: string;
  /** Patch name */
  name: string;
  /** Optional description */
  description: string | null;
  /** Task ID this patch was created from */
  taskId: string | null;
  /** Contract ID */
  contractId: string;
  /** Number of files in the patch */
  filesCount: number;
  /** Total lines added */
  linesAdded: number;
  /** Total lines removed */
  linesRemoved: number;
  /** List of file paths in the patch (if available) */
  files: string[] | null;
  /** When the patch was created */
  createdAt: string;
  /** When the patch was last updated */
  updatedAt: string;
}

/**
 * List patches for a task.
 * Returns contract files of type "patch" associated with the task.
 */
export async function listTaskPatches(taskId: string, contractId: string): Promise<PatchSummary[]> {
  const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/patches?contractId=${contractId}`);
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to list patches: ${errorText || res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Chain Types and API
// =============================================================================

/** Chain status */
export type ChainStatus = "pending" | "active" | "completed" | "archived";

/** Chain summary for list view */
export interface ChainSummary {
  id: string;
  name: string;
  description: string | null;
  status: ChainStatus;
  contractCount: number;
  completedContractCount: number;
  loopEnabled: boolean;
  loopCurrentIteration: number | null;
  loopMaxIterations: number | null;
  createdAt: string;
  updatedAt: string;
}

/** Chain repository */
export interface ChainRepository {
  id: string;
  chainId: string;
  name: string;
  repositoryUrl: string | null;
  localPath: string | null;
  sourceType: string;
  status: string;
  isPrimary: boolean;
  createdAt: string;
  updatedAt: string;
}

/** Full chain with contracts */
export interface Chain {
  id: string;
  ownerId: string;
  name: string;
  description: string | null;
  status: ChainStatus;
  loopEnabled: boolean;
  loopMaxIterations: number | null;
  loopCurrentIteration: number | null;
  loopProgressCheck: string | null;
  version: number;
  createdAt: string;
  updatedAt: string;
}

/** Contract detail within a chain */
export interface ChainContractDetail {
  id: string;
  chainId: string;
  contractId: string;
  contractName: string;
  contractStatus: string;
  contractPhase: string;
  dependsOn: string[];
  orderIndex: number;
  editorX: number | null;
  editorY: number | null;
  createdAt: string;
}

/** Chain with contracts (chain fields are flattened via serde(flatten)) */
export interface ChainWithContracts extends Chain {
  contracts: ChainContractDetail[];
  repositories: ChainRepository[];
}

/** Node in chain graph visualization */
export interface ChainGraphNode {
  id: string;
  contractId: string;
  name: string;
  status: string;
  phase: string;
  x: number;
  y: number;
}

/** Edge in chain graph */
export interface ChainGraphEdge {
  from: string;
  to: string;
}

/** Chain graph response */
export interface ChainGraphResponse {
  chainId: string;
  chainName: string;
  chainStatus: string;
  nodes: ChainGraphNode[];
  edges: ChainGraphEdge[];
}

/** Chain event */
export interface ChainEvent {
  id: string;
  chainId: string;
  eventType: string;
  contractId: string | null;
  eventData: Record<string, unknown> | null;
  createdAt: string;
}

/** Chain list response */
export interface ChainListResponse {
  chains: ChainSummary[];
  total: number;
}

/** Add chain repository request */
export interface AddChainRepositoryRequest {
  name: string;
  repositoryUrl?: string;
  localPath?: string;
  sourceType?: string;
  isPrimary?: boolean;
}

/** Create chain request */
export interface CreateChainRequest {
  name: string;
  description?: string;
  repositoryUrl?: string; // Legacy field for backwards compatibility
  repositories?: AddChainRepositoryRequest[];
  loopEnabled?: boolean;
  loopMaxIterations?: number;
  loopProgressCheck?: string;
  contracts?: CreateChainContractRequest[];
}

/** Create chain contract request */
export interface CreateChainContractRequest {
  name: string;
  description?: string;
  contractType?: string;
  initialPhase?: string;
  phases?: string[];
  dependsOn?: string[];
  tasks?: { name: string; plan: string }[];
  deliverables?: { id: string; name: string; priority?: string }[];
  editorX?: number;
  editorY?: number;
}

/** Update chain request */
export interface UpdateChainRequest {
  name?: string;
  description?: string;
  status?: ChainStatus;
  loopEnabled?: boolean;
  loopMaxIterations?: number;
  loopProgressCheck?: string;
  version?: number;
}

/** List chains */
export async function listChains(
  status?: ChainStatus,
  limit = 50,
  offset = 0
): Promise<ChainListResponse> {
  const params = new URLSearchParams();
  if (status) params.set("status", status);
  params.set("limit", String(limit));
  params.set("offset", String(offset));

  const res = await authFetch(`${API_BASE}/api/v1/chains?${params}`);
  if (!res.ok) {
    throw new Error(`Failed to list chains: ${res.statusText}`);
  }
  return res.json();
}

/** Get chain by ID */
export async function getChain(chainId: string): Promise<ChainWithContracts> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}`);
  if (!res.ok) {
    throw new Error(`Failed to get chain: ${res.statusText}`);
  }
  return res.json();
}

/** Create a new chain */
export async function createChain(req: CreateChainRequest): Promise<Chain> {
  const res = await authFetch(`${API_BASE}/api/v1/chains`, {
    method: "POST",
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to create chain: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Update a chain */
export async function updateChain(
  chainId: string,
  req: UpdateChainRequest
): Promise<Chain> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}`, {
    method: "PUT",
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to update chain: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Archive a chain */
export async function archiveChain(chainId: string): Promise<{ archived: boolean }> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to archive chain: ${res.statusText}`);
  }
  return res.json();
}

/** Get chain contracts */
export async function getChainContracts(
  chainId: string
): Promise<ChainContractDetail[]> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/contracts`);
  if (!res.ok) {
    throw new Error(`Failed to get chain contracts: ${res.statusText}`);
  }
  return res.json();
}

/** Get chain graph for visualization */
export async function getChainGraph(chainId: string): Promise<ChainGraphResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/graph`);
  if (!res.ok) {
    throw new Error(`Failed to get chain graph: ${res.statusText}`);
  }
  return res.json();
}

/** Get chain events */
export async function getChainEvents(chainId: string): Promise<ChainEvent[]> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/events`);
  if (!res.ok) {
    throw new Error(`Failed to get chain events: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Chain Contract Definitions
// =============================================================================

/** Task definition for chain contract definitions */
export interface ChainTaskDefinition {
  name: string;
  plan: string;
}

/** Deliverable definition for chain contract definitions (optional priority) */
export interface ChainDeliverableDefinition {
  id: string;
  name: string;
  priority?: string;
}

/** Validation configuration for checkpoint contracts */
export interface CheckpointValidation {
  /** Check that all required deliverables from upstream contracts exist */
  checkDeliverables?: boolean;
  /** Run tests in the repository */
  runTests?: boolean;
  /** Custom validation instructions for Claude */
  checkContent?: string;
  /** Action on failure: "block", "retry", "warn" */
  onFailure?: "block" | "retry" | "warn";
  /** Max retry attempts for upstream contracts */
  maxRetries?: number;
}

/** Contract definition stored in chain (before actual contract is created) */
export interface ChainContractDefinition {
  id: string;
  chainId: string;
  name: string;
  description: string | null;
  contractType: string;
  initialPhase: string | null;
  dependsOnNames: string[];
  tasks: ChainTaskDefinition[] | null;
  deliverables: ChainDeliverableDefinition[] | null;
  /** Validation config for checkpoint contracts */
  validation: CheckpointValidation | null;
  editorX: number | null;
  editorY: number | null;
  orderIndex: number;
  createdAt: string;
}

/** Request to add a contract definition to a chain */
export interface AddContractDefinitionRequest {
  name: string;
  description?: string;
  contractType?: string;
  initialPhase?: string;
  dependsOn?: string[];
  tasks?: ChainTaskDefinition[];
  deliverables?: ChainDeliverableDefinition[];
  /** Validation config (for checkpoint contracts) */
  validation?: CheckpointValidation;
  editorX?: number;
  editorY?: number;
  orderIndex?: number;
}

/** Request to update a contract definition */
export interface UpdateContractDefinitionRequest {
  name?: string;
  description?: string;
  contractType?: string;
  initialPhase?: string;
  dependsOn?: string[];
  tasks?: ChainTaskDefinition[];
  deliverables?: ChainDeliverableDefinition[];
  /** Validation config (for checkpoint contracts) */
  validation?: CheckpointValidation;
  editorX?: number;
  editorY?: number;
  orderIndex?: number;
}

/** Response when starting a chain */
export interface StartChainResponse {
  chainId: string;
  contractsCreated: string[];
  status: string;
}

/** Node in definition graph (shows definitions + instantiation status) */
export interface ChainDefinitionGraphNode {
  id: string;
  name: string;
  contractType: string;
  x: number;
  y: number;
  isInstantiated: boolean;
  contractId: string | null;
  contractStatus: string | null;
}

/** Definition graph response */
export interface ChainDefinitionGraphResponse {
  chainId: string;
  chainName: string;
  chainStatus: string;
  nodes: ChainDefinitionGraphNode[];
  edges: ChainGraphEdge[];
}

/** List contract definitions for a chain */
export async function listChainDefinitions(
  chainId: string
): Promise<ChainContractDefinition[]> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/definitions`);
  if (!res.ok) {
    throw new Error(`Failed to list chain definitions: ${res.statusText}`);
  }
  return res.json();
}

/** Create a contract definition for a chain */
export async function createChainDefinition(
  chainId: string,
  req: AddContractDefinitionRequest
): Promise<ChainContractDefinition> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/definitions`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    throw new Error(`Failed to create chain definition: ${res.statusText}`);
  }
  return res.json();
}

/** Update a contract definition */
export async function updateChainDefinition(
  chainId: string,
  definitionId: string,
  req: UpdateContractDefinitionRequest
): Promise<ChainContractDefinition> {
  const res = await authFetch(
    `${API_BASE}/api/v1/chains/${chainId}/definitions/${definitionId}`,
    {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(req),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to update chain definition: ${res.statusText}`);
  }
  return res.json();
}

/** Delete a contract definition */
export async function deleteChainDefinition(
  chainId: string,
  definitionId: string
): Promise<{ deleted: boolean }> {
  const res = await authFetch(
    `${API_BASE}/api/v1/chains/${chainId}/definitions/${definitionId}`,
    { method: "DELETE" }
  );
  if (!res.ok) {
    throw new Error(`Failed to delete chain definition: ${res.statusText}`);
  }
  return res.json();
}

/** Get definition graph for a chain */
export async function getChainDefinitionGraph(
  chainId: string
): Promise<ChainDefinitionGraphResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/definitions/graph`);
  if (!res.ok) {
    throw new Error(`Failed to get chain definition graph: ${res.statusText}`);
  }
  return res.json();
}

/** Start a chain (creates root contracts based on DAG) */
export async function startChain(chainId: string): Promise<StartChainResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/start`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({}),
  });
  if (!res.ok) {
    const error = await res.json().catch(() => ({ message: res.statusText }));
    throw new Error(error.message || `Failed to start chain: ${res.statusText}`);
  }
  return res.json();
}

/** Stop a chain (marks as archived) */
export async function stopChain(chainId: string): Promise<{ stopped: boolean; status: string }> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/stop`, {
    method: "POST",
  });
  if (!res.ok) {
    const error = await res.json().catch(() => ({ message: res.statusText }));
    throw new Error(error.message || `Failed to stop chain: ${res.statusText}`);
  }
  return res.json();
}

// ============================================================================
// Chain Repository Operations
// ============================================================================

/** List repositories for a chain */
export async function listChainRepositories(chainId: string): Promise<ChainRepository[]> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/repositories`);
  if (!res.ok) {
    throw new Error(`Failed to list chain repositories: ${res.statusText}`);
  }
  return res.json();
}

/** Add a repository to a chain */
export async function addChainRepository(
  chainId: string,
  req: AddChainRepositoryRequest
): Promise<ChainRepository> {
  const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/repositories`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    const error = await res.json().catch(() => ({ message: res.statusText }));
    throw new Error(error.message || `Failed to add chain repository: ${res.statusText}`);
  }
  return res.json();
}

/** Delete a repository from a chain */
export async function deleteChainRepository(
  chainId: string,
  repositoryId: string
): Promise<{ deleted: boolean }> {
  const res = await authFetch(
    `${API_BASE}/api/v1/chains/${chainId}/repositories/${repositoryId}`,
    { method: "DELETE" }
  );
  if (!res.ok) {
    throw new Error(`Failed to delete chain repository: ${res.statusText}`);
  }
  return res.json();
}

/** Set a repository as primary for a chain */
export async function setChainRepositoryPrimary(
  chainId: string,
  repositoryId: string
): Promise<ChainRepository> {
  const res = await authFetch(
    `${API_BASE}/api/v1/chains/${chainId}/repositories/${repositoryId}/primary`,
    { method: "PUT" }
  );
  if (!res.ok) {
    throw new Error(`Failed to set chain repository as primary: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Directive Types and API
// =============================================================================

/** Directive status */
export type DirectiveStatus =
  | "draft"
  | "planning"
  | "active"
  | "paused"
  | "completed"
  | "archived"
  | "failed";

/** Autonomy level */
export type AutonomyLevel = "full_auto" | "guardrails" | "manual";

/** Confidence level (traffic light) */
export type ConfidenceLevel = "green" | "yellow" | "red";

/** Step status */
export type StepStatus =
  | "pending"
  | "ready"
  | "running"
  | "evaluating"
  | "passed"
  | "failed"
  | "rework"
  | "skipped"
  | "blocked";

/** Evaluation type */
export type EvaluationType = "programmatic" | "llm" | "composite" | "manual";

/** Directive summary for list view */
export interface DirectiveSummary {
  id: string;
  title: string;
  goal: string;
  status: DirectiveStatus;
  autonomyLevel: AutonomyLevel;
  repositoryUrl: string | null;
  currentChainId: string | null;
  currentChainGeneration: number | null;
  totalSteps: number;
  completedSteps: number;
  failedSteps: number;
  currentConfidence: number | null;
  totalCostUsd: number;
  createdAt: string;
  updatedAt: string;
}

/** Full directive */
export interface Directive {
  id: string;
  ownerId: string;
  title: string;
  goal: string;
  status: DirectiveStatus;
  autonomyLevel: AutonomyLevel;
  repositoryUrl: string | null;
  localPath: string | null;
  requirements: unknown | null;
  acceptanceCriteria: unknown | null;
  constraints: unknown | null;
  confidenceThresholdGreen: number;
  confidenceThresholdYellow: number;
  maxReworkCycles: number;
  maxTotalCostUsd: number | null;
  maxWallTimeMinutes: number | null;
  maxChainRegenerations: number;
  totalCostUsd: number;
  currentChainId: string | null;
  version: number;
  createdAt: string;
  updatedAt: string;
  startedAt: string | null;
  completedAt: string | null;
}

/** Directive chain */
export interface DirectiveChain {
  id: string;
  directiveId: string;
  generation: number;
  name: string;
  description: string | null;
  rationale: string | null;
  planningModel: string | null;
  status: string;
  totalSteps: number;
  completedSteps: number;
  failedSteps: number;
  currentConfidence: number | null;
  startedAt: string | null;
  completedAt: string | null;
  version: number;
  createdAt: string;
  updatedAt: string;
}

/** Chain step */
export interface ChainStep {
  id: string;
  chainId: string;
  name: string;
  description: string | null;
  stepType: string;
  contractType: string;
  initialPhase: string | null;
  taskPlan: string | null;
  phases: string[];
  dependsOn: string[];
  parallelGroup: string | null;
  requirementIds: string[];
  acceptanceCriteriaIds: string[];
  verifierConfig: unknown;
  status: StepStatus;
  contractId: string | null;
  supervisorTaskId: string | null;
  confidenceScore: number | null;
  confidenceLevel: ConfidenceLevel | null;
  evaluationCount: number;
  reworkCount: number;
  lastEvaluationId: string | null;
  editorX: number | null;
  editorY: number | null;
  startedAt: string | null;
  completedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

/** Directive with progress info */
export interface DirectiveWithProgress extends Directive {
  chain: DirectiveChain | null;
  steps: ChainStep[];
  recentEvents: DirectiveEvent[];
  pendingApprovals: DirectiveApproval[];
}

/** Directive evaluation */
export interface DirectiveEvaluation {
  id: string;
  directiveId: string;
  chainId: string | null;
  stepId: string | null;
  evaluationType: EvaluationType;
  passed: boolean;
  overallScore: number;
  confidenceLevel: ConfidenceLevel;
  programmaticResults: unknown | null;
  llmResults: unknown | null;
  compositeBreakdown: unknown | null;
  feedback: string | null;
  reworkInstructions: string | null;
  verifierIds: string[];
  evaluatedBy: string | null;
  createdAt: string;
}

/** Directive event */
export interface DirectiveEvent {
  id: string;
  directiveId: string;
  chainId: string | null;
  stepId: string | null;
  eventType: string;
  severity: string;
  eventData: unknown | null;
  actorType: string;
  actorId: string | null;
  createdAt: string;
}

/** Directive approval */
export interface DirectiveApproval {
  id: string;
  directiveId: string;
  chainId: string | null;
  stepId: string | null;
  approvalType: string;
  description: string;
  context: unknown | null;
  urgency: string;
  status: string;
  requestedAt: string;
  resolvedAt: string | null;
  resolvedBy: string | null;
  response: string | null;
}

/** Directive verifier */
export interface DirectiveVerifier {
  id: string;
  directiveId: string;
  name: string;
  verifierType: string;
  command: string | null;
  workingDirectory: string | null;
  timeoutSeconds: number;
  environment: unknown;
  autoDetect: boolean;
  detectFiles: string[];
  weight: number;
  required: boolean;
  enabled: boolean;
  lastRunAt: string | null;
  lastResult: unknown | null;
  createdAt: string;
  updatedAt: string;
}

/** Directive graph node */
export interface DirectiveGraphNode {
  id: string;
  name: string;
  stepType: string;
  status: StepStatus;
  confidenceScore: number | null;
  confidenceLevel: ConfidenceLevel | null;
  contractId: string | null;
  editorX: number | null;
  editorY: number | null;
}

/** Directive graph edge */
export interface DirectiveGraphEdge {
  source: string;
  target: string;
}

/** Directive graph response */
export interface DirectiveGraphResponse {
  chainId: string;
  directiveId: string;
  nodes: DirectiveGraphNode[];
  edges: DirectiveGraphEdge[];
}

/** Create directive request */
export interface CreateDirectiveRequest {
  goal: string;
  repositoryUrl?: string;
  localPath?: string;
  autonomyLevel?: AutonomyLevel;
  confidenceThresholdGreen?: number;
  confidenceThresholdYellow?: number;
  maxReworkCycles?: number;
  maxTotalCostUsd?: number;
  maxWallTimeMinutes?: number;
}

/** Update directive request */
export interface UpdateDirectiveRequest {
  title?: string;
  goal?: string;
  requirements?: unknown;
  acceptanceCriteria?: unknown;
  constraints?: unknown;
  autonomyLevel?: AutonomyLevel;
  confidenceThresholdGreen?: number;
  confidenceThresholdYellow?: number;
  maxReworkCycles?: number;
  maxTotalCostUsd?: number;
  maxWallTimeMinutes?: number;
  version?: number;
}

/** Add step request */
export interface AddStepRequest {
  name: string;
  description?: string;
  stepType?: string;
  contractType?: string;
  initialPhase?: string;
  taskPlan?: string;
  phases?: string[];
  dependsOn?: string[];
  parallelGroup?: string;
  requirementIds?: string[];
  acceptanceCriteriaIds?: string[];
  verifierConfig?: unknown;
  editorX?: number;
  editorY?: number;
}

/** Update step request */
export interface UpdateStepRequest {
  name?: string;
  description?: string;
  initialPhase?: string;
  taskPlan?: string;
  phases?: string[];
  dependsOn?: string[];
  parallelGroup?: string;
  requirementIds?: string[];
  acceptanceCriteriaIds?: string[];
  verifierConfig?: unknown;
  editorX?: number;
  editorY?: number;
}

/** Create verifier request */
export interface CreateVerifierRequest {
  name: string;
  verifierType: string;
  command?: string;
  workingDirectory?: string;
  timeoutSeconds?: number;
  weight?: number;
  required?: boolean;
  enabled?: boolean;
}

/** Update verifier request */
export interface UpdateVerifierRequest {
  command?: string;
  weight?: number;
  required?: boolean;
  enabled?: boolean;
}

/** Approval action request */
export interface ApprovalActionRequest {
  response?: string;
}

/** Start directive response */
export interface StartDirectiveResponse {
  directiveId: string;
  chainId: string;
  chainGeneration: number;
  steps: ChainStep[];
  status: string;
}

/** Directive list response */
export interface DirectiveListResponse {
  directives: DirectiveSummary[];
  total: number;
}

// =============================================================================
// Directive API Functions
// =============================================================================

/** List directives */
export async function listDirectives(
  status?: DirectiveStatus,
  limit = 50,
  offset = 0
): Promise<DirectiveListResponse> {
  const params = new URLSearchParams();
  if (status) params.set("status", status);
  params.set("limit", String(limit));
  params.set("offset", String(offset));

  const res = await authFetch(`${API_BASE}/api/v1/directives?${params}`);
  if (!res.ok) {
    throw new Error(`Failed to list directives: ${res.statusText}`);
  }
  return res.json();
}

/** Get directive by ID */
export async function getDirective(directiveId: string): Promise<DirectiveWithProgress> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`);
  if (!res.ok) {
    throw new Error(`Failed to get directive: ${res.statusText}`);
  }
  return res.json();
}

/** Create a new directive */
export async function createDirective(req: CreateDirectiveRequest): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives`, {
    method: "POST",
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    const errorText = await res.text();
    throw new Error(`Failed to create directive: ${errorText || res.statusText}`);
  }
  return res.json();
}

/** Update directive */
export async function updateDirective(
  directiveId: string,
  req: UpdateDirectiveRequest
): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`, {
    method: "PUT",
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    throw new Error(`Failed to update directive: ${res.statusText}`);
  }
  return res.json();
}

/** Archive directive */
export async function archiveDirective(directiveId: string): Promise<{ archived: boolean }> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`, {
    method: "DELETE",
  });
  if (!res.ok) {
    throw new Error(`Failed to archive directive: ${res.statusText}`);
  }
  return res.json();
}

/** Start directive */
export async function startDirective(directiveId: string): Promise<StartDirectiveResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/start`, {
    method: "POST",
  });
  if (!res.ok) {
    throw new Error(`Failed to start directive: ${res.statusText}`);
  }
  return res.json();
}

/** Pause directive */
export async function pauseDirective(directiveId: string): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/pause`, {
    method: "POST",
  });
  if (!res.ok) {
    throw new Error(`Failed to pause directive: ${res.statusText}`);
  }
  return res.json();
}

/** Resume directive */
export async function resumeDirective(directiveId: string): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/resume`, {
    method: "POST",
  });
  if (!res.ok) {
    throw new Error(`Failed to resume directive: ${res.statusText}`);
  }
  return res.json();
}

/** Stop directive */
export async function stopDirective(directiveId: string): Promise<Directive> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/stop`, {
    method: "POST",
  });
  if (!res.ok) {
    throw new Error(`Failed to stop directive: ${res.statusText}`);
  }
  return res.json();
}

/** Get directive chain */
export async function getDirectiveChain(
  directiveId: string
): Promise<{ chain: DirectiveChain | null; steps: ChainStep[] }> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain`);
  if (!res.ok) {
    throw new Error(`Failed to get directive chain: ${res.statusText}`);
  }
  return res.json();
}

/** Get directive chain graph */
export async function getDirectiveGraph(directiveId: string): Promise<DirectiveGraphResponse> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/graph`);
  if (!res.ok) {
    throw new Error(`Failed to get directive graph: ${res.statusText}`);
  }
  return res.json();
}

/** Replan directive chain */
export async function replanDirectiveChain(directiveId: string): Promise<DirectiveChain> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/replan`, {
    method: "POST",
  });
  if (!res.ok) {
    throw new Error(`Failed to replan directive chain: ${res.statusText}`);
  }
  return res.json();
}

/** Add step to directive chain */
export async function addDirectiveStep(
  directiveId: string,
  req: AddStepRequest
): Promise<ChainStep> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/steps`, {
    method: "POST",
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    throw new Error(`Failed to add step: ${res.statusText}`);
  }
  return res.json();
}

/** Get step details */
export async function getDirectiveStep(
  directiveId: string,
  stepId: string
): Promise<ChainStep> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`);
  if (!res.ok) {
    throw new Error(`Failed to get step: ${res.statusText}`);
  }
  return res.json();
}

/** Update step */
export async function updateDirectiveStep(
  directiveId: string,
  stepId: string,
  req: UpdateStepRequest
): Promise<ChainStep> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/chain/steps/${stepId}`,
    {
      method: "PUT",
      body: JSON.stringify(req),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to update step: ${res.statusText}`);
  }
  return res.json();
}

/** Delete step */
export async function deleteDirectiveStep(
  directiveId: string,
  stepId: string
): Promise<{ deleted: boolean }> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/chain/steps/${stepId}`,
    {
      method: "DELETE",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to delete step: ${res.statusText}`);
  }
  return res.json();
}

/** Skip step */
export async function skipDirectiveStep(
  directiveId: string,
  stepId: string
): Promise<ChainStep> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/skip`,
    {
      method: "POST",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to skip step: ${res.statusText}`);
  }
  return res.json();
}

/** List directive evaluations */
export async function listDirectiveEvaluations(
  directiveId: string,
  limit = 50
): Promise<DirectiveEvaluation[]> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/evaluations?limit=${limit}`
  );
  if (!res.ok) {
    throw new Error(`Failed to list evaluations: ${res.statusText}`);
  }
  return res.json();
}

/** List directive events */
export async function listDirectiveEvents(
  directiveId: string,
  limit = 50
): Promise<DirectiveEvent[]> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/events?limit=${limit}`
  );
  if (!res.ok) {
    throw new Error(`Failed to list events: ${res.statusText}`);
  }
  return res.json();
}

/** List directive verifiers */
export async function listDirectiveVerifiers(
  directiveId: string
): Promise<DirectiveVerifier[]> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/verifiers`);
  if (!res.ok) {
    throw new Error(`Failed to list verifiers: ${res.statusText}`);
  }
  return res.json();
}

/** Add verifier */
export async function addDirectiveVerifier(
  directiveId: string,
  req: CreateVerifierRequest
): Promise<DirectiveVerifier> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/verifiers`, {
    method: "POST",
    body: JSON.stringify(req),
  });
  if (!res.ok) {
    throw new Error(`Failed to add verifier: ${res.statusText}`);
  }
  return res.json();
}

/** Update verifier */
export async function updateDirectiveVerifier(
  directiveId: string,
  verifierId: string,
  req: UpdateVerifierRequest
): Promise<DirectiveVerifier> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/verifiers/${verifierId}`,
    {
      method: "PUT",
      body: JSON.stringify(req),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to update verifier: ${res.statusText}`);
  }
  return res.json();
}

/** Auto-detect verifiers */
export async function autoDetectVerifiers(
  directiveId: string
): Promise<DirectiveVerifier[]> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/verifiers/auto-detect`,
    {
      method: "POST",
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to auto-detect verifiers: ${res.statusText}`);
  }
  return res.json();
}

/** List pending approvals */
export async function listDirectiveApprovals(
  directiveId: string
): Promise<DirectiveApproval[]> {
  const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/approvals`);
  if (!res.ok) {
    throw new Error(`Failed to list approvals: ${res.statusText}`);
  }
  return res.json();
}

/** Approve request */
export async function approveDirectiveRequest(
  directiveId: string,
  approvalId: string,
  req?: ApprovalActionRequest
): Promise<DirectiveApproval> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/approvals/${approvalId}/approve`,
    {
      method: "POST",
      body: JSON.stringify(req || {}),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to approve request: ${res.statusText}`);
  }
  return res.json();
}

/** Deny request */
export async function denyDirectiveRequest(
  directiveId: string,
  approvalId: string,
  req?: ApprovalActionRequest
): Promise<DirectiveApproval> {
  const res = await authFetch(
    `${API_BASE}/api/v1/directives/${directiveId}/approvals/${approvalId}/deny`,
    {
      method: "POST",
      body: JSON.stringify(req || {}),
    }
  );
  if (!res.ok) {
    throw new Error(`Failed to deny request: ${res.statusText}`);
  }
  return res.json();
}

/** Subscribe to directive events via SSE */
export async function subscribeToDirectiveEvents(
  directiveId: string,
  onEvent: (event: DirectiveEvent) => void,
  onError?: (error: Error) => void
): Promise<() => void> {
  // Get auth token for the request
  let authToken: string | null = null;
  if (supabase) {
    const { data: { session } } = await supabase.auth.getSession();
    if (session?.access_token) {
      authToken = session.access_token;
    }
  }

  // Build URL with auth token as query param (since EventSource doesn't support headers)
  const url = new URL(`${API_BASE}/api/v1/directives/${directiveId}/events/stream`);
  if (authToken) {
    url.searchParams.set("token", authToken);
  } else {
    const apiKey = getStoredApiKey();
    if (apiKey) {
      url.searchParams.set("api_key", apiKey);
    }
  }

  // Create EventSource connection
  const eventSource = new EventSource(url.toString());

  eventSource.onmessage = (e) => {
    try {
      const event = JSON.parse(e.data) as DirectiveEvent;
      onEvent(event);
    } catch (err) {
      console.error("Failed to parse SSE event:", err);
    }
  };

  eventSource.onerror = (_e) => {
    if (onError) {
      onError(new Error("SSE connection error"));
    }
  };

  // Return cleanup function
  return () => {
    eventSource.close();
  };
}