@grackle-ai/server 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ws-bridge.js CHANGED
@@ -4,7 +4,7 @@ import { grackle, powerline } from "@grackle-ai/common";
4
4
  import * as envRegistry from "./env-registry.js";
5
5
  import * as sessionStore from "./session-store.js";
6
6
  import * as adapterManager from "./adapter-manager.js";
7
- import { reconnectOrProvision } from "./adapters/adapter.js";
7
+ import { reconnectOrProvision, } from "./adapters/adapter.js";
8
8
  import * as streamHub from "./stream-hub.js";
9
9
  import * as tokenBroker from "./token-broker.js";
10
10
  import * as projectStore from "./project-store.js";
@@ -110,7 +110,10 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
110
110
  }
111
111
  const adapter = adapterManager.getAdapter(env.adapterType);
112
112
  if (!adapter) {
113
- sendWs(ws, { type: "error", payload: { message: `No adapter for type: ${env.adapterType}` } });
113
+ sendWs(ws, {
114
+ type: "error",
115
+ payload: { message: `No adapter for type: ${env.adapterType}` },
116
+ });
114
117
  return undefined;
115
118
  }
116
119
  logger.info({ environmentId, ...logContext }, "Auto-provisioning environment");
@@ -123,7 +126,12 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
123
126
  logger.info({ environmentId, stage: provEvent.stage, ...logContext }, "Auto-provision progress");
124
127
  broadcast({
125
128
  type: "provision_progress",
126
- payload: { environmentId, stage: provEvent.stage, message: provEvent.message, progress: provEvent.progress },
129
+ payload: {
130
+ environmentId,
131
+ stage: provEvent.stage,
132
+ message: provEvent.message,
133
+ progress: provEvent.progress,
134
+ },
127
135
  });
128
136
  }
129
137
  conn = await adapter.connect(environmentId, config, powerlineToken);
@@ -136,7 +144,12 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
136
144
  logger.info({ environmentId, ...logContext }, "Auto-provision complete");
137
145
  broadcast({
138
146
  type: "provision_progress",
139
- payload: { environmentId, stage: "ready", message: "Environment connected", progress: 1 },
147
+ payload: {
148
+ environmentId,
149
+ stage: "ready",
150
+ message: "Environment connected",
151
+ progress: 1,
152
+ },
140
153
  });
141
154
  return conn;
142
155
  }
@@ -147,9 +160,19 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
147
160
  const errorMessage = err instanceof Error ? err.message : String(err);
148
161
  broadcast({
149
162
  type: "provision_progress",
150
- payload: { environmentId, stage: "error", message: `Auto-provision failed: ${errorMessage}`, progress: 0 },
163
+ payload: {
164
+ environmentId,
165
+ stage: "error",
166
+ message: `Auto-provision failed: ${errorMessage}`,
167
+ progress: 0,
168
+ },
169
+ });
170
+ sendWs(ws, {
171
+ type: "error",
172
+ payload: {
173
+ message: `Failed to auto-connect environment ${environmentId}: ${errorMessage}`,
174
+ },
151
175
  });
152
- sendWs(ws, { type: "error", payload: { message: `Failed to auto-connect environment ${environmentId}: ${errorMessage}` } });
153
176
  return undefined;
154
177
  }
155
178
  }
@@ -173,19 +196,29 @@ async function startTaskSession(ws, task, options) {
173
196
  logger.warn({ taskId: task.id, environmentId }, "startTaskSession failed: environment not found");
174
197
  return `Environment not found: ${environmentId}`;
175
198
  }
176
- const conn = await autoProvisionEnvironment(ws, environmentId, env, { taskId: task.id });
199
+ const conn = await autoProvisionEnvironment(ws, environmentId, env, {
200
+ taskId: task.id,
201
+ });
177
202
  if (!conn) {
178
203
  return undefined;
179
204
  }
180
205
  // Resolve persona
181
206
  const resolvedPersonaId = options?.personaId || task.personaId;
182
- const persona = resolvedPersonaId ? personaStore.getPersona(resolvedPersonaId) : undefined;
207
+ const persona = resolvedPersonaId
208
+ ? personaStore.getPersona(resolvedPersonaId)
209
+ : undefined;
183
210
  if (resolvedPersonaId && !persona) {
184
211
  return `Persona not found: ${resolvedPersonaId}`;
185
212
  }
186
213
  const sessionId = uuid();
187
- const runtime = options?.runtime || persona?.runtime || env.defaultRuntime || DEFAULT_RUNTIME;
188
- const model = options?.model || persona?.model || process.env.GRACKLE_DEFAULT_MODEL || DEFAULT_MODEL;
214
+ const runtime = options?.runtime ||
215
+ persona?.runtime ||
216
+ env.defaultRuntime ||
217
+ DEFAULT_RUNTIME;
218
+ const model = options?.model ||
219
+ persona?.model ||
220
+ process.env.GRACKLE_DEFAULT_MODEL ||
221
+ DEFAULT_MODEL;
189
222
  const maxTurns = persona?.maxTurns || 0;
190
223
  const logPath = join(grackleHome, LOGS_DIR, sessionId);
191
224
  const freshTask = taskStore.getTask(task.id) || task;
@@ -196,7 +229,14 @@ async function startTaskSession(ws, task, options) {
196
229
  sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath);
197
230
  taskStore.setTaskSession(freshTask.id, sessionId);
198
231
  taskStore.markTaskStarted(freshTask.id);
199
- broadcast({ type: "task_started", payload: { taskId: freshTask.id, sessionId, projectId: freshTask.projectId } });
232
+ broadcast({
233
+ type: "task_started",
234
+ payload: {
235
+ taskId: freshTask.id,
236
+ sessionId,
237
+ projectId: freshTask.projectId,
238
+ },
239
+ });
200
240
  let mcpServersJson = "";
201
241
  if (persona) {
202
242
  try {
@@ -219,7 +259,9 @@ async function startTaskSession(ws, task, options) {
219
259
  model,
220
260
  maxTurns,
221
261
  branch: freshTask.branch,
222
- worktreeBasePath: freshTask.branch ? (process.env.GRACKLE_WORKTREE_BASE || "/workspace") : "",
262
+ worktreeBasePath: freshTask.branch
263
+ ? process.env.GRACKLE_WORKTREE_BASE || "/workspace"
264
+ : "",
223
265
  systemContext,
224
266
  projectId: freshTask.projectId,
225
267
  taskId: freshTask.id,
@@ -240,7 +282,10 @@ async function startTaskSession(ws, task, options) {
240
282
  else if (sess?.status === "failed") {
241
283
  taskStore.markTaskCompleted(freshTask.id, "failed");
242
284
  }
243
- broadcast({ type: "task_updated", payload: { taskId: freshTask.id, projectId: freshTask.projectId } });
285
+ broadcast({
286
+ type: "task_updated",
287
+ payload: { taskId: freshTask.id, projectId: freshTask.projectId },
288
+ });
244
289
  }
245
290
  },
246
291
  });
@@ -357,12 +402,18 @@ async function handleMessage(ws, msg, subscriptions) {
357
402
  const branch = msg.payload?.branch || "";
358
403
  const systemContext = msg.payload?.systemContext || "";
359
404
  if (!environmentId || !prompt) {
360
- sendWs(ws, { type: "error", payload: { message: "environmentId and prompt required" } });
405
+ sendWs(ws, {
406
+ type: "error",
407
+ payload: { message: "environmentId and prompt required" },
408
+ });
361
409
  return;
362
410
  }
363
411
  const env = envRegistry.getEnvironment(environmentId);
364
412
  if (!env) {
365
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
413
+ sendWs(ws, {
414
+ type: "error",
415
+ payload: { message: `Environment not found: ${environmentId}` },
416
+ });
366
417
  return;
367
418
  }
368
419
  // Auto-provision the environment if not already connected
@@ -407,29 +458,59 @@ async function handleMessage(ws, msg, subscriptions) {
407
458
  const sessionId = msg.payload?.sessionId;
408
459
  const text = msg.payload?.text;
409
460
  if (!sessionId || !text) {
410
- sendWs(ws, { type: "error", payload: { message: "sessionId and text required" } });
461
+ sendWs(ws, {
462
+ type: "error",
463
+ payload: { message: "sessionId and text required" },
464
+ });
411
465
  return;
412
466
  }
413
467
  const session = sessionStore.getSession(sessionId);
414
468
  if (!session) {
415
- sendWs(ws, { type: "error", payload: { message: `Session not found: ${sessionId}` } });
469
+ sendWs(ws, {
470
+ type: "error",
471
+ payload: { message: `Session not found: ${sessionId}` },
472
+ });
416
473
  return;
417
474
  }
418
475
  if (session.status !== "waiting_input") {
419
- sendWs(ws, { type: "error", payload: { message: `Session ${sessionId} is not currently waiting for input (status: ${session.status})` } });
476
+ sendWs(ws, {
477
+ type: "error",
478
+ payload: {
479
+ message: `Session ${sessionId} is not currently waiting for input (status: ${session.status})`,
480
+ },
481
+ });
420
482
  return;
421
483
  }
422
484
  const conn = adapterManager.getConnection(session.environmentId);
423
485
  if (!conn) {
424
- sendWs(ws, { type: "error", payload: { message: `Environment ${session.environmentId} is not connected` } });
486
+ sendWs(ws, {
487
+ type: "error",
488
+ payload: {
489
+ message: `Environment ${session.environmentId} is not connected`,
490
+ },
491
+ });
425
492
  return;
426
493
  }
494
+ // Record the user's input as a session event before forwarding to the agent
495
+ const userInputEvent = create(grackle.SessionEventSchema, {
496
+ sessionId,
497
+ type: grackle.EventType.USER_INPUT,
498
+ timestamp: new Date().toISOString(),
499
+ content: text,
500
+ });
501
+ if (session.logPath) {
502
+ logWriter.writeEvent(session.logPath, userInputEvent);
503
+ }
504
+ streamHub.publish(userInputEvent);
427
505
  try {
428
506
  await conn.client.sendInput(create(powerline.InputMessageSchema, { sessionId, text }));
429
507
  }
430
508
  catch (err) {
431
509
  const errMessage = err instanceof Error ? err.message : String(err);
432
- sendWs(ws, { type: "error", payload: { message: `Failed to send input: ${errMessage}` } });
510
+ sendWs(ws, {
511
+ type: "error",
512
+ payload: { message: `Failed to send input: ${errMessage}` },
513
+ });
433
514
  }
434
515
  break;
435
516
  }
@@ -448,7 +529,10 @@ async function handleMessage(ws, msg, subscriptions) {
448
529
  await conn.client.kill(create(powerline.SessionIdSchema, { id: sessionId }));
449
530
  }
450
531
  catch (err) {
451
- sendWs(ws, { type: "error", payload: { message: `Kill failed: ${err}` } });
532
+ sendWs(ws, {
533
+ type: "error",
534
+ payload: { message: `Kill failed: ${err}` },
535
+ });
452
536
  return;
453
537
  }
454
538
  }
@@ -507,6 +591,102 @@ async function handleMessage(ws, msg, subscriptions) {
507
591
  broadcast({ type: "project_archived", payload: { projectId } });
508
592
  break;
509
593
  }
594
+ // ─── Personas ──────────────────────────────────────────
595
+ case "list_personas": {
596
+ const rows = personaStore.listPersonas();
597
+ sendWs(ws, {
598
+ type: "personas",
599
+ payload: {
600
+ personas: rows.map((r) => ({
601
+ id: r.id,
602
+ name: r.name,
603
+ description: r.description,
604
+ systemPrompt: r.systemPrompt,
605
+ toolConfig: r.toolConfig,
606
+ runtime: r.runtime,
607
+ model: r.model,
608
+ maxTurns: r.maxTurns,
609
+ mcpServers: r.mcpServers,
610
+ createdAt: r.createdAt,
611
+ updatedAt: r.updatedAt,
612
+ })),
613
+ },
614
+ });
615
+ break;
616
+ }
617
+ case "create_persona": {
618
+ const personaName = msg.payload?.name;
619
+ if (!personaName) {
620
+ sendWs(ws, { type: "error", payload: { message: "name required" } });
621
+ return;
622
+ }
623
+ const personaSystemPrompt = msg.payload?.systemPrompt;
624
+ if (!personaSystemPrompt) {
625
+ sendWs(ws, {
626
+ type: "error",
627
+ payload: { message: "systemPrompt required" },
628
+ });
629
+ return;
630
+ }
631
+ let personaId = slugify(personaName) || uuid().slice(0, 8);
632
+ const MAX_ID_RETRIES = 10;
633
+ for (let i = 0; i < MAX_ID_RETRIES && personaStore.getPersona(personaId); i++) {
634
+ personaId = `${slugify(personaName) || "persona"}-${uuid().slice(0, 4)}`;
635
+ }
636
+ personaStore.createPersona(personaId, personaName, msg.payload?.description || "", personaSystemPrompt, msg.payload?.toolConfig || "{}", msg.payload?.runtime || "", msg.payload?.model || "", msg.payload?.maxTurns || 0, msg.payload?.mcpServers || "[]");
637
+ broadcast({ type: "persona_created", payload: { personaId } });
638
+ break;
639
+ }
640
+ case "get_persona": {
641
+ const getPersonaId = msg.payload?.personaId;
642
+ if (!getPersonaId)
643
+ return;
644
+ const personaRow = personaStore.getPersona(getPersonaId);
645
+ if (!personaRow) {
646
+ sendWs(ws, {
647
+ type: "error",
648
+ payload: { message: `Persona not found: ${getPersonaId}` },
649
+ });
650
+ return;
651
+ }
652
+ sendWs(ws, { type: "persona", payload: { persona: personaRow } });
653
+ break;
654
+ }
655
+ case "update_persona": {
656
+ const updatePersonaId = msg.payload?.personaId;
657
+ if (!updatePersonaId) {
658
+ sendWs(ws, {
659
+ type: "error",
660
+ payload: { message: "personaId required" },
661
+ });
662
+ return;
663
+ }
664
+ const existingPersona = personaStore.getPersona(updatePersonaId);
665
+ if (!existingPersona) {
666
+ sendWs(ws, {
667
+ type: "error",
668
+ payload: { message: `Persona not found: ${updatePersonaId}` },
669
+ });
670
+ return;
671
+ }
672
+ personaStore.updatePersona(updatePersonaId, msg.payload?.name || existingPersona.name, msg.payload?.description || existingPersona.description, msg.payload?.systemPrompt || existingPersona.systemPrompt, msg.payload?.toolConfig || existingPersona.toolConfig, msg.payload?.runtime || existingPersona.runtime, msg.payload?.model || existingPersona.model, msg.payload?.maxTurns || existingPersona.maxTurns, msg.payload?.mcpServers || existingPersona.mcpServers);
673
+ broadcast({
674
+ type: "persona_updated",
675
+ payload: { personaId: updatePersonaId },
676
+ });
677
+ break;
678
+ }
679
+ case "delete_persona": {
680
+ const deletePersonaId = msg.payload?.personaId;
681
+ if (!deletePersonaId)
682
+ return;
683
+ personaStore.deletePersona(deletePersonaId);
684
+ broadcast({
685
+ type: "persona_deleted",
686
+ payload: { personaId: deletePersonaId },
687
+ });
688
+ break;
689
+ }
510
690
  // ─── Tasks ─────────────────────────────────────────────
511
691
  case "list_tasks": {
512
692
  const projectId = msg.payload?.projectId;
@@ -545,21 +725,45 @@ async function handleMessage(ws, msg, subscriptions) {
545
725
  const projectId = msg.payload?.projectId;
546
726
  const title = msg.payload?.title;
547
727
  if (!projectId || !title) {
548
- sendWs(ws, { type: "error", payload: { message: "projectId and title required" } });
728
+ sendWs(ws, {
729
+ type: "error",
730
+ payload: { message: "projectId and title required" },
731
+ });
549
732
  return;
550
733
  }
551
734
  const project = projectStore.getProject(projectId);
552
735
  if (!project) {
553
- sendWs(ws, { type: "error", payload: { message: `Project not found: ${projectId}` } });
736
+ sendWs(ws, {
737
+ type: "error",
738
+ payload: { message: `Project not found: ${projectId}` },
739
+ });
554
740
  return;
555
741
  }
556
742
  const parentTaskId = msg.payload?.parentTaskId || "";
557
743
  const rawCanDecompose = msg.payload?.canDecompose;
558
744
  const canDecompose = typeof rawCanDecompose === "boolean" ? rawCanDecompose : undefined;
745
+ // Resolve environment: explicit > parent task's env > project default
746
+ let resolvedEnvId = msg.payload?.environmentId || "";
747
+ if (!resolvedEnvId && parentTaskId) {
748
+ const parentTask = taskStore.getTask(parentTaskId);
749
+ if (parentTask?.environmentId) {
750
+ resolvedEnvId = parentTask.environmentId;
751
+ }
752
+ }
753
+ if (!resolvedEnvId) {
754
+ resolvedEnvId = project.defaultEnvironmentId;
755
+ }
559
756
  const id = uuid().slice(0, 8);
560
- taskStore.createTask(id, projectId, title, msg.payload?.description || "", msg.payload?.environmentId || project.defaultEnvironmentId, msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose, msg.payload?.personaId || "");
757
+ taskStore.createTask(id, projectId, title, msg.payload?.description || "", resolvedEnvId, msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose, msg.payload?.personaId || "");
561
758
  const row = taskStore.getTask(id);
562
- broadcast({ type: "task_created", payload: { task: row ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) } : null } });
759
+ broadcast({
760
+ type: "task_created",
761
+ payload: {
762
+ task: row
763
+ ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) }
764
+ : null,
765
+ },
766
+ });
563
767
  break;
564
768
  }
565
769
  case "start_task": {
@@ -568,15 +772,26 @@ async function handleMessage(ws, msg, subscriptions) {
568
772
  return;
569
773
  const task = taskStore.getTask(taskId);
570
774
  if (!task) {
571
- sendWs(ws, { type: "error", payload: { message: `Task not found: ${taskId}` } });
775
+ sendWs(ws, {
776
+ type: "error",
777
+ payload: { message: `Task not found: ${taskId}` },
778
+ });
572
779
  return;
573
780
  }
574
781
  if (!["pending", "assigned", "failed"].includes(task.status)) {
575
- sendWs(ws, { type: "error", payload: { message: `Task cannot be started (status: ${task.status})` } });
782
+ sendWs(ws, {
783
+ type: "error",
784
+ payload: {
785
+ message: `Task cannot be started (status: ${task.status})`,
786
+ },
787
+ });
576
788
  return;
577
789
  }
578
790
  if (!taskStore.areDependenciesMet(taskId)) {
579
- sendWs(ws, { type: "error", payload: { message: "Task has unmet dependencies" } });
791
+ sendWs(ws, {
792
+ type: "error",
793
+ payload: { message: "Task has unmet dependencies" },
794
+ });
580
795
  return;
581
796
  }
582
797
  const startError = await startTaskSession(ws, task, {
@@ -614,7 +829,12 @@ async function handleMessage(ws, msg, subscriptions) {
614
829
  if (!task)
615
830
  return;
616
831
  if (task.status !== "review") {
617
- sendWs(ws, { type: "error", payload: { message: `Task cannot be rejected (status: ${task.status})` } });
832
+ sendWs(ws, {
833
+ type: "error",
834
+ payload: {
835
+ message: `Task cannot be rejected (status: ${task.status})`,
836
+ },
837
+ });
618
838
  return;
619
839
  }
620
840
  // Preserve runtime/model from the previous session so the retry
@@ -624,7 +844,10 @@ async function handleMessage(ws, msg, subscriptions) {
624
844
  : undefined;
625
845
  // Store review notes and set status to assigned
626
846
  taskStore.updateTask(task.id, task.title, task.description, "assigned", task.environmentId, safeParseJsonArray(task.dependsOn), reviewNotes);
627
- broadcast({ type: "task_rejected", payload: { taskId, projectId: task.projectId } });
847
+ broadcast({
848
+ type: "task_rejected",
849
+ payload: { taskId, projectId: task.projectId },
850
+ });
628
851
  // Auto-retry: start a new session with the review feedback
629
852
  const freshTask = taskStore.getTask(taskId);
630
853
  if (freshTask) {
@@ -642,14 +865,51 @@ async function handleMessage(ws, msg, subscriptions) {
642
865
  const taskId = msg.payload?.taskId;
643
866
  if (!taskId)
644
867
  return;
868
+ const deletedTask = taskStore.getTask(taskId);
869
+ if (!deletedTask) {
870
+ sendWs(ws, { type: "error", payload: { message: `Task not found: ${taskId}` } });
871
+ return;
872
+ }
645
873
  const children = taskStore.getChildren(taskId);
646
874
  if (children.length > 0) {
647
- sendWs(ws, { type: "error", payload: { message: "Cannot delete task with children. Delete children first." } });
875
+ sendWs(ws, {
876
+ type: "error",
877
+ payload: {
878
+ message: "Cannot delete task with children. Delete children first.",
879
+ },
880
+ });
648
881
  return;
649
882
  }
650
- const deletedTask = taskStore.getTask(taskId);
651
- taskStore.deleteTask(taskId);
652
- broadcast({ type: "task_deleted", payload: { taskId, projectId: deletedTask?.projectId } });
883
+ // Kill active session before deleting the task
884
+ if (deletedTask.sessionId) {
885
+ const activeSession = sessionStore.getSession(deletedTask.sessionId);
886
+ if (activeSession && (activeSession.status === "running" || activeSession.status === "waiting_input")) {
887
+ const conn = adapterManager.getConnection(activeSession.environmentId);
888
+ if (conn) {
889
+ try {
890
+ await conn.client.kill(create(powerline.SessionIdSchema, { id: deletedTask.sessionId }));
891
+ }
892
+ catch (err) {
893
+ logger.warn({ taskId, sessionId: deletedTask.sessionId, err }, "Failed to kill session during task deletion");
894
+ }
895
+ }
896
+ sessionStore.updateSession(deletedTask.sessionId, "killed");
897
+ streamHub.publish(create(grackle.SessionEventSchema, {
898
+ sessionId: deletedTask.sessionId,
899
+ type: grackle.EventType.STATUS,
900
+ timestamp: new Date().toISOString(),
901
+ content: "killed",
902
+ raw: "",
903
+ }));
904
+ }
905
+ }
906
+ const changes = taskStore.deleteTask(taskId);
907
+ if (changes === 0) {
908
+ logger.error({ taskId }, "deleteTask returned 0 changes despite task existing");
909
+ sendWs(ws, { type: "error", payload: { message: `Failed to delete task ${taskId}: no rows affected` } });
910
+ return;
911
+ }
912
+ broadcast({ type: "task_deleted", payload: { taskId, projectId: deletedTask.projectId } });
653
913
  break;
654
914
  }
655
915
  // ─── Findings ──────────────────────────────────────────
@@ -681,7 +941,10 @@ async function handleMessage(ws, msg, subscriptions) {
681
941
  const projectId = msg.payload?.projectId;
682
942
  const title = msg.payload?.title;
683
943
  if (!projectId || !title) {
684
- sendWs(ws, { type: "error", payload: { message: "projectId and title required" } });
944
+ sendWs(ws, {
945
+ type: "error",
946
+ payload: { message: "projectId and title required" },
947
+ });
685
948
  return;
686
949
  }
687
950
  const id = uuid().slice(0, 8);
@@ -696,17 +959,27 @@ async function handleMessage(ws, msg, subscriptions) {
696
959
  return;
697
960
  const task = taskStore.getTask(taskId);
698
961
  if (!task || !task.branch) {
699
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "No branch" } });
962
+ sendWs(ws, {
963
+ type: "task_diff",
964
+ payload: { taskId, error: "No branch" },
965
+ });
700
966
  return;
701
967
  }
702
- const environmentId = task.environmentId || projectStore.getProject(task.projectId)?.defaultEnvironmentId;
968
+ const environmentId = task.environmentId ||
969
+ projectStore.getProject(task.projectId)?.defaultEnvironmentId;
703
970
  if (!environmentId) {
704
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "No environment" } });
971
+ sendWs(ws, {
972
+ type: "task_diff",
973
+ payload: { taskId, error: "No environment" },
974
+ });
705
975
  return;
706
976
  }
707
977
  const conn = adapterManager.getConnection(environmentId);
708
978
  if (!conn) {
709
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "Environment not connected" } });
979
+ sendWs(ws, {
980
+ type: "task_diff",
981
+ payload: { taskId, error: "Environment not connected" },
982
+ });
710
983
  return;
711
984
  }
712
985
  try {
@@ -728,24 +1001,36 @@ async function handleMessage(ws, msg, subscriptions) {
728
1001
  });
729
1002
  }
730
1003
  catch (err) {
731
- sendWs(ws, { type: "task_diff", payload: { taskId, error: String(err) } });
1004
+ sendWs(ws, {
1005
+ type: "task_diff",
1006
+ payload: { taskId, error: String(err) },
1007
+ });
732
1008
  }
733
1009
  break;
734
1010
  }
735
1011
  case "provision_environment": {
736
1012
  const environmentId = msg.payload?.environmentId;
737
1013
  if (!environmentId) {
738
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1014
+ sendWs(ws, {
1015
+ type: "error",
1016
+ payload: { message: "environmentId required" },
1017
+ });
739
1018
  return;
740
1019
  }
741
1020
  const env = envRegistry.getEnvironment(environmentId);
742
1021
  if (!env) {
743
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
1022
+ sendWs(ws, {
1023
+ type: "error",
1024
+ payload: { message: `Environment not found: ${environmentId}` },
1025
+ });
744
1026
  return;
745
1027
  }
746
1028
  const adapter = adapterManager.getAdapter(env.adapterType);
747
1029
  if (!adapter) {
748
- sendWs(ws, { type: "error", payload: { message: `No adapter for type: ${env.adapterType}` } });
1030
+ sendWs(ws, {
1031
+ type: "error",
1032
+ payload: { message: `No adapter for type: ${env.adapterType}` },
1033
+ });
749
1034
  return;
750
1035
  }
751
1036
  logger.info({ environmentId, adapterType: env.adapterType }, "Provisioning environment");
@@ -761,7 +1046,12 @@ async function handleMessage(ws, msg, subscriptions) {
761
1046
  logger.info({ environmentId, stage: event.stage, message: event.message }, "Provision progress");
762
1047
  broadcast({
763
1048
  type: "provision_progress",
764
- payload: { environmentId, stage: event.stage, message: event.message, progress: event.progress },
1049
+ payload: {
1050
+ environmentId,
1051
+ stage: event.stage,
1052
+ message: event.message,
1053
+ progress: event.progress,
1054
+ },
765
1055
  });
766
1056
  }
767
1057
  logger.info({ environmentId }, "Provision complete, calling adapter.connect");
@@ -774,7 +1064,12 @@ async function handleMessage(ws, msg, subscriptions) {
774
1064
  logger.info({ environmentId }, "Environment connected");
775
1065
  broadcast({
776
1066
  type: "provision_progress",
777
- payload: { environmentId, stage: "ready", message: "Environment connected", progress: 1 },
1067
+ payload: {
1068
+ environmentId,
1069
+ stage: "ready",
1070
+ message: "Environment connected",
1071
+ progress: 1,
1072
+ },
778
1073
  });
779
1074
  }
780
1075
  catch (err) {
@@ -783,7 +1078,12 @@ async function handleMessage(ws, msg, subscriptions) {
783
1078
  const errorMessage = err instanceof Error ? err.message : String(err);
784
1079
  broadcast({
785
1080
  type: "provision_progress",
786
- payload: { environmentId, stage: "error", message: `Connection failed: ${errorMessage}`, progress: 0 },
1081
+ payload: {
1082
+ environmentId,
1083
+ stage: "error",
1084
+ message: `Connection failed: ${errorMessage}`,
1085
+ progress: 0,
1086
+ },
787
1087
  });
788
1088
  }
789
1089
  broadcastEnvironments();
@@ -793,12 +1093,18 @@ async function handleMessage(ws, msg, subscriptions) {
793
1093
  case "stop_environment": {
794
1094
  const environmentId = msg.payload?.environmentId;
795
1095
  if (!environmentId) {
796
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1096
+ sendWs(ws, {
1097
+ type: "error",
1098
+ payload: { message: "environmentId required" },
1099
+ });
797
1100
  return;
798
1101
  }
799
1102
  const env = envRegistry.getEnvironment(environmentId);
800
1103
  if (!env) {
801
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
1104
+ sendWs(ws, {
1105
+ type: "error",
1106
+ payload: { message: `Environment not found: ${environmentId}` },
1107
+ });
802
1108
  return;
803
1109
  }
804
1110
  const adapter = adapterManager.getAdapter(env.adapterType);
@@ -816,11 +1122,17 @@ async function handleMessage(ws, msg, subscriptions) {
816
1122
  const displayName = msg.payload?.displayName || "";
817
1123
  const adapterType = msg.payload?.adapterType || "";
818
1124
  if (!displayName || !adapterType) {
819
- sendWs(ws, { type: "error", payload: { message: "displayName and adapterType required" } });
1125
+ sendWs(ws, {
1126
+ type: "error",
1127
+ payload: { message: "displayName and adapterType required" },
1128
+ });
820
1129
  return;
821
1130
  }
822
1131
  if (!adapterManager.getAdapter(adapterType)) {
823
- sendWs(ws, { type: "error", payload: { message: `Unknown adapter type: ${adapterType}` } });
1132
+ sendWs(ws, {
1133
+ type: "error",
1134
+ payload: { message: `Unknown adapter type: ${adapterType}` },
1135
+ });
824
1136
  return;
825
1137
  }
826
1138
  const baseEnvId = slugify(displayName) || uuid().slice(0, 8);
@@ -842,7 +1154,10 @@ async function handleMessage(ws, msg, subscriptions) {
842
1154
  JSON.parse(normalized);
843
1155
  }
844
1156
  catch {
845
- sendWs(ws, { type: "error", payload: { message: "adapterConfig string is not valid JSON" } });
1157
+ sendWs(ws, {
1158
+ type: "error",
1159
+ payload: { message: "adapterConfig string is not valid JSON" },
1160
+ });
846
1161
  return;
847
1162
  }
848
1163
  adapterConfig = normalized;
@@ -851,7 +1166,12 @@ async function handleMessage(ws, msg, subscriptions) {
851
1166
  adapterConfig = JSON.stringify(rawAdapterConfig);
852
1167
  }
853
1168
  else {
854
- sendWs(ws, { type: "error", payload: { message: "adapterConfig must be an object or JSON string" } });
1169
+ sendWs(ws, {
1170
+ type: "error",
1171
+ payload: {
1172
+ message: "adapterConfig must be an object or JSON string",
1173
+ },
1174
+ });
855
1175
  return;
856
1176
  }
857
1177
  const defaultRuntime = msg.payload?.defaultRuntime || DEFAULT_RUNTIME;
@@ -864,7 +1184,10 @@ async function handleMessage(ws, msg, subscriptions) {
864
1184
  case "remove_environment": {
865
1185
  const environmentId = msg.payload?.environmentId;
866
1186
  if (!environmentId) {
867
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1187
+ sendWs(ws, {
1188
+ type: "error",
1189
+ payload: { message: "environmentId required" },
1190
+ });
868
1191
  return;
869
1192
  }
870
1193
  const env = envRegistry.getEnvironment(environmentId);
@@ -875,11 +1198,15 @@ async function handleMessage(ws, msg, subscriptions) {
875
1198
  try {
876
1199
  await adapter.destroy(environmentId, config);
877
1200
  }
878
- catch { /* best-effort */ }
1201
+ catch {
1202
+ /* best-effort */
1203
+ }
879
1204
  try {
880
1205
  await adapter.disconnect(environmentId);
881
1206
  }
882
- catch { /* best-effort */ }
1207
+ catch {
1208
+ /* best-effort */
1209
+ }
883
1210
  }
884
1211
  }
885
1212
  adapterManager.removeConnection(environmentId);
@@ -894,31 +1221,43 @@ async function handleMessage(ws, msg, subscriptions) {
894
1221
  case "list_codespaces": {
895
1222
  try {
896
1223
  const result = await exec("gh", [
897
- "codespace", "list",
898
- "--json", "name,repository,state,gitStatus",
899
- "--limit", String(GH_CODESPACE_LIST_LIMIT),
1224
+ "codespace",
1225
+ "list",
1226
+ "--json",
1227
+ "name,repository,state,gitStatus",
1228
+ "--limit",
1229
+ String(GH_CODESPACE_LIST_LIMIT),
900
1230
  ], { timeout: GH_CODESPACE_LIST_TIMEOUT_MS });
901
1231
  const codespaces = JSON.parse(result.stdout || "[]");
902
1232
  sendWs(ws, { type: "codespaces_list", payload: { codespaces } });
903
1233
  }
904
1234
  catch (err) {
905
1235
  logger.warn({ err }, "Failed to list codespaces");
906
- sendWs(ws, { type: "codespaces_list", payload: { codespaces: [], error: String(err) } });
1236
+ sendWs(ws, {
1237
+ type: "codespaces_list",
1238
+ payload: { codespaces: [], error: String(err) },
1239
+ });
907
1240
  }
908
1241
  break;
909
1242
  }
910
1243
  case "create_codespace": {
911
1244
  const repo = msg.payload?.repo;
912
1245
  if (typeof repo !== "string" || repo.trim().length === 0) {
913
- sendWs(ws, { type: "codespace_create_error", payload: { message: "repo required" } });
1246
+ sendWs(ws, {
1247
+ type: "codespace_create_error",
1248
+ payload: { message: "repo required" },
1249
+ });
914
1250
  return;
915
1251
  }
916
1252
  const trimmedRepo = repo.trim();
917
1253
  try {
918
1254
  const result = await exec("gh", [
919
- "codespace", "create",
920
- "--repo", trimmedRepo,
921
- "--machine", "basicLinux32gb",
1255
+ "codespace",
1256
+ "create",
1257
+ "--repo",
1258
+ trimmedRepo,
1259
+ "--machine",
1260
+ "basicLinux32gb",
922
1261
  ], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
923
1262
  const codespaceName = result.stdout.trim();
924
1263
  sendWs(ws, {
@@ -928,7 +1267,10 @@ async function handleMessage(ws, msg, subscriptions) {
928
1267
  }
929
1268
  catch (err) {
930
1269
  logger.error({ err, repo }, "Failed to create codespace");
931
- sendWs(ws, { type: "codespace_create_error", payload: { message: String(err) } });
1270
+ sendWs(ws, {
1271
+ type: "codespace_create_error",
1272
+ payload: { message: String(err) },
1273
+ });
932
1274
  }
933
1275
  break;
934
1276
  }
@@ -953,7 +1295,10 @@ async function handleMessage(ws, msg, subscriptions) {
953
1295
  const name = msg.payload?.name;
954
1296
  const value = msg.payload?.value;
955
1297
  if (!name || !value) {
956
- sendWs(ws, { type: "error", payload: { message: "name and value required" } });
1298
+ sendWs(ws, {
1299
+ type: "error",
1300
+ payload: { message: "name and value required" },
1301
+ });
957
1302
  return;
958
1303
  }
959
1304
  await tokenBroker.setToken({