jinn-cli 0.7.8 → 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.
- package/dist/bin/jimmy.js +2 -1
- package/dist/bin/jimmy.js.map +1 -1
- package/dist/package.json +54 -0
- package/dist/src/cli/__tests__/migrate.test.d.ts +2 -0
- package/dist/src/cli/__tests__/migrate.test.d.ts.map +1 -0
- package/dist/src/cli/__tests__/migrate.test.js +79 -0
- package/dist/src/cli/__tests__/migrate.test.js.map +1 -0
- package/dist/src/cli/migrate.d.ts.map +1 -1
- package/dist/src/cli/migrate.js +3 -2
- package/dist/src/cli/migrate.js.map +1 -1
- package/dist/src/connectors/discord/index.d.ts +6 -0
- package/dist/src/connectors/discord/index.d.ts.map +1 -1
- package/dist/src/connectors/discord/index.js +9 -3
- package/dist/src/connectors/discord/index.js.map +1 -1
- package/dist/src/connectors/discord/threads.d.ts +1 -1
- package/dist/src/connectors/discord/threads.d.ts.map +1 -1
- package/dist/src/connectors/discord/threads.js +4 -4
- package/dist/src/connectors/discord/threads.js.map +1 -1
- package/dist/src/connectors/telegram/__tests__/connector.test.d.ts +2 -0
- package/dist/src/connectors/telegram/__tests__/connector.test.d.ts.map +1 -0
- package/dist/src/connectors/telegram/__tests__/connector.test.js +257 -0
- package/dist/src/connectors/telegram/__tests__/connector.test.js.map +1 -0
- package/dist/src/connectors/telegram/__tests__/format.test.d.ts +2 -0
- package/dist/src/connectors/telegram/__tests__/format.test.d.ts.map +1 -0
- package/dist/src/connectors/telegram/__tests__/format.test.js +79 -0
- package/dist/src/connectors/telegram/__tests__/format.test.js.map +1 -0
- package/dist/src/connectors/telegram/__tests__/threads.test.d.ts +2 -0
- package/dist/src/connectors/telegram/__tests__/threads.test.d.ts.map +1 -0
- package/dist/src/connectors/telegram/__tests__/threads.test.js +51 -0
- package/dist/src/connectors/telegram/__tests__/threads.test.js.map +1 -0
- package/dist/src/connectors/telegram/format.d.ts +12 -0
- package/dist/src/connectors/telegram/format.d.ts.map +1 -0
- package/dist/src/connectors/telegram/format.js +61 -0
- package/dist/src/connectors/telegram/format.js.map +1 -0
- package/dist/src/connectors/telegram/index.d.ts +26 -0
- package/dist/src/connectors/telegram/index.d.ts.map +1 -0
- package/dist/src/connectors/telegram/index.js +185 -0
- package/dist/src/connectors/telegram/index.js.map +1 -0
- package/dist/src/connectors/telegram/threads.d.ts +29 -0
- package/dist/src/connectors/telegram/threads.d.ts.map +1 -0
- package/dist/src/connectors/telegram/threads.js +26 -0
- package/dist/src/connectors/telegram/threads.js.map +1 -0
- package/dist/src/cron/runner.js.map +1 -1
- package/dist/src/engines/__tests__/gemini.test.d.ts +2 -0
- package/dist/src/engines/__tests__/gemini.test.d.ts.map +1 -0
- package/dist/src/engines/__tests__/gemini.test.js +404 -0
- package/dist/src/engines/__tests__/gemini.test.js.map +1 -0
- package/dist/src/engines/gemini.d.ts +32 -0
- package/dist/src/engines/gemini.d.ts.map +1 -0
- package/dist/src/engines/gemini.js +328 -0
- package/dist/src/engines/gemini.js.map +1 -0
- package/dist/src/gateway/__tests__/org-hierarchy.test.d.ts +2 -0
- package/dist/src/gateway/__tests__/org-hierarchy.test.d.ts.map +1 -0
- package/dist/src/gateway/__tests__/org-hierarchy.test.js +161 -0
- package/dist/src/gateway/__tests__/org-hierarchy.test.js.map +1 -0
- package/dist/src/gateway/__tests__/org-update.test.d.ts +2 -0
- package/dist/src/gateway/__tests__/org-update.test.d.ts.map +1 -0
- package/dist/src/gateway/__tests__/org-update.test.js +99 -0
- package/dist/src/gateway/__tests__/org-update.test.js.map +1 -0
- package/dist/src/gateway/__tests__/org.test.d.ts +2 -0
- package/dist/src/gateway/__tests__/org.test.d.ts.map +1 -0
- package/dist/src/gateway/__tests__/org.test.js +77 -0
- package/dist/src/gateway/__tests__/org.test.js.map +1 -0
- package/dist/src/gateway/__tests__/services.test.d.ts +2 -0
- package/dist/src/gateway/__tests__/services.test.d.ts.map +1 -0
- package/dist/src/gateway/__tests__/services.test.js +153 -0
- package/dist/src/gateway/__tests__/services.test.js.map +1 -0
- package/dist/src/gateway/api.d.ts +5 -0
- package/dist/src/gateway/api.d.ts.map +1 -1
- package/dist/src/gateway/api.js +302 -77
- package/dist/src/gateway/api.js.map +1 -1
- package/dist/src/gateway/org-hierarchy.d.ts +5 -0
- package/dist/src/gateway/org-hierarchy.d.ts.map +1 -0
- package/dist/src/gateway/org-hierarchy.js +174 -0
- package/dist/src/gateway/org-hierarchy.js.map +1 -0
- package/dist/src/gateway/org.d.ts +7 -0
- package/dist/src/gateway/org.d.ts.map +1 -1
- package/dist/src/gateway/org.js +64 -0
- package/dist/src/gateway/org.js.map +1 -1
- package/dist/src/gateway/server.d.ts.map +1 -1
- package/dist/src/gateway/server.js +262 -5
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/services.d.ts +28 -0
- package/dist/src/gateway/services.d.ts.map +1 -0
- package/dist/src/gateway/services.js +112 -0
- package/dist/src/gateway/services.js.map +1 -0
- package/dist/src/sessions/__tests__/callbacks.test.js +36 -0
- package/dist/src/sessions/__tests__/callbacks.test.js.map +1 -1
- package/dist/src/sessions/callbacks.d.ts +2 -0
- package/dist/src/sessions/callbacks.d.ts.map +1 -1
- package/dist/src/sessions/callbacks.js +3 -1
- package/dist/src/sessions/callbacks.js.map +1 -1
- package/dist/src/sessions/context.d.ts +1 -0
- package/dist/src/sessions/context.d.ts.map +1 -1
- package/dist/src/sessions/context.js +121 -6
- package/dist/src/sessions/context.js.map +1 -1
- package/dist/src/sessions/fork.d.ts +30 -0
- package/dist/src/sessions/fork.d.ts.map +1 -0
- package/dist/src/sessions/fork.js +178 -0
- package/dist/src/sessions/fork.js.map +1 -0
- package/dist/src/sessions/manager.d.ts.map +1 -1
- package/dist/src/sessions/manager.js +16 -5
- package/dist/src/sessions/manager.js.map +1 -1
- package/dist/src/sessions/registry.d.ts +9 -0
- package/dist/src/sessions/registry.d.ts.map +1 -1
- package/dist/src/sessions/registry.js +40 -0
- package/dist/src/sessions/registry.js.map +1 -1
- package/dist/src/shared/types.d.ts +81 -1
- package/dist/src/shared/types.d.ts.map +1 -1
- package/dist/web/404.html +1 -1
- package/dist/web/_next/static/chunks/155-655a087df45da990.js +1 -0
- package/dist/web/_next/static/chunks/192-c0150e3d227be735.js +1 -0
- package/dist/web/_next/static/chunks/579-09eb1d7ab35333fc.js +1 -0
- package/dist/web/_next/static/chunks/943.7dec3c35287ad545.js +1 -0
- package/dist/web/_next/static/chunks/app/chat/page-4ba0008df3b5d032.js +1 -0
- package/dist/web/_next/static/chunks/app/cron/page-648f892629b4af65.js +1 -0
- package/dist/web/_next/static/chunks/app/kanban/{page-0aa74ece5109e58f.js → page-5e00ac0363936a53.js} +1 -1
- package/dist/web/_next/static/chunks/app/{layout-497d1e93737edeae.js → layout-f5c5165308a903e2.js} +1 -1
- package/dist/web/_next/static/chunks/app/org/page-0f7617805f6f6eb7.js +1 -0
- package/dist/web/_next/static/chunks/app/settings/page-5588e1b2bb8a5196.js +1 -0
- package/dist/web/_next/static/chunks/{webpack-2e375360ad2078fe.js → webpack-21588284a59cb80f.js} +1 -1
- package/dist/web/_next/static/css/334114aa3839160f.css +1 -0
- package/dist/web/chat.html +1 -1
- package/dist/web/chat.txt +5 -5
- package/dist/web/cron.html +1 -1
- package/dist/web/cron.txt +5 -5
- package/dist/web/index.html +1 -1
- package/dist/web/index.txt +5 -5
- package/dist/web/kanban.html +1 -1
- package/dist/web/kanban.txt +5 -5
- package/dist/web/logs.html +2 -2
- package/dist/web/logs.txt +5 -5
- package/dist/web/org.html +1 -1
- package/dist/web/org.txt +5 -5
- package/dist/web/sessions.html +1 -1
- package/dist/web/sessions.txt +4 -4
- package/dist/web/settings.html +1 -1
- package/dist/web/settings.txt +5 -5
- package/dist/web/skills.html +1 -1
- package/dist/web/skills.txt +5 -5
- package/package.json +5 -3
- package/template/AGENTS.md +132 -31
- package/template/CLAUDE.md +186 -24
- package/template/migrations/0.8.0/MIGRATION.md +73 -0
- package/template/skills/management/SKILL.md +32 -9
- package/dist/web/_next/static/chunks/155-5843f8840f40f6f8.js +0 -1
- package/dist/web/_next/static/chunks/192-f566317c5713c743.js +0 -1
- package/dist/web/_next/static/chunks/660-7b281827cf68988a.js +0 -1
- package/dist/web/_next/static/chunks/943.c30215b2dbec402b.js +0 -1
- package/dist/web/_next/static/chunks/app/chat/page-01d396112fbc8623.js +0 -1
- package/dist/web/_next/static/chunks/app/cron/page-9787c557594dc688.js +0 -1
- package/dist/web/_next/static/chunks/app/org/page-b14614ed22c7a80d.js +0 -1
- package/dist/web/_next/static/chunks/app/settings/page-c2b014fb0706aa88.js +0 -1
- package/dist/web/_next/static/css/29fd6f6f1915cce0.css +0 -1
- /package/dist/web/_next/static/{73aF2gsMYJ-t64sWQw1Av → VBCQXj1bVyMDiXQTiHQ-p}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{73aF2gsMYJ-t64sWQw1Av → VBCQXj1bVyMDiXQTiHQ-p}/_ssgManifest.js +0 -0
package/dist/src/gateway/api.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
@@ -255,6 +278,7 @@ export async function handleApiRequest(req, res, context) {
|
|
|
255
278
|
default: config.engines.default,
|
|
256
279
|
claude: { model: config.engines.claude.model, available: true },
|
|
257
280
|
codex: { model: config.engines.codex.model, available: true },
|
|
281
|
+
...(config.engines.gemini ? { gemini: { model: config.engines.gemini.model, available: true } } : {}),
|
|
258
282
|
},
|
|
259
283
|
sessions: { total: sessions.length, running, active: running },
|
|
260
284
|
connectors,
|
|
@@ -307,6 +331,34 @@ export async function handleApiRequest(req, res, context) {
|
|
|
307
331
|
}
|
|
308
332
|
return json(res, { ...serializeSession(session, context), messages });
|
|
309
333
|
}
|
|
334
|
+
// PUT /api/sessions/:id
|
|
335
|
+
params = matchRoute("/api/sessions/:id", pathname);
|
|
336
|
+
if (method === "PUT" && params) {
|
|
337
|
+
const session = getSession(params.id);
|
|
338
|
+
if (!session)
|
|
339
|
+
return notFound(res);
|
|
340
|
+
const _parsed = await readJsonBody(req, res);
|
|
341
|
+
if (!_parsed.ok)
|
|
342
|
+
return;
|
|
343
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
344
|
+
const body = _parsed.body;
|
|
345
|
+
const updates = {};
|
|
346
|
+
if (body.title !== undefined) {
|
|
347
|
+
if (typeof body.title !== "string")
|
|
348
|
+
return badRequest(res, "title must be a string");
|
|
349
|
+
const trimmed = body.title.trim();
|
|
350
|
+
if (!trimmed)
|
|
351
|
+
return badRequest(res, "title must not be empty");
|
|
352
|
+
updates.title = trimmed.slice(0, 200);
|
|
353
|
+
}
|
|
354
|
+
if (Object.keys(updates).length === 0)
|
|
355
|
+
return badRequest(res, "no valid fields to update");
|
|
356
|
+
const updated = updateSession(params.id, updates);
|
|
357
|
+
if (!updated)
|
|
358
|
+
return notFound(res);
|
|
359
|
+
context.emit("session:updated", { sessionId: params.id });
|
|
360
|
+
return json(res, serializeSession(updated, context));
|
|
361
|
+
}
|
|
310
362
|
// DELETE /api/sessions/:id
|
|
311
363
|
params = matchRoute("/api/sessions/:id", pathname);
|
|
312
364
|
if (method === "DELETE" && params) {
|
|
@@ -366,6 +418,45 @@ export async function handleApiRequest(req, res, context) {
|
|
|
366
418
|
context.emit("session:updated", { sessionId: params.id });
|
|
367
419
|
return json(res, { status: "reset", sessionId: params.id });
|
|
368
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
|
+
}
|
|
369
460
|
// DELETE /api/sessions/:id/queue/:itemId — cancel specific item
|
|
370
461
|
const queueItemParams = matchRoute("/api/sessions/:id/queue/:itemId", pathname);
|
|
371
462
|
if (method === "DELETE" && queueItemParams) {
|
|
@@ -728,62 +819,153 @@ export async function handleApiRequest(req, res, context) {
|
|
|
728
819
|
// GET /api/org
|
|
729
820
|
if (method === "GET" && pathname === "/api/org") {
|
|
730
821
|
if (!fs.existsSync(ORG_DIR))
|
|
731
|
-
return json(res, { departments: [], employees: [] });
|
|
822
|
+
return json(res, { departments: [], employees: [], hierarchy: { root: null, sorted: [], warnings: [] } });
|
|
732
823
|
const entries = fs.readdirSync(ORG_DIR, { withFileTypes: true });
|
|
733
824
|
const departments = entries
|
|
734
825
|
.filter((e) => e.isDirectory())
|
|
735
826
|
.map((e) => e.name);
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
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
|
+
});
|
|
764
852
|
}
|
|
765
853
|
// GET /api/org/employees/:name
|
|
766
854
|
params = matchRoute("/api/org/employees/:name", pathname);
|
|
767
855
|
if (method === "GET" && params) {
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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)
|
|
861
|
+
return notFound(res);
|
|
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
|
+
});
|
|
871
|
+
}
|
|
872
|
+
// PATCH /api/org/employees/:name — update employee fields (currently only alwaysNotify)
|
|
873
|
+
params = matchRoute("/api/org/employees/:name", pathname);
|
|
874
|
+
if (method === "PATCH" && params) {
|
|
875
|
+
const _parsed = await readJsonBody(req, res);
|
|
876
|
+
if (!_parsed.ok)
|
|
877
|
+
return;
|
|
878
|
+
const body = _parsed.body;
|
|
879
|
+
const { updateEmployeeYaml } = await import("./org.js");
|
|
880
|
+
const updated = updateEmployeeYaml(params.name, {
|
|
881
|
+
alwaysNotify: typeof body.alwaysNotify === "boolean" ? body.alwaysNotify : undefined,
|
|
882
|
+
});
|
|
883
|
+
if (!updated)
|
|
884
|
+
return notFound(res);
|
|
885
|
+
context.emit("org:updated", { employee: params.name });
|
|
886
|
+
return json(res, { status: "ok" });
|
|
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");
|
|
781
915
|
}
|
|
782
|
-
const
|
|
783
|
-
|
|
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)
|
|
784
922
|
return notFound(res);
|
|
785
|
-
const
|
|
786
|
-
|
|
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);
|
|
787
969
|
}
|
|
788
970
|
// GET /api/org/departments/:name/board
|
|
789
971
|
params = matchRoute("/api/org/departments/:name/board", pathname);
|
|
@@ -944,18 +1126,34 @@ export async function handleApiRequest(req, res, context) {
|
|
|
944
1126
|
if (method === "GET" && pathname === "/api/config") {
|
|
945
1127
|
const config = context.getConfig();
|
|
946
1128
|
// Sanitize: remove any secrets/tokens from connectors
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
{
|
|
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] = {
|
|
952
1143
|
...v,
|
|
953
1144
|
token: v?.token ? "***" : undefined,
|
|
954
1145
|
signingSecret: v?.signingSecret ? "***" : undefined,
|
|
955
1146
|
botToken: v?.botToken ? "***" : undefined,
|
|
956
1147
|
appToken: v?.appToken ? "***" : undefined,
|
|
957
|
-
}
|
|
958
|
-
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
sanitizedConnectors[k] = v;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const sanitized = {
|
|
1155
|
+
...config,
|
|
1156
|
+
connectors: sanitizedConnectors,
|
|
959
1157
|
};
|
|
960
1158
|
return json(res, sanitized);
|
|
961
1159
|
}
|
|
@@ -1035,9 +1233,26 @@ export async function handleApiRequest(req, res, context) {
|
|
|
1035
1233
|
const lines = allLines.slice(-n);
|
|
1036
1234
|
return json(res, { lines });
|
|
1037
1235
|
}
|
|
1038
|
-
// POST /api/connectors/
|
|
1039
|
-
if (method === "POST" && pathname === "/api/connectors/
|
|
1040
|
-
|
|
1236
|
+
// POST /api/connectors/reload — stop 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);
|
|
1041
1256
|
if (!connector)
|
|
1042
1257
|
return notFound(res);
|
|
1043
1258
|
if (!("deliverMessage" in connector)) {
|
|
@@ -1063,7 +1278,7 @@ export async function handleApiRequest(req, res, context) {
|
|
|
1063
1278
|
return att;
|
|
1064
1279
|
}));
|
|
1065
1280
|
const incomingMsg = {
|
|
1066
|
-
connector:
|
|
1281
|
+
connector: params.id,
|
|
1067
1282
|
source: "discord",
|
|
1068
1283
|
sessionKey: body.sessionKey,
|
|
1069
1284
|
channel: body.channel,
|
|
@@ -1081,9 +1296,11 @@ export async function handleApiRequest(req, res, context) {
|
|
|
1081
1296
|
connector.deliverMessage(incomingMsg);
|
|
1082
1297
|
return json(res, { status: "delivered" });
|
|
1083
1298
|
}
|
|
1084
|
-
// POST /api/connectors/
|
|
1085
|
-
|
|
1086
|
-
|
|
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);
|
|
1087
1304
|
if (!connector)
|
|
1088
1305
|
return notFound(res);
|
|
1089
1306
|
const _parsed = await readJsonBody(req, res);
|
|
@@ -1159,8 +1376,10 @@ export async function handleApiRequest(req, res, context) {
|
|
|
1159
1376
|
}
|
|
1160
1377
|
// GET /api/connectors — list available connectors
|
|
1161
1378
|
if (method === "GET" && pathname === "/api/connectors") {
|
|
1162
|
-
const connectors = Array.from(context.connectors.
|
|
1379
|
+
const connectors = Array.from(context.connectors.entries()).map(([instanceId, connector]) => ({
|
|
1163
1380
|
name: connector.name,
|
|
1381
|
+
instanceId,
|
|
1382
|
+
employee: connector.getEmployee?.() ?? undefined,
|
|
1164
1383
|
...connector.getHealth(),
|
|
1165
1384
|
}));
|
|
1166
1385
|
return json(res, connectors);
|
|
@@ -1665,15 +1884,18 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
|
|
|
1665
1884
|
lastActivity: new Date().toISOString(),
|
|
1666
1885
|
});
|
|
1667
1886
|
}
|
|
1887
|
+
// If this session has an assigned employee, load their persona
|
|
1888
|
+
let employee;
|
|
1889
|
+
if (currentSession.employee) {
|
|
1890
|
+
const { findEmployee } = await import("./org.js");
|
|
1891
|
+
const { scanOrg } = await import("./org.js");
|
|
1892
|
+
const registry = scanOrg();
|
|
1893
|
+
employee = findEmployee(currentSession.employee, registry);
|
|
1894
|
+
}
|
|
1895
|
+
const { scanOrg: scanOrgForHierarchy } = await import("./org.js");
|
|
1896
|
+
const { resolveOrgHierarchy } = await import("./org-hierarchy.js");
|
|
1897
|
+
const orgHierarchy = resolveOrgHierarchy(scanOrgForHierarchy());
|
|
1668
1898
|
try {
|
|
1669
|
-
// If this session has an assigned employee, load their persona
|
|
1670
|
-
let employee;
|
|
1671
|
-
if (currentSession.employee) {
|
|
1672
|
-
const { findEmployee } = await import("./org.js");
|
|
1673
|
-
const { scanOrg } = await import("./org.js");
|
|
1674
|
-
const registry = scanOrg();
|
|
1675
|
-
employee = findEmployee(currentSession.employee, registry);
|
|
1676
|
-
}
|
|
1677
1899
|
const systemPrompt = buildContext({
|
|
1678
1900
|
source: "web",
|
|
1679
1901
|
channel: currentSession.sourceRef,
|
|
@@ -1682,10 +1904,13 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
|
|
|
1682
1904
|
connectors: Array.from(context.connectors.keys()),
|
|
1683
1905
|
config,
|
|
1684
1906
|
sessionId: currentSession.id,
|
|
1907
|
+
hierarchy: orgHierarchy,
|
|
1685
1908
|
});
|
|
1686
1909
|
const engineConfig = currentSession.engine === "codex"
|
|
1687
1910
|
? config.engines.codex
|
|
1688
|
-
:
|
|
1911
|
+
: currentSession.engine === "gemini"
|
|
1912
|
+
? config.engines.gemini ?? config.engines.claude
|
|
1913
|
+
: config.engines.claude;
|
|
1689
1914
|
const effortLevel = resolveEffort(engineConfig, currentSession, employee);
|
|
1690
1915
|
let lastHeartbeatAt = 0;
|
|
1691
1916
|
const runHeartbeat = setInterval(() => {
|
|
@@ -1831,7 +2056,7 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
|
|
|
1831
2056
|
lastError: fallbackResult.error ?? null,
|
|
1832
2057
|
});
|
|
1833
2058
|
if (completedFallback) {
|
|
1834
|
-
notifyParentSession(completedFallback, { result: fallbackResult.result, error: fallbackResult.error ?? null, cost: fallbackResult.cost, durationMs: fallbackResult.durationMs });
|
|
2059
|
+
notifyParentSession(completedFallback, { result: fallbackResult.result, error: fallbackResult.error ?? null, cost: fallbackResult.cost, durationMs: fallbackResult.durationMs }, { alwaysNotify: employee?.alwaysNotify });
|
|
1835
2060
|
}
|
|
1836
2061
|
context.emit("session:completed", {
|
|
1837
2062
|
sessionId: currentSession.id,
|
|
@@ -1941,7 +2166,7 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
|
|
|
1941
2166
|
if (completedAfterRetry) {
|
|
1942
2167
|
notifyRateLimitResumed(completedAfterRetry);
|
|
1943
2168
|
notifyDiscordChannel(`✅ Claude usage limit cleared. Session ${currentSession.id}${currentSession.employee ? ` (${currentSession.employee})` : ""} resumed.`);
|
|
1944
|
-
notifyParentSession(completedAfterRetry, { result: retryResult.result, error: retryResult.error ?? null, cost: retryResult.cost, durationMs: retryResult.durationMs });
|
|
2169
|
+
notifyParentSession(completedAfterRetry, { result: retryResult.result, error: retryResult.error ?? null, cost: retryResult.cost, durationMs: retryResult.durationMs }, { alwaysNotify: employee?.alwaysNotify });
|
|
1945
2170
|
}
|
|
1946
2171
|
context.emit("session:completed", {
|
|
1947
2172
|
sessionId: currentSession.id,
|
|
@@ -1963,7 +2188,7 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
|
|
|
1963
2188
|
lastError: "Claude usage limit did not clear in time",
|
|
1964
2189
|
});
|
|
1965
2190
|
if (erroredSession) {
|
|
1966
|
-
notifyParentSession(erroredSession, { error: "Claude usage limit did not clear in time" });
|
|
2191
|
+
notifyParentSession(erroredSession, { error: "Claude usage limit did not clear in time" }, { alwaysNotify: employee?.alwaysNotify });
|
|
1967
2192
|
}
|
|
1968
2193
|
context.emit("session:completed", {
|
|
1969
2194
|
sessionId: currentSession.id,
|
|
@@ -1996,7 +2221,7 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
|
|
|
1996
2221
|
}
|
|
1997
2222
|
}
|
|
1998
2223
|
if (completedSession) {
|
|
1999
|
-
notifyParentSession(completedSession, { result: result.result, error: result.error ?? null, cost: result.cost, durationMs: result.durationMs });
|
|
2224
|
+
notifyParentSession(completedSession, { result: result.result, error: result.error ?? null, cost: result.cost, durationMs: result.durationMs }, { alwaysNotify: employee?.alwaysNotify });
|
|
2000
2225
|
}
|
|
2001
2226
|
context.emit("session:completed", {
|
|
2002
2227
|
sessionId: currentSession.id,
|
|
@@ -2023,7 +2248,7 @@ async function runWebSession(session, prompt, engine, config, context, attachmen
|
|
|
2023
2248
|
lastError: errMsg,
|
|
2024
2249
|
});
|
|
2025
2250
|
if (erroredSession) {
|
|
2026
|
-
notifyParentSession(erroredSession, { error: errMsg });
|
|
2251
|
+
notifyParentSession(erroredSession, { error: errMsg }, { alwaysNotify: employee?.alwaysNotify });
|
|
2027
2252
|
}
|
|
2028
2253
|
context.emit("session:completed", {
|
|
2029
2254
|
sessionId: currentSession.id,
|