jinn-cli 0.8.0 → 0.9.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.
Files changed (94) hide show
  1. package/dist/package.json +2 -2
  2. package/dist/src/connectors/discord/index.d.ts +6 -0
  3. package/dist/src/connectors/discord/index.d.ts.map +1 -1
  4. package/dist/src/connectors/discord/index.js +9 -3
  5. package/dist/src/connectors/discord/index.js.map +1 -1
  6. package/dist/src/connectors/discord/threads.d.ts +1 -1
  7. package/dist/src/connectors/discord/threads.d.ts.map +1 -1
  8. package/dist/src/connectors/discord/threads.js +4 -4
  9. package/dist/src/connectors/discord/threads.js.map +1 -1
  10. package/dist/src/gateway/__tests__/org-hierarchy.test.d.ts +2 -0
  11. package/dist/src/gateway/__tests__/org-hierarchy.test.d.ts.map +1 -0
  12. package/dist/src/gateway/__tests__/org-hierarchy.test.js +161 -0
  13. package/dist/src/gateway/__tests__/org-hierarchy.test.js.map +1 -0
  14. package/dist/src/gateway/__tests__/services.test.js +139 -151
  15. package/dist/src/gateway/__tests__/services.test.js.map +1 -1
  16. package/dist/src/gateway/api.d.ts +5 -0
  17. package/dist/src/gateway/api.d.ts.map +1 -1
  18. package/dist/src/gateway/api.js +242 -64
  19. package/dist/src/gateway/api.js.map +1 -1
  20. package/dist/src/gateway/org-hierarchy.d.ts +5 -0
  21. package/dist/src/gateway/org-hierarchy.d.ts.map +1 -0
  22. package/dist/src/gateway/org-hierarchy.js +174 -0
  23. package/dist/src/gateway/org-hierarchy.js.map +1 -0
  24. package/dist/src/gateway/org.d.ts.map +1 -1
  25. package/dist/src/gateway/org.js +6 -0
  26. package/dist/src/gateway/org.js.map +1 -1
  27. package/dist/src/gateway/server.d.ts.map +1 -1
  28. package/dist/src/gateway/server.js +235 -5
  29. package/dist/src/gateway/server.js.map +1 -1
  30. package/dist/src/gateway/services.d.ts +20 -9
  31. package/dist/src/gateway/services.d.ts.map +1 -1
  32. package/dist/src/gateway/services.js +73 -34
  33. package/dist/src/gateway/services.js.map +1 -1
  34. package/dist/src/sessions/context.d.ts +1 -0
  35. package/dist/src/sessions/context.d.ts.map +1 -1
  36. package/dist/src/sessions/context.js +113 -5
  37. package/dist/src/sessions/context.js.map +1 -1
  38. package/dist/src/sessions/fork.d.ts +30 -0
  39. package/dist/src/sessions/fork.d.ts.map +1 -0
  40. package/dist/src/sessions/fork.js +178 -0
  41. package/dist/src/sessions/fork.js.map +1 -0
  42. package/dist/src/sessions/manager.d.ts.map +1 -1
  43. package/dist/src/sessions/manager.js +8 -0
  44. package/dist/src/sessions/manager.js.map +1 -1
  45. package/dist/src/sessions/registry.d.ts +8 -0
  46. package/dist/src/sessions/registry.d.ts.map +1 -1
  47. package/dist/src/sessions/registry.js +36 -0
  48. package/dist/src/sessions/registry.js.map +1 -1
  49. package/dist/src/shared/types.d.ts +66 -0
  50. package/dist/src/shared/types.d.ts.map +1 -1
  51. package/dist/web/404.html +1 -1
  52. package/dist/web/_next/static/chunks/{155-d60b5aa6e01f7738.js → 155-655a087df45da990.js} +1 -1
  53. package/dist/web/_next/static/chunks/192-c0150e3d227be735.js +1 -0
  54. package/dist/web/_next/static/chunks/579-09eb1d7ab35333fc.js +1 -0
  55. package/dist/web/_next/static/chunks/943.7dec3c35287ad545.js +1 -0
  56. package/dist/web/_next/static/chunks/app/chat/page-4ba0008df3b5d032.js +1 -0
  57. package/dist/web/_next/static/chunks/app/cron/page-648f892629b4af65.js +1 -0
  58. package/dist/web/_next/static/chunks/app/kanban/{page-0aa74ece5109e58f.js → page-5e00ac0363936a53.js} +1 -1
  59. package/dist/web/_next/static/chunks/app/org/page-0f7617805f6f6eb7.js +1 -0
  60. package/dist/web/_next/static/chunks/app/settings/page-5588e1b2bb8a5196.js +1 -0
  61. package/dist/web/_next/static/chunks/{webpack-2e375360ad2078fe.js → webpack-21588284a59cb80f.js} +1 -1
  62. package/dist/web/_next/static/css/334114aa3839160f.css +1 -0
  63. package/dist/web/chat.html +1 -1
  64. package/dist/web/chat.txt +4 -4
  65. package/dist/web/cron.html +1 -1
  66. package/dist/web/cron.txt +4 -4
  67. package/dist/web/index.html +1 -1
  68. package/dist/web/index.txt +4 -4
  69. package/dist/web/kanban.html +1 -1
  70. package/dist/web/kanban.txt +4 -4
  71. package/dist/web/logs.html +2 -2
  72. package/dist/web/logs.txt +4 -4
  73. package/dist/web/org.html +1 -1
  74. package/dist/web/org.txt +4 -4
  75. package/dist/web/sessions.html +1 -1
  76. package/dist/web/sessions.txt +3 -3
  77. package/dist/web/settings.html +1 -1
  78. package/dist/web/settings.txt +4 -4
  79. package/dist/web/skills.html +1 -1
  80. package/dist/web/skills.txt +4 -4
  81. package/package.json +2 -2
  82. package/template/AGENTS.md +132 -31
  83. package/template/CLAUDE.md +186 -24
  84. package/template/skills/management/SKILL.md +32 -9
  85. package/dist/web/_next/static/chunks/192-8281220a22c7a155.js +0 -1
  86. package/dist/web/_next/static/chunks/379-11f5e4203461fd6a.js +0 -1
  87. package/dist/web/_next/static/chunks/943.c30215b2dbec402b.js +0 -1
  88. package/dist/web/_next/static/chunks/app/chat/page-3a532ea2d0f8b961.js +0 -1
  89. package/dist/web/_next/static/chunks/app/cron/page-9787c557594dc688.js +0 -1
  90. package/dist/web/_next/static/chunks/app/org/page-99d1b67b8f17e4d5.js +0 -1
  91. package/dist/web/_next/static/chunks/app/settings/page-c2b014fb0706aa88.js +0 -1
  92. package/dist/web/_next/static/css/214f7107c416b9a3.css +0 -1
  93. /package/dist/web/_next/static/{yhgZ2UX_j_Aier68GaRfB → VBCQXj1bVyMDiXQTiHQ-p}/_buildManifest.js +0 -0
  94. /package/dist/web/_next/static/{yhgZ2UX_j_Aier68GaRfB → VBCQXj1bVyMDiXQTiHQ-p}/_ssgManifest.js +0 -0
@@ -5,7 +5,8 @@ import path from "node:path";
5
5
  import yaml from "js-yaml";
6
6
  import { isInterruptibleEngine } from "../shared/types.js";
7
7
  import { buildContext } from "../sessions/context.js";
8
- import { initDb, listSessions, getSession, createSession, updateSession, deleteSession, deleteSessions, insertMessage, getMessages, enqueueQueueItem, cancelQueueItem, getQueueItems, cancelAllPendingQueueItems, listAllPendingQueueItems, getFile, } from "../sessions/registry.js";
8
+ import { initDb, listSessions, getSession, createSession, updateSession, deleteSession, deleteSessions, duplicateSession, insertMessage, getMessages, enqueueQueueItem, cancelQueueItem, getQueueItems, cancelAllPendingQueueItems, listAllPendingQueueItems, getFile, } from "../sessions/registry.js";
9
+ import { forkEngineSession } from "../sessions/fork.js";
9
10
  import { CONFIG_PATH, CRON_RUNS, ORG_DIR, SKILLS_DIR, LOGS_DIR, TMP_DIR, FILES_DIR, } from "../shared/paths.js";
10
11
  import { logger } from "../shared/logger.js";
11
12
  import { getSttStatus, downloadModel, transcribe as sttTranscribe, resolveLanguages, WHISPER_LANGUAGES } from "../stt/stt.js";
@@ -185,12 +186,34 @@ function badRequest(res, message) {
185
186
  function serverError(res, message) {
186
187
  json(res, { error: message }, 500);
187
188
  }
189
+ const SANITIZED_KEYS = new Set(["token", "botToken", "signingSecret", "appToken"]);
188
190
  function deepMerge(target, source) {
189
191
  const result = { ...target };
190
192
  for (const key of Object.keys(source)) {
191
193
  const sv = source[key];
192
194
  const tv = target[key];
193
- if (sv && typeof sv === "object" && !Array.isArray(sv) && tv && typeof tv === "object" && !Array.isArray(tv)) {
195
+ // Skip sanitized secret placeholders keep original value
196
+ if (SANITIZED_KEYS.has(key) && sv === "***")
197
+ continue;
198
+ if (Array.isArray(sv)) {
199
+ // For arrays (e.g. instances), preserve secrets from matching items
200
+ if (Array.isArray(tv)) {
201
+ result[key] = sv.map((item) => {
202
+ if (item && typeof item === "object" && !Array.isArray(item)) {
203
+ const srcItem = item;
204
+ // Find matching target item by id
205
+ const matchTarget = tv.find((t) => t && typeof t === "object" && t.id === srcItem.id);
206
+ if (matchTarget)
207
+ return deepMerge(matchTarget, srcItem);
208
+ }
209
+ return item;
210
+ });
211
+ }
212
+ else {
213
+ result[key] = sv;
214
+ }
215
+ }
216
+ else if (sv && typeof sv === "object" && !Array.isArray(sv) && tv && typeof tv === "object" && !Array.isArray(tv)) {
194
217
  result[key] = deepMerge(tv, sv);
195
218
  }
196
219
  else {
@@ -395,6 +418,45 @@ export async function handleApiRequest(req, res, context) {
395
418
  context.emit("session:updated", { sessionId: params.id });
396
419
  return json(res, { status: "reset", sessionId: params.id });
397
420
  }
421
+ // POST /api/sessions/:id/duplicate — duplicate a session (snapshot fork)
422
+ params = matchRoute("/api/sessions/:id/duplicate", pathname);
423
+ if (method === "POST" && params) {
424
+ const source = getSession(params.id);
425
+ if (!source)
426
+ return notFound(res);
427
+ if (!source.engineSessionId) {
428
+ res.writeHead(400, { "Content-Type": "application/json" });
429
+ res.end(JSON.stringify({ error: "Session has no engine session ID — cannot duplicate" }));
430
+ return;
431
+ }
432
+ let newSessionId = null;
433
+ try {
434
+ // 1. Duplicate session + messages in the registry
435
+ const { session: newSession, messageCount } = duplicateSession(params.id);
436
+ newSessionId = newSession.id;
437
+ // 2. Fork the engine session (Claude/Codex/Gemini)
438
+ const forkResult = forkEngineSession(source.engine, source.engineSessionId, JINN_HOME);
439
+ // 3. Store the new engine session ID
440
+ updateSession(newSession.id, { engineSessionId: forkResult.engineSessionId });
441
+ const result = getSession(newSession.id);
442
+ logger.info(`Session duplicated: ${params.id} → ${newSession.id} (engine: ${forkResult.engineSessionId}, ${messageCount} messages)`);
443
+ context.emit("session:created", { sessionId: newSession.id });
444
+ return json(res, serializeSession(result, context));
445
+ }
446
+ catch (err) {
447
+ // Clean up orphaned session if the engine fork failed after DB insert
448
+ if (newSessionId) {
449
+ try {
450
+ deleteSession(newSessionId);
451
+ }
452
+ catch { /* best effort */ }
453
+ }
454
+ logger.error(`Failed to duplicate session ${params.id}: ${err.message}`);
455
+ res.writeHead(500, { "Content-Type": "application/json" });
456
+ res.end(JSON.stringify({ error: `Duplicate failed: ${err.message}` }));
457
+ return;
458
+ }
459
+ }
398
460
  // DELETE /api/sessions/:id/queue/:itemId — cancel specific item
399
461
  const queueItemParams = matchRoute("/api/sessions/:id/queue/:itemId", pathname);
400
462
  if (method === "DELETE" && queueItemParams) {
@@ -757,62 +819,55 @@ export async function handleApiRequest(req, res, context) {
757
819
  // GET /api/org
758
820
  if (method === "GET" && pathname === "/api/org") {
759
821
  if (!fs.existsSync(ORG_DIR))
760
- return json(res, { departments: [], employees: [] });
822
+ return json(res, { departments: [], employees: [], hierarchy: { root: null, sorted: [], warnings: [] } });
761
823
  const entries = fs.readdirSync(ORG_DIR, { withFileTypes: true });
762
824
  const departments = entries
763
825
  .filter((e) => e.isDirectory())
764
826
  .map((e) => e.name);
765
- const employees = [];
766
- // Scan root-level YAML files
767
- for (const e of entries) {
768
- if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))) {
769
- employees.push(e.name.replace(/\.ya?ml$/, ""));
770
- }
771
- }
772
- // Scan employees/ subdirectory
773
- const employeesDir = path.join(ORG_DIR, "employees");
774
- if (fs.existsSync(employeesDir)) {
775
- const empEntries = fs.readdirSync(employeesDir, { withFileTypes: true });
776
- for (const e of empEntries) {
777
- if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))) {
778
- employees.push(e.name.replace(/\.ya?ml$/, ""));
779
- }
780
- }
781
- }
782
- // Scan inside each department directory for YAML files (excluding department.yaml)
783
- for (const dept of departments) {
784
- const deptDir = path.join(ORG_DIR, dept);
785
- const deptEntries = fs.readdirSync(deptDir, { withFileTypes: true });
786
- for (const e of deptEntries) {
787
- if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml")) && e.name !== "department.yaml") {
788
- employees.push(e.name.replace(/\.ya?ml$/, ""));
789
- }
790
- }
791
- }
792
- return json(res, { departments, employees });
827
+ const { scanOrg } = await import("./org.js");
828
+ const { resolveOrgHierarchy } = await import("./org-hierarchy.js");
829
+ const orgRegistry = scanOrg();
830
+ const hierarchy = resolveOrgHierarchy(orgRegistry);
831
+ const employees = hierarchy.sorted.map((name) => {
832
+ const node = hierarchy.nodes[name];
833
+ const emp = node.employee;
834
+ const { persona, ...rest } = emp;
835
+ return {
836
+ ...rest,
837
+ parentName: node.parentName,
838
+ directReports: node.directReports,
839
+ depth: node.depth,
840
+ chain: node.chain,
841
+ };
842
+ });
843
+ return json(res, {
844
+ departments,
845
+ employees,
846
+ hierarchy: {
847
+ root: hierarchy.root,
848
+ sorted: hierarchy.sorted,
849
+ warnings: hierarchy.warnings,
850
+ },
851
+ });
793
852
  }
794
853
  // GET /api/org/employees/:name
795
854
  params = matchRoute("/api/org/employees/:name", pathname);
796
855
  if (method === "GET" && params) {
797
- const candidates = [
798
- path.join(ORG_DIR, "employees", `${params.name}.yaml`),
799
- path.join(ORG_DIR, "employees", `${params.name}.yml`),
800
- path.join(ORG_DIR, `${params.name}.yaml`),
801
- path.join(ORG_DIR, `${params.name}.yml`),
802
- ];
803
- // Also search inside each department directory
804
- if (fs.existsSync(ORG_DIR)) {
805
- const dirs = fs.readdirSync(ORG_DIR, { withFileTypes: true }).filter((e) => e.isDirectory());
806
- for (const dir of dirs) {
807
- candidates.push(path.join(ORG_DIR, dir.name, `${params.name}.yaml`));
808
- candidates.push(path.join(ORG_DIR, dir.name, `${params.name}.yml`));
809
- }
810
- }
811
- const filePath = candidates.find((c) => fs.existsSync(c));
812
- if (!filePath)
856
+ const { scanOrg } = await import("./org.js");
857
+ const { resolveOrgHierarchy } = await import("./org-hierarchy.js");
858
+ const orgRegistry = scanOrg();
859
+ const emp = orgRegistry.get(params.name);
860
+ if (!emp)
813
861
  return notFound(res);
814
- const content = yaml.load(fs.readFileSync(filePath, "utf-8"));
815
- return json(res, content);
862
+ const hierarchy = resolveOrgHierarchy(orgRegistry);
863
+ const node = hierarchy.nodes[params.name];
864
+ return json(res, {
865
+ ...emp,
866
+ parentName: node?.parentName ?? null,
867
+ directReports: node?.directReports ?? [],
868
+ depth: node?.depth ?? 0,
869
+ chain: node?.chain ?? [params.name],
870
+ });
816
871
  }
817
872
  // PATCH /api/org/employees/:name — update employee fields (currently only alwaysNotify)
818
873
  params = matchRoute("/api/org/employees/:name", pathname);
@@ -830,6 +885,88 @@ export async function handleApiRequest(req, res, context) {
830
885
  context.emit("org:updated", { employee: params.name });
831
886
  return json(res, { status: "ok" });
832
887
  }
888
+ // GET /api/org/services — list all cross-department services
889
+ if (method === "GET" && pathname === "/api/org/services") {
890
+ const { scanOrg } = await import("./org.js");
891
+ const { buildServiceRegistry } = await import("./services.js");
892
+ const orgRegistry = scanOrg();
893
+ const services = buildServiceRegistry(orgRegistry);
894
+ const result = Array.from(services.values()).map((entry) => ({
895
+ name: entry.declaration.name,
896
+ description: entry.declaration.description,
897
+ provider: {
898
+ name: entry.provider.name,
899
+ displayName: entry.provider.displayName,
900
+ department: entry.provider.department,
901
+ rank: entry.provider.rank,
902
+ },
903
+ }));
904
+ return json(res, { services: result });
905
+ }
906
+ // POST /api/org/cross-request — route a service request to the provider
907
+ if (method === "POST" && pathname === "/api/org/cross-request") {
908
+ const parsed = await readJsonBody(req, res);
909
+ if (!parsed.ok)
910
+ return;
911
+ const body = parsed.body;
912
+ const { fromEmployee, service, prompt, parentSessionId } = body;
913
+ if (!fromEmployee || !service || !prompt) {
914
+ return badRequest(res, "Missing required fields: fromEmployee, service, prompt");
915
+ }
916
+ const { scanOrg } = await import("./org.js");
917
+ const { resolveOrgHierarchy } = await import("./org-hierarchy.js");
918
+ const { buildServiceRegistry, buildRoutePath, resolveManagerChain } = await import("./services.js");
919
+ const orgRegistry = scanOrg();
920
+ const requester = orgRegistry.get(fromEmployee);
921
+ if (!requester)
922
+ return notFound(res);
923
+ const services = buildServiceRegistry(orgRegistry);
924
+ const entry = services.get(service);
925
+ if (!entry) {
926
+ return json(res, { error: `Service "${service}" not found` }, 404);
927
+ }
928
+ const hierarchy = resolveOrgHierarchy(orgRegistry);
929
+ const route = buildRoutePath(fromEmployee, entry.provider.name, hierarchy);
930
+ const managers = resolveManagerChain(route, hierarchy);
931
+ const crossBrief = `## Cross-service request
932
+
933
+ **From**: ${requester.displayName} (${requester.department})
934
+ **Service**: ${service} — ${entry.declaration.description}
935
+
936
+ ### Request
937
+ ${prompt}
938
+
939
+ ---
940
+ Handle this as a priority request from a colleague.`;
941
+ const config = context.getConfig();
942
+ const session = createSession({
943
+ engine: entry.provider.engine || config.engines.default,
944
+ model: entry.provider.model || undefined,
945
+ source: "cross-request",
946
+ sourceRef: `cross:${fromEmployee}:${service}`,
947
+ connector: "web",
948
+ sessionKey: `cross:${Date.now()}`,
949
+ replyContext: { source: "cross-request" },
950
+ employee: entry.provider.name,
951
+ parentSessionId: parentSessionId || undefined,
952
+ prompt: crossBrief,
953
+ portalName: config.portal?.portalName,
954
+ title: `Cross-request: ${fromEmployee} → ${service}`,
955
+ });
956
+ insertMessage(session.id, "user", crossBrief);
957
+ logger.info(`Cross-request session created: ${session.id} (${fromEmployee} → ${service} → ${entry.provider.name})`);
958
+ return json(res, {
959
+ sessionId: session.id,
960
+ provider: {
961
+ name: entry.provider.name,
962
+ displayName: entry.provider.displayName,
963
+ department: entry.provider.department,
964
+ },
965
+ route,
966
+ managers: managers.map((m) => m.employee.name),
967
+ service,
968
+ }, 201);
969
+ }
833
970
  // GET /api/org/departments/:name/board
834
971
  params = matchRoute("/api/org/departments/:name/board", pathname);
835
972
  if (method === "GET" && params) {
@@ -989,18 +1126,34 @@ export async function handleApiRequest(req, res, context) {
989
1126
  if (method === "GET" && pathname === "/api/config") {
990
1127
  const config = context.getConfig();
991
1128
  // Sanitize: remove any secrets/tokens from connectors
992
- const sanitized = {
993
- ...config,
994
- connectors: Object.fromEntries(Object.entries(config.connectors || {}).map(([k, v]) => [
995
- k,
996
- {
1129
+ const rawConnectors = config.connectors || {};
1130
+ const sanitizedConnectors = {};
1131
+ for (const [k, v] of Object.entries(rawConnectors)) {
1132
+ if (k === "instances" && Array.isArray(v)) {
1133
+ sanitizedConnectors.instances = v.map((inst) => ({
1134
+ ...inst,
1135
+ token: inst?.token ? "***" : undefined,
1136
+ signingSecret: inst?.signingSecret ? "***" : undefined,
1137
+ botToken: inst?.botToken ? "***" : undefined,
1138
+ appToken: inst?.appToken ? "***" : undefined,
1139
+ }));
1140
+ }
1141
+ else if (v && typeof v === "object") {
1142
+ sanitizedConnectors[k] = {
997
1143
  ...v,
998
1144
  token: v?.token ? "***" : undefined,
999
1145
  signingSecret: v?.signingSecret ? "***" : undefined,
1000
1146
  botToken: v?.botToken ? "***" : undefined,
1001
1147
  appToken: v?.appToken ? "***" : undefined,
1002
- },
1003
- ])),
1148
+ };
1149
+ }
1150
+ else {
1151
+ sanitizedConnectors[k] = v;
1152
+ }
1153
+ }
1154
+ const sanitized = {
1155
+ ...config,
1156
+ connectors: sanitizedConnectors,
1004
1157
  };
1005
1158
  return json(res, sanitized);
1006
1159
  }
@@ -1080,9 +1233,26 @@ export async function handleApiRequest(req, res, context) {
1080
1233
  const lines = allLines.slice(-n);
1081
1234
  return json(res, { lines });
1082
1235
  }
1083
- // POST /api/connectors/discord/incomingreceive proxied Discord messages from primary instance
1084
- if (method === "POST" && pathname === "/api/connectors/discord/incoming") {
1085
- const connector = context.connectors.get("discord");
1236
+ // POST /api/connectors/reloadstop all instance connectors and restart from config
1237
+ if (method === "POST" && pathname === "/api/connectors/reload") {
1238
+ if (!context.reloadConnectorInstances) {
1239
+ return json(res, { error: "Connector reload not available" }, 501);
1240
+ }
1241
+ try {
1242
+ const result = await context.reloadConnectorInstances();
1243
+ context.emit("connectors:reloaded", result);
1244
+ return json(res, result);
1245
+ }
1246
+ catch (err) {
1247
+ return json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
1248
+ }
1249
+ }
1250
+ // POST /api/connectors/:id/incoming — receive proxied Discord messages from primary instance
1251
+ // Supports both the legacy /api/connectors/discord/incoming and named instance ids
1252
+ params = matchRoute("/api/connectors/:id/incoming", pathname);
1253
+ if (method === "POST" && params && params.id) {
1254
+ // Try the exact instance id first, then fall back to "discord" for the legacy path
1255
+ const connector = context.connectors.get(params.id) ?? (params.id === "discord" ? context.connectors.get("discord") : undefined);
1086
1256
  if (!connector)
1087
1257
  return notFound(res);
1088
1258
  if (!("deliverMessage" in connector)) {
@@ -1108,7 +1278,7 @@ export async function handleApiRequest(req, res, context) {
1108
1278
  return att;
1109
1279
  }));
1110
1280
  const incomingMsg = {
1111
- connector: "discord",
1281
+ connector: params.id,
1112
1282
  source: "discord",
1113
1283
  sessionKey: body.sessionKey,
1114
1284
  channel: body.channel,
@@ -1126,9 +1296,11 @@ export async function handleApiRequest(req, res, context) {
1126
1296
  connector.deliverMessage(incomingMsg);
1127
1297
  return json(res, { status: "delivered" });
1128
1298
  }
1129
- // POST /api/connectors/discord/proxy — proxy connector operations from remote instances
1130
- if (method === "POST" && pathname === "/api/connectors/discord/proxy") {
1131
- const connector = context.connectors.get("discord");
1299
+ // POST /api/connectors/:id/proxy — proxy connector operations from remote instances
1300
+ // Supports both the legacy /api/connectors/discord/proxy and named instance ids
1301
+ params = matchRoute("/api/connectors/:id/proxy", pathname);
1302
+ if (method === "POST" && params && params.id) {
1303
+ const connector = context.connectors.get(params.id) ?? (params.id === "discord" ? context.connectors.get("discord") : undefined);
1132
1304
  if (!connector)
1133
1305
  return notFound(res);
1134
1306
  const _parsed = await readJsonBody(req, res);
@@ -1204,8 +1376,10 @@ export async function handleApiRequest(req, res, context) {
1204
1376
  }
1205
1377
  // GET /api/connectors — list available connectors
1206
1378
  if (method === "GET" && pathname === "/api/connectors") {
1207
- const connectors = Array.from(context.connectors.values()).map((connector) => ({
1379
+ const connectors = Array.from(context.connectors.entries()).map(([instanceId, connector]) => ({
1208
1380
  name: connector.name,
1381
+ instanceId,
1382
+ employee: connector.getEmployee?.() ?? undefined,
1209
1383
  ...connector.getHealth(),
1210
1384
  }));
1211
1385
  return json(res, connectors);
@@ -1718,6 +1892,9 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
1718
1892
  const registry = scanOrg();
1719
1893
  employee = findEmployee(currentSession.employee, registry);
1720
1894
  }
1895
+ const { scanOrg: scanOrgForHierarchy } = await import("./org.js");
1896
+ const { resolveOrgHierarchy } = await import("./org-hierarchy.js");
1897
+ const orgHierarchy = resolveOrgHierarchy(scanOrgForHierarchy());
1721
1898
  try {
1722
1899
  const systemPrompt = buildContext({
1723
1900
  source: "web",
@@ -1727,6 +1904,7 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
1727
1904
  connectors: Array.from(context.connectors.keys()),
1728
1905
  config,
1729
1906
  sessionId: currentSession.id,
1907
+ hierarchy: orgHierarchy,
1730
1908
  });
1731
1909
  const engineConfig = currentSession.engine === "codex"
1732
1910
  ? config.engines.codex