jinn-cli 0.1.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 (227) hide show
  1. package/dist/bin/jimmy.d.ts +3 -0
  2. package/dist/bin/jimmy.d.ts.map +1 -0
  3. package/dist/bin/jimmy.js +148 -0
  4. package/dist/bin/jimmy.js.map +1 -0
  5. package/dist/src/cli/chrome-allow.d.ts +5 -0
  6. package/dist/src/cli/chrome-allow.d.ts.map +1 -0
  7. package/dist/src/cli/chrome-allow.js +241 -0
  8. package/dist/src/cli/chrome-allow.js.map +1 -0
  9. package/dist/src/cli/create.d.ts +2 -0
  10. package/dist/src/cli/create.d.ts.map +1 -0
  11. package/dist/src/cli/create.js +72 -0
  12. package/dist/src/cli/create.js.map +1 -0
  13. package/dist/src/cli/instances.d.ts +14 -0
  14. package/dist/src/cli/instances.d.ts.map +1 -0
  15. package/dist/src/cli/instances.js +43 -0
  16. package/dist/src/cli/instances.js.map +1 -0
  17. package/dist/src/cli/list.d.ts +2 -0
  18. package/dist/src/cli/list.d.ts.map +1 -0
  19. package/dist/src/cli/list.js +38 -0
  20. package/dist/src/cli/list.js.map +1 -0
  21. package/dist/src/cli/migrate.d.ts +5 -0
  22. package/dist/src/cli/migrate.d.ts.map +1 -0
  23. package/dist/src/cli/migrate.js +203 -0
  24. package/dist/src/cli/migrate.js.map +1 -0
  25. package/dist/src/cli/nuke.d.ts +2 -0
  26. package/dist/src/cli/nuke.d.ts.map +1 -0
  27. package/dist/src/cli/nuke.js +91 -0
  28. package/dist/src/cli/nuke.js.map +1 -0
  29. package/dist/src/cli/remove.d.ts +4 -0
  30. package/dist/src/cli/remove.d.ts.map +1 -0
  31. package/dist/src/cli/remove.js +47 -0
  32. package/dist/src/cli/remove.js.map +1 -0
  33. package/dist/src/cli/setup.d.ts +4 -0
  34. package/dist/src/cli/setup.d.ts.map +1 -0
  35. package/dist/src/cli/setup.js +483 -0
  36. package/dist/src/cli/setup.js.map +1 -0
  37. package/dist/src/cli/skills.d.ts +28 -0
  38. package/dist/src/cli/skills.d.ts.map +1 -0
  39. package/dist/src/cli/skills.js +284 -0
  40. package/dist/src/cli/skills.js.map +1 -0
  41. package/dist/src/cli/start.d.ts +5 -0
  42. package/dist/src/cli/start.d.ts.map +1 -0
  43. package/dist/src/cli/start.js +34 -0
  44. package/dist/src/cli/start.js.map +1 -0
  45. package/dist/src/cli/status.d.ts +2 -0
  46. package/dist/src/cli/status.d.ts.map +1 -0
  47. package/dist/src/cli/status.js +60 -0
  48. package/dist/src/cli/status.js.map +1 -0
  49. package/dist/src/cli/stop.d.ts +2 -0
  50. package/dist/src/cli/stop.d.ts.map +1 -0
  51. package/dist/src/cli/stop.js +11 -0
  52. package/dist/src/cli/stop.js.map +1 -0
  53. package/dist/src/connectors/slack/format.d.ts +10 -0
  54. package/dist/src/connectors/slack/format.d.ts.map +1 -0
  55. package/dist/src/connectors/slack/format.js +55 -0
  56. package/dist/src/connectors/slack/format.js.map +1 -0
  57. package/dist/src/connectors/slack/index.d.ts +18 -0
  58. package/dist/src/connectors/slack/index.d.ts.map +1 -0
  59. package/dist/src/connectors/slack/index.js +122 -0
  60. package/dist/src/connectors/slack/index.js.map +1 -0
  61. package/dist/src/connectors/slack/threads.d.ts +2 -0
  62. package/dist/src/connectors/slack/threads.d.ts.map +1 -0
  63. package/dist/src/connectors/slack/threads.js +10 -0
  64. package/dist/src/connectors/slack/threads.js.map +1 -0
  65. package/dist/src/cron/jobs.d.ts +5 -0
  66. package/dist/src/cron/jobs.d.ts.map +1 -0
  67. package/dist/src/cron/jobs.js +23 -0
  68. package/dist/src/cron/jobs.js.map +1 -0
  69. package/dist/src/cron/runner.d.ts +3 -0
  70. package/dist/src/cron/runner.d.ts.map +1 -0
  71. package/dist/src/cron/runner.js +118 -0
  72. package/dist/src/cron/runner.js.map +1 -0
  73. package/dist/src/cron/scheduler.d.ts +5 -0
  74. package/dist/src/cron/scheduler.d.ts.map +1 -0
  75. package/dist/src/cron/scheduler.js +39 -0
  76. package/dist/src/cron/scheduler.js.map +1 -0
  77. package/dist/src/engines/claude.d.ts +14 -0
  78. package/dist/src/engines/claude.d.ts.map +1 -0
  79. package/dist/src/engines/claude.js +264 -0
  80. package/dist/src/engines/claude.js.map +1 -0
  81. package/dist/src/engines/codex.d.ts +15 -0
  82. package/dist/src/engines/codex.d.ts.map +1 -0
  83. package/dist/src/engines/codex.js +346 -0
  84. package/dist/src/engines/codex.js.map +1 -0
  85. package/dist/src/gateway/api.d.ts +13 -0
  86. package/dist/src/gateway/api.d.ts.map +1 -0
  87. package/dist/src/gateway/api.js +819 -0
  88. package/dist/src/gateway/api.js.map +1 -0
  89. package/dist/src/gateway/daemon-entry.d.ts +2 -0
  90. package/dist/src/gateway/daemon-entry.d.ts.map +1 -0
  91. package/dist/src/gateway/daemon-entry.js +12 -0
  92. package/dist/src/gateway/daemon-entry.js.map +1 -0
  93. package/dist/src/gateway/lifecycle.d.ts +10 -0
  94. package/dist/src/gateway/lifecycle.d.ts.map +1 -0
  95. package/dist/src/gateway/lifecycle.js +124 -0
  96. package/dist/src/gateway/lifecycle.js.map +1 -0
  97. package/dist/src/gateway/org.d.ts +10 -0
  98. package/dist/src/gateway/org.d.ts.map +1 -0
  99. package/dist/src/gateway/org.js +71 -0
  100. package/dist/src/gateway/org.js.map +1 -0
  101. package/dist/src/gateway/server.d.ts +4 -0
  102. package/dist/src/gateway/server.d.ts.map +1 -0
  103. package/dist/src/gateway/server.js +301 -0
  104. package/dist/src/gateway/server.js.map +1 -0
  105. package/dist/src/gateway/watcher.d.ts +14 -0
  106. package/dist/src/gateway/watcher.d.ts.map +1 -0
  107. package/dist/src/gateway/watcher.js +104 -0
  108. package/dist/src/gateway/watcher.js.map +1 -0
  109. package/dist/src/index.d.ts +5 -0
  110. package/dist/src/index.d.ts.map +1 -0
  111. package/dist/src/index.js +4 -0
  112. package/dist/src/index.js.map +1 -0
  113. package/dist/src/sessions/context.d.ts +20 -0
  114. package/dist/src/sessions/context.d.ts.map +1 -0
  115. package/dist/src/sessions/context.js +532 -0
  116. package/dist/src/sessions/context.js.map +1 -0
  117. package/dist/src/sessions/manager.d.ts +38 -0
  118. package/dist/src/sessions/manager.d.ts.map +1 -0
  119. package/dist/src/sessions/manager.js +208 -0
  120. package/dist/src/sessions/manager.js.map +1 -0
  121. package/dist/src/sessions/queue.d.ts +14 -0
  122. package/dist/src/sessions/queue.d.ts.map +1 -0
  123. package/dist/src/sessions/queue.js +42 -0
  124. package/dist/src/sessions/queue.js.map +1 -0
  125. package/dist/src/sessions/registry.d.ts +46 -0
  126. package/dist/src/sessions/registry.d.ts.map +1 -0
  127. package/dist/src/sessions/registry.js +193 -0
  128. package/dist/src/sessions/registry.js.map +1 -0
  129. package/dist/src/shared/config.d.ts +3 -0
  130. package/dist/src/shared/config.d.ts.map +1 -0
  131. package/dist/src/shared/config.js +11 -0
  132. package/dist/src/shared/config.js.map +1 -0
  133. package/dist/src/shared/logger.d.ts +12 -0
  134. package/dist/src/shared/logger.d.ts.map +1 -0
  135. package/dist/src/shared/logger.js +35 -0
  136. package/dist/src/shared/logger.js.map +1 -0
  137. package/dist/src/shared/paths.d.ts +19 -0
  138. package/dist/src/shared/paths.d.ts.map +1 -0
  139. package/dist/src/shared/paths.js +31 -0
  140. package/dist/src/shared/paths.js.map +1 -0
  141. package/dist/src/shared/types.d.ts +166 -0
  142. package/dist/src/shared/types.d.ts.map +1 -0
  143. package/dist/src/shared/types.js +4 -0
  144. package/dist/src/shared/types.js.map +1 -0
  145. package/dist/src/shared/version.d.ts +15 -0
  146. package/dist/src/shared/version.d.ts.map +1 -0
  147. package/dist/src/shared/version.js +56 -0
  148. package/dist/src/shared/version.js.map +1 -0
  149. package/dist/web/404.html +1 -0
  150. package/dist/web/_next/static/chunks/198-fd91406a158c5c25.js +1 -0
  151. package/dist/web/_next/static/chunks/517.62389e8d3c929c43.js +1 -0
  152. package/dist/web/_next/static/chunks/534-17c49c944e0d0fe1.js +1 -0
  153. package/dist/web/_next/static/chunks/573-070537ec2452d03e.js +1 -0
  154. package/dist/web/_next/static/chunks/590-2c34156c7417317e.js +1 -0
  155. package/dist/web/_next/static/chunks/704-af2893821e1d18dc.js +1 -0
  156. package/dist/web/_next/static/chunks/7273c211.06e3b6021d90b73f.js +1 -0
  157. package/dist/web/_next/static/chunks/73-c226535579393e21.js +1 -0
  158. package/dist/web/_next/static/chunks/743-5bb03adbb0e4ddec.js +1 -0
  159. package/dist/web/_next/static/chunks/874.97d5a27895061057.js +1 -0
  160. package/dist/web/_next/static/chunks/8e6518bb-c26e82767f1faf66.js +1 -0
  161. package/dist/web/_next/static/chunks/app/_not-found/page-bb075b0779827928.js +1 -0
  162. package/dist/web/_next/static/chunks/app/chat/page-6d5bc707a45c92c6.js +1 -0
  163. package/dist/web/_next/static/chunks/app/costs/page-d6c03718defdb599.js +1 -0
  164. package/dist/web/_next/static/chunks/app/cron/page-4c563eef2b6231fe.js +1 -0
  165. package/dist/web/_next/static/chunks/app/kanban/page-55a73165a36f4077.js +1 -0
  166. package/dist/web/_next/static/chunks/app/layout-5129b67d5f126cf0.js +1 -0
  167. package/dist/web/_next/static/chunks/app/logs/page-e18889d67e48c9c9.js +1 -0
  168. package/dist/web/_next/static/chunks/app/org/page-d5cd8d9b7864737b.js +1 -0
  169. package/dist/web/_next/static/chunks/app/page-b81992940fd1dbc6.js +1 -0
  170. package/dist/web/_next/static/chunks/app/sessions/page-2eef6ac7882a28ba.js +1 -0
  171. package/dist/web/_next/static/chunks/app/settings/page-4fb01b9b09500170.js +1 -0
  172. package/dist/web/_next/static/chunks/app/skills/page-df9465e314561bb5.js +1 -0
  173. package/dist/web/_next/static/chunks/framework-077b27ad7787463c.js +1 -0
  174. package/dist/web/_next/static/chunks/main-app-437f51faf74fbb3b.js +1 -0
  175. package/dist/web/_next/static/chunks/main-f1c74cefd4965abf.js +1 -0
  176. package/dist/web/_next/static/chunks/pages/_app-77a85fe7d6bca671.js +1 -0
  177. package/dist/web/_next/static/chunks/pages/_error-68febf4b34900064.js +1 -0
  178. package/dist/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  179. package/dist/web/_next/static/chunks/webpack-0f39b7e91dce9791.js +1 -0
  180. package/dist/web/_next/static/css/4a6a5bca9238c104.css +1 -0
  181. package/dist/web/_next/static/vLvOwhC8JocJzSHTHKKOv/_buildManifest.js +1 -0
  182. package/dist/web/_next/static/vLvOwhC8JocJzSHTHKKOv/_ssgManifest.js +1 -0
  183. package/dist/web/chat.html +1 -0
  184. package/dist/web/chat.txt +20 -0
  185. package/dist/web/costs.html +16 -0
  186. package/dist/web/costs.txt +20 -0
  187. package/dist/web/cron.html +1 -0
  188. package/dist/web/cron.txt +20 -0
  189. package/dist/web/index.html +1 -0
  190. package/dist/web/index.txt +20 -0
  191. package/dist/web/kanban.html +1 -0
  192. package/dist/web/kanban.txt +20 -0
  193. package/dist/web/logs.html +7 -0
  194. package/dist/web/logs.txt +20 -0
  195. package/dist/web/org.html +1 -0
  196. package/dist/web/org.txt +20 -0
  197. package/dist/web/sessions.html +1 -0
  198. package/dist/web/sessions.txt +20 -0
  199. package/dist/web/settings.html +1 -0
  200. package/dist/web/settings.txt +20 -0
  201. package/dist/web/skills.html +1 -0
  202. package/dist/web/skills.txt +20 -0
  203. package/package.json +43 -0
  204. package/template/AGENTS.md +167 -0
  205. package/template/CLAUDE.md +106 -0
  206. package/template/config.default.yaml +27 -0
  207. package/template/docs/architecture.md +74 -0
  208. package/template/docs/connectors.md +72 -0
  209. package/template/docs/cron.md +137 -0
  210. package/template/docs/org.md +105 -0
  211. package/template/docs/overview.md +39 -0
  212. package/template/docs/self-modification.md +65 -0
  213. package/template/docs/skills.md +58 -0
  214. package/template/knowledge/.gitkeep +0 -0
  215. package/template/migrations/.gitkeep +0 -0
  216. package/template/migrations/0.1.0/MIGRATION.md +25 -0
  217. package/template/skills/cron-manager/SKILL.md +127 -0
  218. package/template/skills/find-and-install/SKILL.md +92 -0
  219. package/template/skills/management/SKILL.md +203 -0
  220. package/template/skills/migrate/SKILL.md +154 -0
  221. package/template/skills/new/SKILL.md +19 -0
  222. package/template/skills/onboarding/SKILL.md +106 -0
  223. package/template/skills/self-heal/SKILL.md +114 -0
  224. package/template/skills/skill-creator/SKILL.md +112 -0
  225. package/template/skills/status/SKILL.md +19 -0
  226. package/template/skills/sync/SKILL.md +67 -0
  227. package/template/skills.json +3 -0
@@ -0,0 +1,819 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+ import { isInterruptibleEngine } from "../shared/types.js";
5
+ import { buildContext } from "../sessions/context.js";
6
+ import { listSessions, getSession, createSession, updateSession, deleteSession, insertMessage, getMessages, } from "../sessions/registry.js";
7
+ import { CONFIG_PATH, CRON_JOBS, CRON_RUNS, ORG_DIR, SKILLS_DIR, LOGS_DIR, } from "../shared/paths.js";
8
+ import { logger } from "../shared/logger.js";
9
+ import { JINN_HOME } from "../shared/paths.js";
10
+ function dispatchWebSessionRun(session, prompt, engine, config, context, opts) {
11
+ const run = async () => {
12
+ await context.sessionManager.getQueue().enqueue(session.sourceRef, async () => {
13
+ context.emit("session:started", { sessionId: session.id });
14
+ await runWebSession(session, prompt, engine, config, context);
15
+ });
16
+ };
17
+ const launch = () => {
18
+ run().catch((err) => {
19
+ const errMsg = err instanceof Error ? err.message : String(err);
20
+ logger.error(`Web session ${session.id} dispatch error: ${errMsg}`);
21
+ updateSession(session.id, {
22
+ status: "error",
23
+ lastActivity: new Date().toISOString(),
24
+ lastError: errMsg,
25
+ });
26
+ context.emit("session:completed", {
27
+ sessionId: session.id,
28
+ result: null,
29
+ error: errMsg,
30
+ });
31
+ });
32
+ };
33
+ if (opts?.delayMs && opts.delayMs > 0) {
34
+ setTimeout(launch, opts.delayMs);
35
+ }
36
+ else {
37
+ launch();
38
+ }
39
+ }
40
+ function readBody(req) {
41
+ return new Promise((resolve, reject) => {
42
+ const chunks = [];
43
+ req.on("data", (chunk) => chunks.push(chunk));
44
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
45
+ req.on("error", reject);
46
+ });
47
+ }
48
+ function json(res, data, status = 200) {
49
+ res.writeHead(status, { "Content-Type": "application/json" });
50
+ res.end(JSON.stringify(data));
51
+ }
52
+ function notFound(res) {
53
+ json(res, { error: "Not found" }, 404);
54
+ }
55
+ function badRequest(res, message) {
56
+ json(res, { error: message }, 400);
57
+ }
58
+ function serverError(res, message) {
59
+ json(res, { error: message }, 500);
60
+ }
61
+ function matchRoute(pattern, pathname) {
62
+ const patternParts = pattern.split("/");
63
+ const pathParts = pathname.split("/");
64
+ if (patternParts.length !== pathParts.length)
65
+ return null;
66
+ const params = {};
67
+ for (let i = 0; i < patternParts.length; i++) {
68
+ if (patternParts[i].startsWith(":")) {
69
+ params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
70
+ }
71
+ else if (patternParts[i] !== pathParts[i]) {
72
+ return null;
73
+ }
74
+ }
75
+ return params;
76
+ }
77
+ export async function handleApiRequest(req, res, context) {
78
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
79
+ const pathname = url.pathname;
80
+ const method = req.method || "GET";
81
+ try {
82
+ // GET /api/status
83
+ if (method === "GET" && pathname === "/api/status") {
84
+ const config = context.getConfig();
85
+ const sessions = listSessions();
86
+ const running = sessions.filter((s) => s.status === "running").length;
87
+ return json(res, {
88
+ status: "ok",
89
+ uptime: Math.floor((Date.now() - context.startTime) / 1000),
90
+ port: config.gateway.port || 7777,
91
+ engines: {
92
+ default: config.engines.default,
93
+ claude: { model: config.engines.claude.model, available: true },
94
+ codex: { model: config.engines.codex.model, available: true },
95
+ },
96
+ sessions: { total: sessions.length, running, active: running },
97
+ });
98
+ }
99
+ // GET /api/sessions
100
+ if (method === "GET" && pathname === "/api/sessions") {
101
+ const sessions = listSessions();
102
+ return json(res, sessions);
103
+ }
104
+ // GET /api/sessions/:id
105
+ let params = matchRoute("/api/sessions/:id", pathname);
106
+ if (method === "GET" && params) {
107
+ const session = getSession(params.id);
108
+ if (!session)
109
+ return notFound(res);
110
+ let messages = getMessages(params.id);
111
+ // Backfill from Claude Code's JSONL transcript if our DB has no messages
112
+ if (messages.length === 0 && session.engineSessionId) {
113
+ const transcriptMessages = loadTranscriptMessages(session.engineSessionId);
114
+ if (transcriptMessages.length > 0) {
115
+ for (const tm of transcriptMessages) {
116
+ insertMessage(params.id, tm.role, tm.content);
117
+ }
118
+ messages = getMessages(params.id);
119
+ }
120
+ }
121
+ return json(res, { ...session, messages });
122
+ }
123
+ // DELETE /api/sessions/:id
124
+ if (method === "DELETE" && params) {
125
+ const session = getSession(params.id);
126
+ if (!session)
127
+ return notFound(res);
128
+ // Kill any live engine process for this session before deleting it.
129
+ const engine = context.sessionManager.getEngine(session.engine);
130
+ if (engine && isInterruptibleEngine(engine) && engine.isAlive(params.id)) {
131
+ logger.info(`Killing live engine process for deleted session ${params.id}`);
132
+ engine.kill(params.id);
133
+ }
134
+ const deleted = deleteSession(params.id);
135
+ if (!deleted)
136
+ return notFound(res);
137
+ logger.info(`Session deleted: ${params.id}`);
138
+ context.emit("session:deleted", { sessionId: params.id });
139
+ return json(res, { status: "deleted" });
140
+ }
141
+ // GET /api/sessions/:id/children
142
+ params = matchRoute("/api/sessions/:id/children", pathname);
143
+ if (method === "GET" && params) {
144
+ const children = listSessions().filter((s) => s.parentSessionId === params.id);
145
+ return json(res, children);
146
+ }
147
+ // POST /api/sessions/stub — create a session with a pre-populated assistant
148
+ // message but do NOT run the engine. Used for lazy onboarding.
149
+ if (method === "POST" && pathname === "/api/sessions/stub") {
150
+ const body = JSON.parse(await readBody(req));
151
+ const greeting = body.greeting || "Hey! Say hi when you're ready to get started.";
152
+ const config = context.getConfig();
153
+ const engineName = body.engine || config.engines.default;
154
+ const session = createSession({
155
+ engine: engineName,
156
+ source: "web",
157
+ sourceRef: `web:${Date.now()}`,
158
+ employee: body.employee,
159
+ title: body.title,
160
+ portalName: config.portal?.portalName,
161
+ });
162
+ insertMessage(session.id, "assistant", greeting);
163
+ logger.info(`Stub session created: ${session.id}`);
164
+ return json(res, session, 201);
165
+ }
166
+ // POST /api/sessions
167
+ if (method === "POST" && pathname === "/api/sessions") {
168
+ const body = JSON.parse(await readBody(req));
169
+ const prompt = body.prompt || body.message;
170
+ if (!prompt)
171
+ return badRequest(res, "prompt or message is required");
172
+ const config = context.getConfig();
173
+ const engineName = body.engine || config.engines.default;
174
+ const session = createSession({
175
+ engine: engineName,
176
+ source: "web",
177
+ sourceRef: `web:${Date.now()}`,
178
+ employee: body.employee,
179
+ parentSessionId: body.parentSessionId,
180
+ prompt,
181
+ portalName: config.portal?.portalName,
182
+ });
183
+ logger.info(`Web session created: ${session.id}`);
184
+ insertMessage(session.id, "user", prompt);
185
+ // Run engine asynchronously — respond immediately, push result via WebSocket
186
+ const engine = context.sessionManager.getEngine(engineName);
187
+ if (!engine) {
188
+ updateSession(session.id, {
189
+ status: "error",
190
+ lastError: `Engine "${engineName}" not available`,
191
+ });
192
+ return json(res, { ...session, status: "error", lastError: `Engine "${engineName}" not available` }, 201);
193
+ }
194
+ // Set status to "running" synchronously BEFORE returning the response.
195
+ // This prevents a race condition where the caller polls immediately and
196
+ // sees "idle" status before runWebSession has a chance to set "running".
197
+ updateSession(session.id, {
198
+ status: "running",
199
+ lastActivity: new Date().toISOString(),
200
+ });
201
+ session.status = "running";
202
+ dispatchWebSessionRun(session, prompt, engine, config, context);
203
+ return json(res, session, 201);
204
+ }
205
+ // POST /api/sessions/:id/message
206
+ params = matchRoute("/api/sessions/:id/message", pathname);
207
+ if (method === "POST" && params) {
208
+ const session = getSession(params.id);
209
+ if (!session)
210
+ return notFound(res);
211
+ const body = JSON.parse(await readBody(req));
212
+ const prompt = body.message || body.prompt;
213
+ if (!prompt)
214
+ return badRequest(res, "message is required");
215
+ const config = context.getConfig();
216
+ const engine = context.sessionManager.getEngine(session.engine);
217
+ if (!engine)
218
+ return serverError(res, `Engine "${session.engine}" not available`);
219
+ // Persist the user message immediately
220
+ insertMessage(session.id, "user", prompt);
221
+ // If a turn is already running, this follow-up will be queued and resume later.
222
+ if (session.status === "running") {
223
+ context.emit("session:queued", { sessionId: session.id, message: prompt });
224
+ }
225
+ dispatchWebSessionRun(session, prompt, engine, config, context);
226
+ return json(res, { status: "queued", sessionId: session.id });
227
+ }
228
+ // GET /api/cron
229
+ if (method === "GET" && pathname === "/api/cron") {
230
+ if (!fs.existsSync(CRON_JOBS))
231
+ return json(res, []);
232
+ const jobs = JSON.parse(fs.readFileSync(CRON_JOBS, "utf-8"));
233
+ return json(res, jobs);
234
+ }
235
+ // GET /api/cron/:id/runs
236
+ params = matchRoute("/api/cron/:id/runs", pathname);
237
+ if (method === "GET" && params) {
238
+ const runFile = path.join(CRON_RUNS, `${params.id}.jsonl`);
239
+ if (!fs.existsSync(runFile))
240
+ return json(res, []);
241
+ const lines = fs
242
+ .readFileSync(runFile, "utf-8")
243
+ .trim()
244
+ .split("\n")
245
+ .filter(Boolean)
246
+ .map((l) => JSON.parse(l));
247
+ return json(res, lines);
248
+ }
249
+ // PUT /api/cron/:id
250
+ params = matchRoute("/api/cron/:id", pathname);
251
+ if (method === "PUT" && params) {
252
+ if (!fs.existsSync(CRON_JOBS))
253
+ return notFound(res);
254
+ const jobs = JSON.parse(fs.readFileSync(CRON_JOBS, "utf-8"));
255
+ const idx = jobs.findIndex((j) => j.id === params.id);
256
+ if (idx === -1)
257
+ return notFound(res);
258
+ const body = JSON.parse(await readBody(req));
259
+ jobs[idx] = { ...jobs[idx], ...body, id: params.id };
260
+ fs.writeFileSync(CRON_JOBS, JSON.stringify(jobs, null, 2));
261
+ return json(res, jobs[idx]);
262
+ }
263
+ // GET /api/org
264
+ if (method === "GET" && pathname === "/api/org") {
265
+ if (!fs.existsSync(ORG_DIR))
266
+ return json(res, { departments: [], employees: [] });
267
+ const entries = fs.readdirSync(ORG_DIR, { withFileTypes: true });
268
+ const departments = entries
269
+ .filter((e) => e.isDirectory())
270
+ .map((e) => e.name);
271
+ const employees = [];
272
+ // Scan root-level YAML files
273
+ for (const e of entries) {
274
+ if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))) {
275
+ employees.push(e.name.replace(/\.ya?ml$/, ""));
276
+ }
277
+ }
278
+ // Scan employees/ subdirectory
279
+ const employeesDir = path.join(ORG_DIR, "employees");
280
+ if (fs.existsSync(employeesDir)) {
281
+ const empEntries = fs.readdirSync(employeesDir, { withFileTypes: true });
282
+ for (const e of empEntries) {
283
+ if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))) {
284
+ employees.push(e.name.replace(/\.ya?ml$/, ""));
285
+ }
286
+ }
287
+ }
288
+ // Scan inside each department directory for YAML files (excluding department.yaml)
289
+ for (const dept of departments) {
290
+ const deptDir = path.join(ORG_DIR, dept);
291
+ const deptEntries = fs.readdirSync(deptDir, { withFileTypes: true });
292
+ for (const e of deptEntries) {
293
+ if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml")) && e.name !== "department.yaml") {
294
+ employees.push(e.name.replace(/\.ya?ml$/, ""));
295
+ }
296
+ }
297
+ }
298
+ return json(res, { departments, employees });
299
+ }
300
+ // GET /api/org/employees/:name
301
+ params = matchRoute("/api/org/employees/:name", pathname);
302
+ if (method === "GET" && params) {
303
+ const candidates = [
304
+ path.join(ORG_DIR, "employees", `${params.name}.yaml`),
305
+ path.join(ORG_DIR, "employees", `${params.name}.yml`),
306
+ path.join(ORG_DIR, `${params.name}.yaml`),
307
+ path.join(ORG_DIR, `${params.name}.yml`),
308
+ ];
309
+ // Also search inside each department directory
310
+ if (fs.existsSync(ORG_DIR)) {
311
+ const dirs = fs.readdirSync(ORG_DIR, { withFileTypes: true }).filter((e) => e.isDirectory());
312
+ for (const dir of dirs) {
313
+ candidates.push(path.join(ORG_DIR, dir.name, `${params.name}.yaml`));
314
+ candidates.push(path.join(ORG_DIR, dir.name, `${params.name}.yml`));
315
+ }
316
+ }
317
+ const filePath = candidates.find((c) => fs.existsSync(c));
318
+ if (!filePath)
319
+ return notFound(res);
320
+ const content = yaml.load(fs.readFileSync(filePath, "utf-8"));
321
+ return json(res, content);
322
+ }
323
+ // GET /api/org/departments/:name/board
324
+ params = matchRoute("/api/org/departments/:name/board", pathname);
325
+ if (method === "GET" && params) {
326
+ const boardPath = path.join(ORG_DIR, params.name, "board.json");
327
+ if (!fs.existsSync(boardPath))
328
+ return notFound(res);
329
+ const board = JSON.parse(fs.readFileSync(boardPath, "utf-8"));
330
+ return json(res, board);
331
+ }
332
+ // PUT /api/org/departments/:name/board
333
+ if (method === "PUT" && matchRoute("/api/org/departments/:name/board", pathname)) {
334
+ const p = matchRoute("/api/org/departments/:name/board", pathname);
335
+ const boardPath = path.join(ORG_DIR, p.name, "board.json");
336
+ const deptDir = path.join(ORG_DIR, p.name);
337
+ if (!fs.existsSync(deptDir))
338
+ return notFound(res);
339
+ const body = JSON.parse(await readBody(req));
340
+ fs.writeFileSync(boardPath, JSON.stringify(body, null, 2));
341
+ context.emit("board:updated", { department: p.name });
342
+ return json(res, { status: "ok" });
343
+ }
344
+ // GET /api/skills/search?q=<query> — search the skills.sh registry
345
+ if (method === "GET" && pathname === "/api/skills/search") {
346
+ const query = url.searchParams.get("q") || "";
347
+ if (!query)
348
+ return badRequest(res, "q parameter is required");
349
+ try {
350
+ const { execSync } = await import("node:child_process");
351
+ const output = execSync(`npx skills find ${JSON.stringify(query)}`, {
352
+ encoding: "utf-8",
353
+ timeout: 30000,
354
+ });
355
+ const results = parseSkillsSearchOutput(output);
356
+ return json(res, results);
357
+ }
358
+ catch (err) {
359
+ const msg = err instanceof Error ? err.stderr || err.message : String(err);
360
+ return json(res, { results: [], error: msg });
361
+ }
362
+ }
363
+ // GET /api/skills/manifest — return skills.json contents
364
+ if (method === "GET" && pathname === "/api/skills/manifest") {
365
+ const { readManifest } = await import("../cli/skills.js");
366
+ return json(res, readManifest());
367
+ }
368
+ // POST /api/skills/install — install a skill from skills.sh
369
+ if (method === "POST" && pathname === "/api/skills/install") {
370
+ const body = JSON.parse(await readBody(req));
371
+ const source = body.source;
372
+ if (!source)
373
+ return badRequest(res, "source is required");
374
+ try {
375
+ const { snapshotDirs, diffSnapshots, copySkillToInstance, upsertManifest, extractSkillName, findExistingSkill, } = await import("../cli/skills.js");
376
+ const { execSync } = await import("node:child_process");
377
+ const before = snapshotDirs();
378
+ execSync(`npx skills add ${JSON.stringify(source)} -g -y`, {
379
+ encoding: "utf-8",
380
+ timeout: 60000,
381
+ });
382
+ const after = snapshotDirs();
383
+ const newDirs = diffSnapshots(before, after);
384
+ let skillName;
385
+ if (newDirs.length > 0) {
386
+ const installed = newDirs[0];
387
+ skillName = installed.name;
388
+ copySkillToInstance(installed.name, path.join(installed.dir, installed.name));
389
+ }
390
+ else {
391
+ skillName = extractSkillName(source);
392
+ const existing = findExistingSkill(skillName);
393
+ if (existing) {
394
+ copySkillToInstance(existing.name, existing.dir);
395
+ }
396
+ else {
397
+ return serverError(res, "Skill installed globally but could not locate the directory");
398
+ }
399
+ }
400
+ upsertManifest(skillName, source);
401
+ return json(res, { status: "installed", name: skillName });
402
+ }
403
+ catch (err) {
404
+ const msg = err instanceof Error ? err.message : String(err);
405
+ return serverError(res, msg);
406
+ }
407
+ }
408
+ // GET /api/skills
409
+ if (method === "GET" && pathname === "/api/skills") {
410
+ if (!fs.existsSync(SKILLS_DIR))
411
+ return json(res, []);
412
+ const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
413
+ const skills = entries.filter((e) => e.isDirectory()).map((e) => {
414
+ const skillMdPath = path.join(SKILLS_DIR, e.name, "SKILL.md");
415
+ let description = "";
416
+ if (fs.existsSync(skillMdPath)) {
417
+ const content = fs.readFileSync(skillMdPath, "utf-8");
418
+ // Extract description from YAML frontmatter, ## Trigger section, or first paragraph
419
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
420
+ if (frontmatterMatch) {
421
+ const descMatch = frontmatterMatch[1].match(/^description:\s*(.+)$/m);
422
+ if (descMatch) {
423
+ description = descMatch[1].trim();
424
+ }
425
+ }
426
+ if (!description) {
427
+ const triggerMatch = content.match(/##\s*Trigger\s*\n+([^\n#]+)/);
428
+ if (triggerMatch) {
429
+ description = triggerMatch[1].trim();
430
+ }
431
+ else {
432
+ // Use first non-heading, non-empty, non-frontmatter line
433
+ const bodyContent = frontmatterMatch ? content.slice(frontmatterMatch[0].length) : content;
434
+ const lines = bodyContent.split("\n");
435
+ for (const line of lines) {
436
+ const trimmed = line.trim();
437
+ if (trimmed && !trimmed.startsWith("#")) {
438
+ description = trimmed;
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ return { name: e.name, description };
446
+ });
447
+ return json(res, skills);
448
+ }
449
+ // GET /api/skills/:name
450
+ params = matchRoute("/api/skills/:name", pathname);
451
+ if (method === "GET" && params) {
452
+ const skillMd = path.join(SKILLS_DIR, params.name, "SKILL.md");
453
+ if (!fs.existsSync(skillMd))
454
+ return notFound(res);
455
+ const content = fs.readFileSync(skillMd, "utf-8");
456
+ return json(res, { name: params.name, content });
457
+ }
458
+ // DELETE /api/skills/:name — remove a skill
459
+ if (method === "DELETE" && params) {
460
+ const skillDir = path.join(SKILLS_DIR, params.name);
461
+ if (!fs.existsSync(skillDir))
462
+ return notFound(res);
463
+ fs.rmSync(skillDir, { recursive: true, force: true });
464
+ const { removeFromManifest } = await import("../cli/skills.js");
465
+ removeFromManifest(params.name);
466
+ logger.info(`Skill removed via API: ${params.name}`);
467
+ return json(res, { status: "removed", name: params.name });
468
+ }
469
+ // GET /api/config
470
+ if (method === "GET" && pathname === "/api/config") {
471
+ const config = context.getConfig();
472
+ // Sanitize: remove any secrets/tokens from connectors
473
+ const sanitized = {
474
+ ...config,
475
+ connectors: Object.fromEntries(Object.entries(config.connectors || {}).map(([k, v]) => [
476
+ k,
477
+ { ...v, token: v?.token ? "***" : undefined, signingSecret: v?.signingSecret ? "***" : undefined },
478
+ ])),
479
+ };
480
+ return json(res, sanitized);
481
+ }
482
+ // PUT /api/config
483
+ if (method === "PUT" && pathname === "/api/config") {
484
+ const body = JSON.parse(await readBody(req));
485
+ const yamlStr = yaml.dump(body);
486
+ fs.writeFileSync(CONFIG_PATH, yamlStr);
487
+ logger.info("Config updated via API");
488
+ return json(res, { status: "ok" });
489
+ }
490
+ // GET /api/logs
491
+ if (method === "GET" && pathname === "/api/logs") {
492
+ const logFile = path.join(LOGS_DIR, "gateway.log");
493
+ if (!fs.existsSync(logFile))
494
+ return json(res, { lines: [] });
495
+ const n = parseInt(url.searchParams.get("n") || "100", 10);
496
+ const content = fs.readFileSync(logFile, "utf-8");
497
+ const allLines = content.trim().split("\n");
498
+ const lines = allLines.slice(-n);
499
+ return json(res, { lines });
500
+ }
501
+ // POST /api/connectors/:name/send — send a message via a connector
502
+ params = matchRoute("/api/connectors/:name/send", pathname);
503
+ if (method === "POST" && params) {
504
+ const connector = context.connectors.get(params.name);
505
+ if (!connector)
506
+ return notFound(res);
507
+ const body = JSON.parse(await readBody(req));
508
+ if (!body.channel || !body.text)
509
+ return badRequest(res, "channel and text are required");
510
+ await connector.sendMessage({ channel: body.channel, thread: body.thread }, body.text);
511
+ return json(res, { status: "sent" });
512
+ }
513
+ // GET /api/connectors — list available connectors
514
+ if (method === "GET" && pathname === "/api/connectors") {
515
+ const names = Array.from(context.connectors.keys());
516
+ return json(res, names);
517
+ }
518
+ // GET /api/activity — recent activity derived from sessions
519
+ if (method === "GET" && pathname === "/api/activity") {
520
+ const sessions = listSessions();
521
+ const events = [];
522
+ for (const s of sessions) {
523
+ const ts = new Date(s.lastActivity || s.createdAt).getTime();
524
+ if (s.status === "running") {
525
+ events.push({ event: "session:started", payload: { sessionId: s.id, employee: s.employee, engine: s.engine }, ts });
526
+ }
527
+ else if (s.status === "idle") {
528
+ events.push({ event: "session:completed", payload: { sessionId: s.id, employee: s.employee, engine: s.engine }, ts });
529
+ }
530
+ else if (s.status === "error") {
531
+ events.push({ event: "session:error", payload: { sessionId: s.id, employee: s.employee, error: s.lastError }, ts });
532
+ }
533
+ }
534
+ events.sort((a, b) => b.ts - a.ts);
535
+ return json(res, events.slice(0, 30));
536
+ }
537
+ // GET /api/onboarding — check if onboarding is needed
538
+ if (method === "GET" && pathname === "/api/onboarding") {
539
+ const sessions = listSessions();
540
+ const hasEmployees = fs.existsSync(ORG_DIR) &&
541
+ fs.readdirSync(ORG_DIR, { recursive: true }).some((f) => String(f).endsWith(".yaml") && !String(f).endsWith("department.yaml"));
542
+ const config = context.getConfig();
543
+ return json(res, {
544
+ needed: sessions.length === 0 && !hasEmployees,
545
+ sessionsCount: sessions.length,
546
+ hasEmployees,
547
+ portalName: config.portal?.portalName ?? null,
548
+ operatorName: config.portal?.operatorName ?? null,
549
+ });
550
+ }
551
+ // POST /api/onboarding — persist portal personalization
552
+ if (method === "POST" && pathname === "/api/onboarding") {
553
+ const body = JSON.parse(await readBody(req));
554
+ const { portalName, operatorName, language } = body;
555
+ // Read current config and merge portal settings
556
+ const config = context.getConfig();
557
+ const updated = {
558
+ ...config,
559
+ portal: {
560
+ ...config.portal,
561
+ ...(portalName !== undefined && { portalName: portalName || undefined }),
562
+ ...(operatorName !== undefined && { operatorName: operatorName || undefined }),
563
+ ...(language !== undefined && { language: language || undefined }),
564
+ },
565
+ };
566
+ // Write updated config
567
+ const yamlStr = yaml.dump(updated, { lineWidth: -1 });
568
+ fs.writeFileSync(CONFIG_PATH, yamlStr);
569
+ logger.info(`Onboarding: portal name="${portalName}", operator="${operatorName}", language="${language}"`);
570
+ const effectiveName = portalName || "Jinn";
571
+ const languageSection = language && language !== "English"
572
+ ? `\n\n## Language\nAlways respond in ${language}. All communication with the user must be in ${language}.`
573
+ : "";
574
+ // Update CLAUDE.md with personalized COO name and language
575
+ const claudeMdPath = path.join(JINN_HOME, "CLAUDE.md");
576
+ if (fs.existsSync(claudeMdPath)) {
577
+ let claudeMd = fs.readFileSync(claudeMdPath, "utf-8");
578
+ // Replace the identity line in CLAUDE.md
579
+ claudeMd = claudeMd.replace(/^You are \w+, the COO of the user's AI organization\.$/m, `You are ${effectiveName}, the COO of the user's AI organization.`);
580
+ // Remove existing language section if present, then add new one if needed
581
+ claudeMd = claudeMd.replace(/\n\n## Language\nAlways respond in .+\. All communication with the user must be in .+\./m, "");
582
+ if (languageSection) {
583
+ claudeMd = claudeMd.trimEnd() + languageSection + "\n";
584
+ }
585
+ fs.writeFileSync(claudeMdPath, claudeMd);
586
+ }
587
+ // Update AGENTS.md with personalized name and language
588
+ const agentsMdPath = path.join(JINN_HOME, "AGENTS.md");
589
+ if (fs.existsSync(agentsMdPath)) {
590
+ let agentsMd = fs.readFileSync(agentsMdPath, "utf-8");
591
+ // Replace the bold identity line (e.g. "You are **Jinn**")
592
+ agentsMd = agentsMd.replace(/You are \*\*\w+\*\*/, `You are **${effectiveName}**`);
593
+ // Remove existing language section if present, then add new one if needed
594
+ agentsMd = agentsMd.replace(/\n\n## Language\nAlways respond in .+\. All communication with the user must be in .+\./m, "");
595
+ if (languageSection) {
596
+ agentsMd = agentsMd.trimEnd() + languageSection + "\n";
597
+ }
598
+ fs.writeFileSync(agentsMdPath, agentsMd);
599
+ }
600
+ context.emit("config:updated", { portal: updated.portal });
601
+ return json(res, { status: "ok", portal: updated.portal });
602
+ }
603
+ return notFound(res);
604
+ }
605
+ catch (err) {
606
+ const msg = err instanceof Error ? err.message : String(err);
607
+ logger.error(`API error: ${msg}`);
608
+ return serverError(res, msg);
609
+ }
610
+ }
611
+ /**
612
+ * Parse the output of `npx skills find <query>` into structured results.
613
+ *
614
+ * Format:
615
+ * ```
616
+ * owner/repo@skill-name <N> installs
617
+ * └ https://skills.sh/owner/repo/skill-name
618
+ * ```
619
+ */
620
+ function stripAnsi(str) {
621
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
622
+ }
623
+ function parseSkillsSearchOutput(output) {
624
+ const results = [];
625
+ const lines = output.trim().split("\n");
626
+ for (let i = 0; i < lines.length; i++) {
627
+ const headerLine = stripAnsi(lines[i]).trim();
628
+ // Match "owner/repo@skill-name <N> installs"
629
+ const headerMatch = headerLine.match(/^(\S+)\s+(\d+)\s+installs?$/);
630
+ if (!headerMatch)
631
+ continue;
632
+ const source = headerMatch[1];
633
+ const installs = parseInt(headerMatch[2], 10);
634
+ const atIdx = source.lastIndexOf("@");
635
+ const name = atIdx > 0 ? source.slice(atIdx + 1) : source;
636
+ // Next line should be the URL
637
+ let url = "";
638
+ if (i + 1 < lines.length) {
639
+ const urlLine = stripAnsi(lines[i + 1]).trim();
640
+ const urlMatch = urlLine.match(/[└]\s*(https?:\/\/\S+)/);
641
+ if (urlMatch) {
642
+ url = urlMatch[1];
643
+ i++; // consume the URL line
644
+ }
645
+ }
646
+ results.push({ name, source, url, installs });
647
+ }
648
+ return results;
649
+ }
650
+ /**
651
+ * Load messages from a Claude Code JSONL transcript file.
652
+ * Used as a fallback when the messages DB is empty (pre-existing sessions).
653
+ */
654
+ function loadTranscriptMessages(engineSessionId) {
655
+ // Claude Code stores transcripts in ~/.claude/projects/<project-key>/<sessionId>.jsonl
656
+ const claudeProjectsDir = path.join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "projects");
657
+ if (!fs.existsSync(claudeProjectsDir))
658
+ return [];
659
+ // Search all project dirs for the transcript
660
+ const projectDirs = fs.readdirSync(claudeProjectsDir, { withFileTypes: true });
661
+ for (const dir of projectDirs) {
662
+ if (!dir.isDirectory())
663
+ continue;
664
+ const jsonlPath = path.join(claudeProjectsDir, dir.name, `${engineSessionId}.jsonl`);
665
+ if (!fs.existsSync(jsonlPath))
666
+ continue;
667
+ const messages = [];
668
+ const lines = fs.readFileSync(jsonlPath, "utf-8").trim().split("\n").filter(Boolean);
669
+ for (const line of lines) {
670
+ try {
671
+ const obj = JSON.parse(line);
672
+ const type = obj.type;
673
+ if (type !== "user" && type !== "assistant")
674
+ continue;
675
+ const msg = obj.message;
676
+ if (!msg)
677
+ continue;
678
+ let content = msg.content;
679
+ if (Array.isArray(content)) {
680
+ content = content
681
+ .filter((b) => b.type === "text")
682
+ .map((b) => b.text)
683
+ .join("");
684
+ }
685
+ if (typeof content === "string" && content.trim()) {
686
+ messages.push({ role: type, content: content.trim() });
687
+ }
688
+ }
689
+ catch {
690
+ continue;
691
+ }
692
+ }
693
+ return messages;
694
+ }
695
+ return [];
696
+ }
697
+ async function runWebSession(session, prompt, engine, config, context) {
698
+ const currentSession = getSession(session.id);
699
+ if (!currentSession) {
700
+ logger.info(`Skipping deleted web session ${session.id} before run start`);
701
+ return;
702
+ }
703
+ logger.info(`Web session ${currentSession.id} running engine "${currentSession.engine}" (model: ${currentSession.model || "default"})`);
704
+ // Ensure status is "running" (may already be set by the POST handler)
705
+ const currentStatus = getSession(currentSession.id);
706
+ if (currentStatus && currentStatus.status !== "running") {
707
+ updateSession(currentSession.id, {
708
+ status: "running",
709
+ lastActivity: new Date().toISOString(),
710
+ });
711
+ }
712
+ try {
713
+ // If this session has an assigned employee, load their persona
714
+ let employee;
715
+ if (currentSession.employee) {
716
+ const { findEmployee } = await import("./org.js");
717
+ const { scanOrg } = await import("./org.js");
718
+ const registry = scanOrg();
719
+ employee = findEmployee(currentSession.employee, registry);
720
+ }
721
+ const systemPrompt = buildContext({
722
+ source: "web",
723
+ channel: currentSession.sourceRef,
724
+ user: "web-user",
725
+ employee,
726
+ connectors: Array.from(context.connectors.keys()),
727
+ config,
728
+ sessionId: currentSession.id,
729
+ });
730
+ const engineConfig = currentSession.engine === "codex"
731
+ ? config.engines.codex
732
+ : config.engines.claude;
733
+ let lastHeartbeatAt = 0;
734
+ const runHeartbeat = setInterval(() => {
735
+ updateSession(currentSession.id, {
736
+ status: "running",
737
+ lastActivity: new Date().toISOString(),
738
+ });
739
+ }, 5000);
740
+ const result = await engine.run({
741
+ prompt,
742
+ resumeSessionId: currentSession.engineSessionId ?? undefined,
743
+ systemPrompt,
744
+ cwd: JINN_HOME,
745
+ bin: engineConfig.bin,
746
+ model: currentSession.model ?? engineConfig.model,
747
+ cliFlags: employee?.cliFlags,
748
+ sessionId: currentSession.id,
749
+ onStream: (delta) => {
750
+ const now = Date.now();
751
+ if (now - lastHeartbeatAt >= 2000) {
752
+ lastHeartbeatAt = now;
753
+ updateSession(currentSession.id, {
754
+ status: "running",
755
+ lastActivity: new Date(now).toISOString(),
756
+ });
757
+ }
758
+ try {
759
+ context.emit("session:delta", {
760
+ sessionId: currentSession.id,
761
+ type: delta.type,
762
+ content: delta.content,
763
+ toolName: delta.toolName,
764
+ });
765
+ }
766
+ catch (err) {
767
+ logger.warn(`Failed to emit stream delta for session ${currentSession.id}: ${err instanceof Error ? err.message : err}`);
768
+ }
769
+ },
770
+ }).finally(() => {
771
+ clearInterval(runHeartbeat);
772
+ });
773
+ if (!getSession(currentSession.id)) {
774
+ logger.info(`Skipping completion for deleted web session ${currentSession.id}`);
775
+ return;
776
+ }
777
+ // Persist the assistant response
778
+ if (result.result) {
779
+ insertMessage(currentSession.id, "assistant", result.result);
780
+ }
781
+ updateSession(currentSession.id, {
782
+ engineSessionId: result.sessionId,
783
+ status: result.error ? "error" : "idle",
784
+ lastActivity: new Date().toISOString(),
785
+ lastError: result.error ?? null,
786
+ });
787
+ context.emit("session:completed", {
788
+ sessionId: currentSession.id,
789
+ employee: currentSession.employee || config.portal?.portalName || "Jinn",
790
+ title: currentSession.title,
791
+ result: result.result,
792
+ error: result.error || null,
793
+ cost: result.cost,
794
+ durationMs: result.durationMs,
795
+ });
796
+ logger.info(`Web session ${currentSession.id} completed` +
797
+ (result.durationMs ? ` in ${result.durationMs}ms` : "") +
798
+ (result.cost ? ` ($${result.cost.toFixed(4)})` : ""));
799
+ }
800
+ catch (err) {
801
+ const errMsg = err instanceof Error ? err.message : String(err);
802
+ if (!getSession(currentSession.id)) {
803
+ logger.info(`Skipping error handling for deleted web session ${currentSession.id}: ${errMsg}`);
804
+ return;
805
+ }
806
+ updateSession(currentSession.id, {
807
+ status: "error",
808
+ lastActivity: new Date().toISOString(),
809
+ lastError: errMsg,
810
+ });
811
+ context.emit("session:completed", {
812
+ sessionId: currentSession.id,
813
+ result: null,
814
+ error: errMsg,
815
+ });
816
+ logger.error(`Web session ${currentSession.id} error: ${errMsg}`);
817
+ }
818
+ }
819
+ //# sourceMappingURL=api.js.map