@grackle-ai/server 0.40.0 → 0.41.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.
Files changed (43) hide show
  1. package/dist/db.d.ts.map +1 -1
  2. package/dist/db.js +10 -0
  3. package/dist/db.js.map +1 -1
  4. package/dist/event-bus.d.ts +37 -0
  5. package/dist/event-bus.d.ts.map +1 -0
  6. package/dist/event-bus.js +65 -0
  7. package/dist/event-bus.js.map +1 -0
  8. package/dist/event-processor.d.ts.map +1 -1
  9. package/dist/event-processor.js +14 -11
  10. package/dist/event-processor.js.map +1 -1
  11. package/dist/event-store.d.ts +9 -0
  12. package/dist/event-store.d.ts.map +1 -0
  13. package/dist/event-store.js +16 -0
  14. package/dist/event-store.js.map +1 -0
  15. package/dist/github-import.js +3 -5
  16. package/dist/github-import.js.map +1 -1
  17. package/dist/grpc-service.d.ts +6 -0
  18. package/dist/grpc-service.d.ts.map +1 -1
  19. package/dist/grpc-service.js +29 -85
  20. package/dist/grpc-service.js.map +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -6
  23. package/dist/index.js.map +1 -1
  24. package/dist/reanimate-agent.d.ts +12 -0
  25. package/dist/reanimate-agent.d.ts.map +1 -0
  26. package/dist/reanimate-agent.js +78 -0
  27. package/dist/reanimate-agent.js.map +1 -0
  28. package/dist/schema.d.ts +85 -0
  29. package/dist/schema.d.ts.map +1 -1
  30. package/dist/schema.js +7 -0
  31. package/dist/schema.js.map +1 -1
  32. package/dist/session-store.d.ts +6 -1
  33. package/dist/session-store.d.ts.map +1 -1
  34. package/dist/session-store.js +23 -5
  35. package/dist/session-store.js.map +1 -1
  36. package/dist/ws-bridge.d.ts.map +1 -1
  37. package/dist/ws-bridge.js +92 -123
  38. package/dist/ws-bridge.js.map +1 -1
  39. package/dist/ws-broadcast.d.ts +5 -0
  40. package/dist/ws-broadcast.d.ts.map +1 -1
  41. package/dist/ws-broadcast.js +24 -0
  42. package/dist/ws-broadcast.js.map +1 -1
  43. package/package.json +7 -6
package/dist/ws-bridge.js CHANGED
@@ -14,7 +14,7 @@ import * as findingStore from "./finding-store.js";
14
14
  import * as personaStore from "./persona-store.js";
15
15
  import { v4 as uuid } from "uuid";
16
16
  import { join } from "node:path";
17
- import { LOGS_DIR, SESSION_STATUS, TASK_STATUS, eventTypeToString, } from "@grackle-ai/common";
17
+ import { LOGS_DIR, SESSION_STATUS, TASK_STATUS, DEFAULT_MCP_PORT, eventTypeToString, } from "@grackle-ai/common";
18
18
  import { resolvePersona } from "./resolve-persona.js";
19
19
  import * as settingsStore from "./settings-store.js";
20
20
  import { isAllowedSettingKey } from "./settings-store.js";
@@ -26,8 +26,13 @@ import { buildTaskSystemContext } from "./utils/system-context.js";
26
26
  import { slugify } from "./utils/slugify.js";
27
27
  import { processEventStream } from "./event-processor.js";
28
28
  import * as processorRegistry from "./processor-registry.js";
29
- import { broadcast, setWssInstance, broadcastEnvironments, envRowToWs } from "./ws-broadcast.js";
30
- import { buildMcpServersJson } from "./grpc-service.js";
29
+ import { setWssInstance, envRowToWs } from "./ws-broadcast.js";
30
+ import { emit } from "./event-bus.js";
31
+ import { buildMcpServersJson, toDialableHost } from "./grpc-service.js";
32
+ import { createScopedToken } from "@grackle-ai/mcp";
33
+ import { loadOrCreateApiKey } from "./api-key.js";
34
+ import { reanimateAgent } from "./reanimate-agent.js";
35
+ import { ConnectError } from "@connectrpc/connect";
31
36
  import { computeTaskStatus } from "./compute-task-status.js";
32
37
  import { exec } from "./utils/exec.js";
33
38
  import { formatGhError } from "./utils/format-gh-error.js";
@@ -111,20 +116,17 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
111
116
  }
112
117
  logger.info({ environmentId, ...logContext }, "Auto-provisioning environment");
113
118
  envRegistry.updateEnvironmentStatus(environmentId, "connecting");
114
- broadcastEnvironments();
119
+ emit("environment.changed", {});
115
120
  try {
116
121
  const config = safeParseAdapterConfig(env.adapterConfig, environmentId);
117
122
  const powerlineToken = env.powerlineToken || "";
118
123
  for await (const provEvent of reconnectOrProvision(environmentId, adapter, config, powerlineToken, !!env.bootstrapped)) {
119
124
  logger.info({ environmentId, stage: provEvent.stage, ...logContext }, "Auto-provision progress");
120
- broadcast({
121
- type: "provision_progress",
122
- payload: {
123
- environmentId,
124
- stage: provEvent.stage,
125
- message: provEvent.message,
126
- progress: provEvent.progress,
127
- },
125
+ emit("environment.provision_progress", {
126
+ environmentId,
127
+ stage: provEvent.stage,
128
+ message: provEvent.message,
129
+ progress: provEvent.progress,
128
130
  });
129
131
  }
130
132
  conn = await adapter.connect(environmentId, config, powerlineToken);
@@ -133,32 +135,26 @@ async function autoProvisionEnvironment(ws, environmentId, env, logContext) {
133
135
  await tokenBroker.pushToEnv(environmentId);
134
136
  envRegistry.updateEnvironmentStatus(environmentId, "connected");
135
137
  envRegistry.markBootstrapped(environmentId);
136
- broadcastEnvironments();
138
+ emit("environment.changed", {});
137
139
  logger.info({ environmentId, ...logContext }, "Auto-provision complete");
138
- broadcast({
139
- type: "provision_progress",
140
- payload: {
141
- environmentId,
142
- stage: "ready",
143
- message: "Environment connected",
144
- progress: 1,
145
- },
140
+ emit("environment.provision_progress", {
141
+ environmentId,
142
+ stage: "ready",
143
+ message: "Environment connected",
144
+ progress: 1,
146
145
  });
147
146
  return conn;
148
147
  }
149
148
  catch (err) {
150
149
  logger.error({ environmentId, ...logContext, err }, "Auto-provision failed");
151
150
  envRegistry.updateEnvironmentStatus(environmentId, "error");
152
- broadcastEnvironments();
151
+ emit("environment.changed", {});
153
152
  const errorMessage = err instanceof Error ? err.message : String(err);
154
- broadcast({
155
- type: "provision_progress",
156
- payload: {
157
- environmentId,
158
- stage: "error",
159
- message: `Auto-provision failed: ${errorMessage}`,
160
- progress: 0,
161
- },
153
+ emit("environment.provision_progress", {
154
+ environmentId,
155
+ stage: "error",
156
+ message: `Auto-provision failed: ${errorMessage}`,
157
+ progress: 0,
162
158
  });
163
159
  sendWs(ws, {
164
160
  type: "error",
@@ -212,13 +208,10 @@ async function startTaskSession(ws, task, options) {
212
208
  systemContext = systemPrompt + "\n\n" + systemContext;
213
209
  }
214
210
  sessionStore.createSession(sessionId, environmentId, runtime, freshTask.title, model, logPath, freshTask.id, resolved.personaId);
215
- broadcast({
216
- type: "task_started",
217
- payload: {
218
- taskId: freshTask.id,
219
- sessionId,
220
- projectId: freshTask.projectId,
221
- },
211
+ emit("task.started", {
212
+ taskId: freshTask.id,
213
+ sessionId,
214
+ projectId: freshTask.projectId,
222
215
  });
223
216
  // Re-push stored tokens + provider credentials (scoped to runtime) so they're fresh for this session.
224
217
  // For local envs, skip file tokens — the PowerLine is on the same machine.
@@ -236,6 +229,11 @@ async function startTaskSession(ws, task, options) {
236
229
  catch {
237
230
  logger.warn("Failed to parse persona.mcpServers JSON; ignoring");
238
231
  }
232
+ // Build MCP broker URL + scoped token so runtimes can call the MCP server.
233
+ const mcpPort = parseInt(process.env.GRACKLE_MCP_PORT || String(DEFAULT_MCP_PORT), 10);
234
+ const mcpDialHost = toDialableHost(process.env.GRACKLE_HOST || "127.0.0.1");
235
+ const mcpUrl = `http://${mcpDialHost}:${mcpPort}/mcp`;
236
+ const mcpToken = createScopedToken({ sub: freshTask.id, pid: freshTask.projectId, per: resolved.personaId, sid: sessionId }, loadOrCreateApiKey());
239
237
  const powerlineReq = create(powerline.SpawnRequestSchema, {
240
238
  sessionId,
241
239
  runtime,
@@ -250,6 +248,8 @@ async function startTaskSession(ws, task, options) {
250
248
  projectId: freshTask.projectId,
251
249
  taskId: freshTask.id,
252
250
  mcpServersJson,
251
+ mcpUrl,
252
+ mcpToken,
253
253
  });
254
254
  processEventStream(conn.client.spawn(powerlineReq), {
255
255
  sessionId,
@@ -529,11 +529,27 @@ async function handleMessage(ws, msg, subscriptions) {
529
529
  if (session.taskId) {
530
530
  const task = taskStore.getTask(session.taskId);
531
531
  if (task) {
532
- broadcast({ type: "task_updated", payload: { taskId: task.id, projectId: task.projectId } });
532
+ emit("task.updated", { taskId: task.id, projectId: task.projectId });
533
533
  }
534
534
  }
535
535
  break;
536
536
  }
537
+ case "resume_agent": {
538
+ const resumeSessionId = msg.payload?.sessionId;
539
+ if (!resumeSessionId) {
540
+ sendWs(ws, { type: "error", payload: { message: "sessionId required" } });
541
+ return;
542
+ }
543
+ try {
544
+ reanimateAgent(resumeSessionId);
545
+ sendWs(ws, { type: "agent_resumed", payload: { sessionId: resumeSessionId } });
546
+ }
547
+ catch (err) {
548
+ const message = err instanceof ConnectError ? err.message : String(err);
549
+ sendWs(ws, { type: "error", payload: { message } });
550
+ }
551
+ break;
552
+ }
537
553
  // ─── Projects ──────────────────────────────────────────
538
554
  case "list_projects": {
539
555
  const rows = projectStore.listProjects();
@@ -574,15 +590,14 @@ async function handleMessage(ws, msg, subscriptions) {
574
590
  // useWorktrees defaults to true when not specified
575
591
  const createUseWorktrees = msg.payload?.useWorktrees ?? true;
576
592
  projectStore.createProject(id, name, msg.payload?.description || "", msg.payload?.repoUrl || "", msg.payload?.defaultEnvironmentId || "", createUseWorktrees, typeof msg.payload?.worktreeBasePath === "string" ? msg.payload.worktreeBasePath.trim() : "", msg.payload?.defaultPersonaId || "");
577
- const row = projectStore.getProject(id);
578
- broadcast({ type: "project_created", payload: { project: row } });
593
+ emit("project.created", { projectId: id });
579
594
  break;
580
595
  }
581
596
  case "archive_project": {
582
597
  const projectId = msg.payload?.projectId;
583
598
  if (projectId)
584
599
  projectStore.archiveProject(projectId);
585
- broadcast({ type: "project_archived", payload: { projectId } });
600
+ emit("project.archived", { projectId });
586
601
  break;
587
602
  }
588
603
  case "update_project": {
@@ -620,7 +635,7 @@ async function handleMessage(ws, msg, subscriptions) {
620
635
  worktreeBasePath: worktreeBasePathVal,
621
636
  defaultPersonaId: defaultPersonaIdVal,
622
637
  });
623
- broadcast({ type: "project_updated", payload: { projectId } });
638
+ emit("project.updated", { projectId });
624
639
  break;
625
640
  }
626
641
  // ─── Personas ──────────────────────────────────────────
@@ -666,7 +681,7 @@ async function handleMessage(ws, msg, subscriptions) {
666
681
  personaId = `${slugify(personaName) || "persona"}-${uuid().slice(0, 4)}`;
667
682
  }
668
683
  personaStore.createPersona(personaId, personaName, msg.payload?.description || "", personaSystemPrompt, msg.payload?.toolConfig || "{}", msg.payload?.runtime || "", msg.payload?.model || "", msg.payload?.maxTurns || 0, msg.payload?.mcpServers || "[]");
669
- broadcast({ type: "persona_created", payload: { personaId } });
684
+ emit("persona.created", { personaId });
670
685
  break;
671
686
  }
672
687
  case "get_persona": {
@@ -702,10 +717,7 @@ async function handleMessage(ws, msg, subscriptions) {
702
717
  return;
703
718
  }
704
719
  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);
705
- broadcast({
706
- type: "persona_updated",
707
- payload: { personaId: updatePersonaId },
708
- });
720
+ emit("persona.updated", { personaId: updatePersonaId });
709
721
  break;
710
722
  }
711
723
  case "delete_persona": {
@@ -713,10 +725,7 @@ async function handleMessage(ws, msg, subscriptions) {
713
725
  if (!deletePersonaId)
714
726
  return;
715
727
  personaStore.deletePersona(deletePersonaId);
716
- broadcast({
717
- type: "persona_deleted",
718
- payload: { personaId: deletePersonaId },
719
- });
728
+ emit("persona.deleted", { personaId: deletePersonaId });
720
729
  break;
721
730
  }
722
731
  // ─── Settings ────────────────────────────────────────────
@@ -754,7 +763,7 @@ async function handleMessage(ws, msg, subscriptions) {
754
763
  }
755
764
  }
756
765
  settingsStore.setSetting(key, value);
757
- broadcast({ type: "setting_changed", payload: { key, value } });
766
+ emit("setting.changed", { key, value });
758
767
  break;
759
768
  }
760
769
  // ─── Tasks ─────────────────────────────────────────────
@@ -834,16 +843,7 @@ async function handleMessage(ws, msg, subscriptions) {
834
843
  try {
835
844
  const id = uuid().slice(0, 8);
836
845
  taskStore.createTask(id, projectId, title, msg.payload?.description || "", msg.payload?.dependsOn || [], slugify(project.name), parentTaskId, canDecompose, msg.payload?.defaultPersonaId || "");
837
- const row = taskStore.getTask(id);
838
- broadcast({
839
- type: "task_created",
840
- payload: {
841
- task: row
842
- ? { ...row, dependsOn: safeParseJsonArray(row.dependsOn) }
843
- : null,
844
- requestId,
845
- },
846
- });
846
+ emit("task.created", { taskId: id, projectId, requestId });
847
847
  }
848
848
  catch (error) {
849
849
  const message = error instanceof Error ? error.message : "Failed to create task";
@@ -897,10 +897,7 @@ async function handleMessage(ws, msg, subscriptions) {
897
897
  sendWs(ws, { type: "error", payload: { message: String(err) } });
898
898
  return;
899
899
  }
900
- broadcast({
901
- type: "task_started",
902
- payload: { taskId: updateTaskId, sessionId: lateBindSessionId, projectId: existingTask.projectId },
903
- });
900
+ emit("task.started", { taskId: updateTaskId, sessionId: lateBindSessionId, projectId: existingTask.projectId });
904
901
  break;
905
902
  }
906
903
  // Only allow editing not_started tasks (non-late-bind path)
@@ -929,17 +926,7 @@ async function handleMessage(ws, msg, subscriptions) {
929
926
  ? msg.payload.defaultPersonaId
930
927
  : undefined;
931
928
  taskStore.updateTask(updateTaskId, updatedTitle, updatedDescription, existingTask.status, updatedDependsOn, updatedDefaultPersonaId);
932
- const updatedRow = taskStore.getTask(updateTaskId);
933
- broadcast({
934
- type: "task_updated",
935
- payload: {
936
- taskId: updateTaskId,
937
- projectId: existingTask.projectId,
938
- task: updatedRow
939
- ? { ...updatedRow, dependsOn: safeParseJsonArray(updatedRow.dependsOn) }
940
- : null,
941
- },
942
- });
929
+ emit("task.updated", { taskId: updateTaskId, projectId: existingTask.projectId });
943
930
  break;
944
931
  }
945
932
  case "start_task": {
@@ -999,10 +986,7 @@ async function handleMessage(ws, msg, subscriptions) {
999
986
  },
1000
987
  });
1001
988
  if (task) {
1002
- broadcast({
1003
- type: "task_completed",
1004
- payload: { taskId, projectId: task.projectId },
1005
- });
989
+ emit("task.completed", { taskId, projectId: task.projectId });
1006
990
  }
1007
991
  break;
1008
992
  }
@@ -1055,10 +1039,7 @@ async function handleMessage(ws, msg, subscriptions) {
1055
1039
  projectId: task.projectId,
1056
1040
  taskId: task.id,
1057
1041
  });
1058
- broadcast({
1059
- type: "task_started",
1060
- payload: { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId },
1061
- });
1042
+ emit("task.started", { taskId: task.id, sessionId: latestSession.id, projectId: task.projectId });
1062
1043
  break;
1063
1044
  }
1064
1045
  case "delete_task": {
@@ -1107,7 +1088,7 @@ async function handleMessage(ws, msg, subscriptions) {
1107
1088
  sendWs(ws, { type: "error", payload: { message: `Failed to delete task ${taskId}: no rows affected` } });
1108
1089
  return;
1109
1090
  }
1110
- broadcast({ type: "task_deleted", payload: { taskId, projectId: deletedTask.projectId } });
1091
+ emit("task.deleted", { taskId, projectId: deletedTask.projectId });
1111
1092
  break;
1112
1093
  }
1113
1094
  // ─── Task Sessions ─────────────────────────────────────
@@ -1258,7 +1239,7 @@ async function handleMessage(ws, msg, subscriptions) {
1258
1239
  }
1259
1240
  logger.info({ environmentId, adapterType: env.adapterType }, "Provisioning environment");
1260
1241
  envRegistry.updateEnvironmentStatus(environmentId, "connecting");
1261
- broadcastEnvironments();
1242
+ emit("environment.changed", {});
1262
1243
  // Run provision in background, broadcasting progress to all connected clients
1263
1244
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1264
1245
  (async () => {
@@ -1267,14 +1248,11 @@ async function handleMessage(ws, msg, subscriptions) {
1267
1248
  const powerlineToken = env.powerlineToken || "";
1268
1249
  for await (const event of reconnectOrProvision(environmentId, adapter, config, powerlineToken, !!env.bootstrapped)) {
1269
1250
  logger.info({ environmentId, stage: event.stage, message: event.message }, "Provision progress");
1270
- broadcast({
1271
- type: "provision_progress",
1272
- payload: {
1273
- environmentId,
1274
- stage: event.stage,
1275
- message: event.message,
1276
- progress: event.progress,
1277
- },
1251
+ emit("environment.provision_progress", {
1252
+ environmentId,
1253
+ stage: event.stage,
1254
+ message: event.message,
1255
+ progress: event.progress,
1278
1256
  });
1279
1257
  }
1280
1258
  logger.info({ environmentId }, "Provision complete, calling adapter.connect");
@@ -1285,31 +1263,25 @@ async function handleMessage(ws, msg, subscriptions) {
1285
1263
  envRegistry.updateEnvironmentStatus(environmentId, "connected");
1286
1264
  envRegistry.markBootstrapped(environmentId);
1287
1265
  logger.info({ environmentId }, "Environment connected");
1288
- broadcast({
1289
- type: "provision_progress",
1290
- payload: {
1291
- environmentId,
1292
- stage: "ready",
1293
- message: "Environment connected",
1294
- progress: 1,
1295
- },
1266
+ emit("environment.provision_progress", {
1267
+ environmentId,
1268
+ stage: "ready",
1269
+ message: "Environment connected",
1270
+ progress: 1,
1296
1271
  });
1297
1272
  }
1298
1273
  catch (err) {
1299
1274
  logger.error({ environmentId, err }, "Provision failed");
1300
1275
  envRegistry.updateEnvironmentStatus(environmentId, "error");
1301
1276
  const errorMessage = err instanceof Error ? err.message : String(err);
1302
- broadcast({
1303
- type: "provision_progress",
1304
- payload: {
1305
- environmentId,
1306
- stage: "error",
1307
- message: `Connection failed: ${errorMessage}`,
1308
- progress: 0,
1309
- },
1277
+ emit("environment.provision_progress", {
1278
+ environmentId,
1279
+ stage: "error",
1280
+ message: `Connection failed: ${errorMessage}`,
1281
+ progress: 0,
1310
1282
  });
1311
1283
  }
1312
- broadcastEnvironments();
1284
+ emit("environment.changed", {});
1313
1285
  })();
1314
1286
  break;
1315
1287
  }
@@ -1338,7 +1310,7 @@ async function handleMessage(ws, msg, subscriptions) {
1338
1310
  adapterManager.removeConnection(environmentId);
1339
1311
  envRegistry.updateEnvironmentStatus(environmentId, "disconnected");
1340
1312
  logger.info({ environmentId }, "Environment stopped");
1341
- broadcastEnvironments();
1313
+ emit("environment.changed", {});
1342
1314
  break;
1343
1315
  }
1344
1316
  case "add_environment": {
@@ -1399,8 +1371,8 @@ async function handleMessage(ws, msg, subscriptions) {
1399
1371
  }
1400
1372
  envRegistry.addEnvironment(id, displayName, adapterType, adapterConfig);
1401
1373
  logger.info({ id, displayName, adapterType }, "Environment added via WebSocket");
1402
- broadcast({ type: "environment_added", payload: { environmentId: id } });
1403
- broadcastEnvironments();
1374
+ emit("environment.added", { environmentId: id });
1375
+ emit("environment.changed", {});
1404
1376
  break;
1405
1377
  }
1406
1378
  case "remove_environment": {
@@ -1435,8 +1407,8 @@ async function handleMessage(ws, msg, subscriptions) {
1435
1407
  sessionStore.deleteByEnvironment(environmentId);
1436
1408
  envRegistry.removeEnvironment(environmentId);
1437
1409
  logger.info({ environmentId }, "Environment removed");
1438
- broadcast({ type: "environment_removed", payload: { environmentId } });
1439
- broadcastEnvironments();
1410
+ emit("environment.removed", { environmentId });
1411
+ emit("environment.changed", {});
1440
1412
  break;
1441
1413
  }
1442
1414
  // ─── Codespaces ─────────────────────────────────────
@@ -1536,7 +1508,7 @@ async function handleMessage(ws, msg, subscriptions) {
1536
1508
  value,
1537
1509
  expiresAt: msg.payload?.expiresAt || "",
1538
1510
  });
1539
- broadcast({ type: "token_changed" });
1511
+ emit("token.changed", {});
1540
1512
  break;
1541
1513
  }
1542
1514
  case "delete_token": {
@@ -1546,7 +1518,7 @@ async function handleMessage(ws, msg, subscriptions) {
1546
1518
  return;
1547
1519
  }
1548
1520
  await tokenBroker.deleteToken(tokenName);
1549
- broadcast({ type: "token_changed" });
1521
+ emit("token.changed", {});
1550
1522
  break;
1551
1523
  }
1552
1524
  case "get_credential_providers": {
@@ -1563,10 +1535,7 @@ async function handleMessage(ws, msg, subscriptions) {
1563
1535
  return;
1564
1536
  }
1565
1537
  credentialProviders.setCredentialProviders(msg.payload);
1566
- broadcast({
1567
- type: "credential_providers",
1568
- payload: credentialProviders.getCredentialProviders(),
1569
- });
1538
+ emit("credential.providers_changed", credentialProviders.getCredentialProviders());
1570
1539
  break;
1571
1540
  }
1572
1541
  }