@grackle-ai/server 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -193,10 +226,17 @@ async function startTaskSession(ws, task, options) {
193
226
  if (persona) {
194
227
  systemContext = persona.systemPrompt + "\n\n" + systemContext;
195
228
  }
196
- sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath);
229
+ sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath, freshTask.id);
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,76 @@ 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 } });
913
+ break;
914
+ }
915
+ // ─── Task Sessions ─────────────────────────────────────
916
+ case "get_task_sessions": {
917
+ const taskId = msg.payload?.taskId;
918
+ if (typeof taskId !== "string" || taskId.length === 0) {
919
+ return;
920
+ }
921
+ const taskSessions = sessionStore.listSessionsForTask(taskId);
922
+ sendWs(ws, {
923
+ type: "task_sessions",
924
+ payload: {
925
+ taskId,
926
+ sessions: taskSessions.map((r) => ({
927
+ id: r.id,
928
+ environmentId: r.environmentId,
929
+ runtime: r.runtime,
930
+ status: r.status,
931
+ prompt: r.prompt,
932
+ startedAt: r.startedAt,
933
+ endedAt: r.endedAt ?? "",
934
+ error: r.error ?? "",
935
+ })),
936
+ },
937
+ });
653
938
  break;
654
939
  }
655
940
  // ─── Findings ──────────────────────────────────────────
@@ -681,7 +966,10 @@ async function handleMessage(ws, msg, subscriptions) {
681
966
  const projectId = msg.payload?.projectId;
682
967
  const title = msg.payload?.title;
683
968
  if (!projectId || !title) {
684
- sendWs(ws, { type: "error", payload: { message: "projectId and title required" } });
969
+ sendWs(ws, {
970
+ type: "error",
971
+ payload: { message: "projectId and title required" },
972
+ });
685
973
  return;
686
974
  }
687
975
  const id = uuid().slice(0, 8);
@@ -696,17 +984,27 @@ async function handleMessage(ws, msg, subscriptions) {
696
984
  return;
697
985
  const task = taskStore.getTask(taskId);
698
986
  if (!task || !task.branch) {
699
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "No branch" } });
987
+ sendWs(ws, {
988
+ type: "task_diff",
989
+ payload: { taskId, error: "No branch" },
990
+ });
700
991
  return;
701
992
  }
702
- const environmentId = task.environmentId || projectStore.getProject(task.projectId)?.defaultEnvironmentId;
993
+ const environmentId = task.environmentId ||
994
+ projectStore.getProject(task.projectId)?.defaultEnvironmentId;
703
995
  if (!environmentId) {
704
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "No environment" } });
996
+ sendWs(ws, {
997
+ type: "task_diff",
998
+ payload: { taskId, error: "No environment" },
999
+ });
705
1000
  return;
706
1001
  }
707
1002
  const conn = adapterManager.getConnection(environmentId);
708
1003
  if (!conn) {
709
- sendWs(ws, { type: "task_diff", payload: { taskId, error: "Environment not connected" } });
1004
+ sendWs(ws, {
1005
+ type: "task_diff",
1006
+ payload: { taskId, error: "Environment not connected" },
1007
+ });
710
1008
  return;
711
1009
  }
712
1010
  try {
@@ -728,24 +1026,36 @@ async function handleMessage(ws, msg, subscriptions) {
728
1026
  });
729
1027
  }
730
1028
  catch (err) {
731
- sendWs(ws, { type: "task_diff", payload: { taskId, error: String(err) } });
1029
+ sendWs(ws, {
1030
+ type: "task_diff",
1031
+ payload: { taskId, error: String(err) },
1032
+ });
732
1033
  }
733
1034
  break;
734
1035
  }
735
1036
  case "provision_environment": {
736
1037
  const environmentId = msg.payload?.environmentId;
737
1038
  if (!environmentId) {
738
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1039
+ sendWs(ws, {
1040
+ type: "error",
1041
+ payload: { message: "environmentId required" },
1042
+ });
739
1043
  return;
740
1044
  }
741
1045
  const env = envRegistry.getEnvironment(environmentId);
742
1046
  if (!env) {
743
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
1047
+ sendWs(ws, {
1048
+ type: "error",
1049
+ payload: { message: `Environment not found: ${environmentId}` },
1050
+ });
744
1051
  return;
745
1052
  }
746
1053
  const adapter = adapterManager.getAdapter(env.adapterType);
747
1054
  if (!adapter) {
748
- sendWs(ws, { type: "error", payload: { message: `No adapter for type: ${env.adapterType}` } });
1055
+ sendWs(ws, {
1056
+ type: "error",
1057
+ payload: { message: `No adapter for type: ${env.adapterType}` },
1058
+ });
749
1059
  return;
750
1060
  }
751
1061
  logger.info({ environmentId, adapterType: env.adapterType }, "Provisioning environment");
@@ -761,7 +1071,12 @@ async function handleMessage(ws, msg, subscriptions) {
761
1071
  logger.info({ environmentId, stage: event.stage, message: event.message }, "Provision progress");
762
1072
  broadcast({
763
1073
  type: "provision_progress",
764
- payload: { environmentId, stage: event.stage, message: event.message, progress: event.progress },
1074
+ payload: {
1075
+ environmentId,
1076
+ stage: event.stage,
1077
+ message: event.message,
1078
+ progress: event.progress,
1079
+ },
765
1080
  });
766
1081
  }
767
1082
  logger.info({ environmentId }, "Provision complete, calling adapter.connect");
@@ -774,7 +1089,12 @@ async function handleMessage(ws, msg, subscriptions) {
774
1089
  logger.info({ environmentId }, "Environment connected");
775
1090
  broadcast({
776
1091
  type: "provision_progress",
777
- payload: { environmentId, stage: "ready", message: "Environment connected", progress: 1 },
1092
+ payload: {
1093
+ environmentId,
1094
+ stage: "ready",
1095
+ message: "Environment connected",
1096
+ progress: 1,
1097
+ },
778
1098
  });
779
1099
  }
780
1100
  catch (err) {
@@ -783,7 +1103,12 @@ async function handleMessage(ws, msg, subscriptions) {
783
1103
  const errorMessage = err instanceof Error ? err.message : String(err);
784
1104
  broadcast({
785
1105
  type: "provision_progress",
786
- payload: { environmentId, stage: "error", message: `Connection failed: ${errorMessage}`, progress: 0 },
1106
+ payload: {
1107
+ environmentId,
1108
+ stage: "error",
1109
+ message: `Connection failed: ${errorMessage}`,
1110
+ progress: 0,
1111
+ },
787
1112
  });
788
1113
  }
789
1114
  broadcastEnvironments();
@@ -793,12 +1118,18 @@ async function handleMessage(ws, msg, subscriptions) {
793
1118
  case "stop_environment": {
794
1119
  const environmentId = msg.payload?.environmentId;
795
1120
  if (!environmentId) {
796
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1121
+ sendWs(ws, {
1122
+ type: "error",
1123
+ payload: { message: "environmentId required" },
1124
+ });
797
1125
  return;
798
1126
  }
799
1127
  const env = envRegistry.getEnvironment(environmentId);
800
1128
  if (!env) {
801
- sendWs(ws, { type: "error", payload: { message: `Environment not found: ${environmentId}` } });
1129
+ sendWs(ws, {
1130
+ type: "error",
1131
+ payload: { message: `Environment not found: ${environmentId}` },
1132
+ });
802
1133
  return;
803
1134
  }
804
1135
  const adapter = adapterManager.getAdapter(env.adapterType);
@@ -816,11 +1147,17 @@ async function handleMessage(ws, msg, subscriptions) {
816
1147
  const displayName = msg.payload?.displayName || "";
817
1148
  const adapterType = msg.payload?.adapterType || "";
818
1149
  if (!displayName || !adapterType) {
819
- sendWs(ws, { type: "error", payload: { message: "displayName and adapterType required" } });
1150
+ sendWs(ws, {
1151
+ type: "error",
1152
+ payload: { message: "displayName and adapterType required" },
1153
+ });
820
1154
  return;
821
1155
  }
822
1156
  if (!adapterManager.getAdapter(adapterType)) {
823
- sendWs(ws, { type: "error", payload: { message: `Unknown adapter type: ${adapterType}` } });
1157
+ sendWs(ws, {
1158
+ type: "error",
1159
+ payload: { message: `Unknown adapter type: ${adapterType}` },
1160
+ });
824
1161
  return;
825
1162
  }
826
1163
  const baseEnvId = slugify(displayName) || uuid().slice(0, 8);
@@ -842,7 +1179,10 @@ async function handleMessage(ws, msg, subscriptions) {
842
1179
  JSON.parse(normalized);
843
1180
  }
844
1181
  catch {
845
- sendWs(ws, { type: "error", payload: { message: "adapterConfig string is not valid JSON" } });
1182
+ sendWs(ws, {
1183
+ type: "error",
1184
+ payload: { message: "adapterConfig string is not valid JSON" },
1185
+ });
846
1186
  return;
847
1187
  }
848
1188
  adapterConfig = normalized;
@@ -851,7 +1191,12 @@ async function handleMessage(ws, msg, subscriptions) {
851
1191
  adapterConfig = JSON.stringify(rawAdapterConfig);
852
1192
  }
853
1193
  else {
854
- sendWs(ws, { type: "error", payload: { message: "adapterConfig must be an object or JSON string" } });
1194
+ sendWs(ws, {
1195
+ type: "error",
1196
+ payload: {
1197
+ message: "adapterConfig must be an object or JSON string",
1198
+ },
1199
+ });
855
1200
  return;
856
1201
  }
857
1202
  const defaultRuntime = msg.payload?.defaultRuntime || DEFAULT_RUNTIME;
@@ -864,7 +1209,10 @@ async function handleMessage(ws, msg, subscriptions) {
864
1209
  case "remove_environment": {
865
1210
  const environmentId = msg.payload?.environmentId;
866
1211
  if (!environmentId) {
867
- sendWs(ws, { type: "error", payload: { message: "environmentId required" } });
1212
+ sendWs(ws, {
1213
+ type: "error",
1214
+ payload: { message: "environmentId required" },
1215
+ });
868
1216
  return;
869
1217
  }
870
1218
  const env = envRegistry.getEnvironment(environmentId);
@@ -875,11 +1223,15 @@ async function handleMessage(ws, msg, subscriptions) {
875
1223
  try {
876
1224
  await adapter.destroy(environmentId, config);
877
1225
  }
878
- catch { /* best-effort */ }
1226
+ catch {
1227
+ /* best-effort */
1228
+ }
879
1229
  try {
880
1230
  await adapter.disconnect(environmentId);
881
1231
  }
882
- catch { /* best-effort */ }
1232
+ catch {
1233
+ /* best-effort */
1234
+ }
883
1235
  }
884
1236
  }
885
1237
  adapterManager.removeConnection(environmentId);
@@ -894,31 +1246,43 @@ async function handleMessage(ws, msg, subscriptions) {
894
1246
  case "list_codespaces": {
895
1247
  try {
896
1248
  const result = await exec("gh", [
897
- "codespace", "list",
898
- "--json", "name,repository,state,gitStatus",
899
- "--limit", String(GH_CODESPACE_LIST_LIMIT),
1249
+ "codespace",
1250
+ "list",
1251
+ "--json",
1252
+ "name,repository,state,gitStatus",
1253
+ "--limit",
1254
+ String(GH_CODESPACE_LIST_LIMIT),
900
1255
  ], { timeout: GH_CODESPACE_LIST_TIMEOUT_MS });
901
1256
  const codespaces = JSON.parse(result.stdout || "[]");
902
1257
  sendWs(ws, { type: "codespaces_list", payload: { codespaces } });
903
1258
  }
904
1259
  catch (err) {
905
1260
  logger.warn({ err }, "Failed to list codespaces");
906
- sendWs(ws, { type: "codespaces_list", payload: { codespaces: [], error: String(err) } });
1261
+ sendWs(ws, {
1262
+ type: "codespaces_list",
1263
+ payload: { codespaces: [], error: String(err) },
1264
+ });
907
1265
  }
908
1266
  break;
909
1267
  }
910
1268
  case "create_codespace": {
911
1269
  const repo = msg.payload?.repo;
912
1270
  if (typeof repo !== "string" || repo.trim().length === 0) {
913
- sendWs(ws, { type: "codespace_create_error", payload: { message: "repo required" } });
1271
+ sendWs(ws, {
1272
+ type: "codespace_create_error",
1273
+ payload: { message: "repo required" },
1274
+ });
914
1275
  return;
915
1276
  }
916
1277
  const trimmedRepo = repo.trim();
917
1278
  try {
918
1279
  const result = await exec("gh", [
919
- "codespace", "create",
920
- "--repo", trimmedRepo,
921
- "--machine", "basicLinux32gb",
1280
+ "codespace",
1281
+ "create",
1282
+ "--repo",
1283
+ trimmedRepo,
1284
+ "--machine",
1285
+ "basicLinux32gb",
922
1286
  ], { timeout: GH_CODESPACE_CREATE_TIMEOUT_MS });
923
1287
  const codespaceName = result.stdout.trim();
924
1288
  sendWs(ws, {
@@ -928,7 +1292,10 @@ async function handleMessage(ws, msg, subscriptions) {
928
1292
  }
929
1293
  catch (err) {
930
1294
  logger.error({ err, repo }, "Failed to create codespace");
931
- sendWs(ws, { type: "codespace_create_error", payload: { message: String(err) } });
1295
+ sendWs(ws, {
1296
+ type: "codespace_create_error",
1297
+ payload: { message: String(err) },
1298
+ });
932
1299
  }
933
1300
  break;
934
1301
  }
@@ -953,7 +1320,10 @@ async function handleMessage(ws, msg, subscriptions) {
953
1320
  const name = msg.payload?.name;
954
1321
  const value = msg.payload?.value;
955
1322
  if (!name || !value) {
956
- sendWs(ws, { type: "error", payload: { message: "name and value required" } });
1323
+ sendWs(ws, {
1324
+ type: "error",
1325
+ payload: { message: "name and value required" },
1326
+ });
957
1327
  return;
958
1328
  }
959
1329
  await tokenBroker.setToken({