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.
- package/dist/bin/jimmy.d.ts +3 -0
- package/dist/bin/jimmy.d.ts.map +1 -0
- package/dist/bin/jimmy.js +148 -0
- package/dist/bin/jimmy.js.map +1 -0
- package/dist/src/cli/chrome-allow.d.ts +5 -0
- package/dist/src/cli/chrome-allow.d.ts.map +1 -0
- package/dist/src/cli/chrome-allow.js +241 -0
- package/dist/src/cli/chrome-allow.js.map +1 -0
- package/dist/src/cli/create.d.ts +2 -0
- package/dist/src/cli/create.d.ts.map +1 -0
- package/dist/src/cli/create.js +72 -0
- package/dist/src/cli/create.js.map +1 -0
- package/dist/src/cli/instances.d.ts +14 -0
- package/dist/src/cli/instances.d.ts.map +1 -0
- package/dist/src/cli/instances.js +43 -0
- package/dist/src/cli/instances.js.map +1 -0
- package/dist/src/cli/list.d.ts +2 -0
- package/dist/src/cli/list.d.ts.map +1 -0
- package/dist/src/cli/list.js +38 -0
- package/dist/src/cli/list.js.map +1 -0
- package/dist/src/cli/migrate.d.ts +5 -0
- package/dist/src/cli/migrate.d.ts.map +1 -0
- package/dist/src/cli/migrate.js +203 -0
- package/dist/src/cli/migrate.js.map +1 -0
- package/dist/src/cli/nuke.d.ts +2 -0
- package/dist/src/cli/nuke.d.ts.map +1 -0
- package/dist/src/cli/nuke.js +91 -0
- package/dist/src/cli/nuke.js.map +1 -0
- package/dist/src/cli/remove.d.ts +4 -0
- package/dist/src/cli/remove.d.ts.map +1 -0
- package/dist/src/cli/remove.js +47 -0
- package/dist/src/cli/remove.js.map +1 -0
- package/dist/src/cli/setup.d.ts +4 -0
- package/dist/src/cli/setup.d.ts.map +1 -0
- package/dist/src/cli/setup.js +483 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/skills.d.ts +28 -0
- package/dist/src/cli/skills.d.ts.map +1 -0
- package/dist/src/cli/skills.js +284 -0
- package/dist/src/cli/skills.js.map +1 -0
- package/dist/src/cli/start.d.ts +5 -0
- package/dist/src/cli/start.d.ts.map +1 -0
- package/dist/src/cli/start.js +34 -0
- package/dist/src/cli/start.js.map +1 -0
- package/dist/src/cli/status.d.ts +2 -0
- package/dist/src/cli/status.d.ts.map +1 -0
- package/dist/src/cli/status.js +60 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/stop.d.ts +2 -0
- package/dist/src/cli/stop.d.ts.map +1 -0
- package/dist/src/cli/stop.js +11 -0
- package/dist/src/cli/stop.js.map +1 -0
- package/dist/src/connectors/slack/format.d.ts +10 -0
- package/dist/src/connectors/slack/format.d.ts.map +1 -0
- package/dist/src/connectors/slack/format.js +55 -0
- package/dist/src/connectors/slack/format.js.map +1 -0
- package/dist/src/connectors/slack/index.d.ts +18 -0
- package/dist/src/connectors/slack/index.d.ts.map +1 -0
- package/dist/src/connectors/slack/index.js +122 -0
- package/dist/src/connectors/slack/index.js.map +1 -0
- package/dist/src/connectors/slack/threads.d.ts +2 -0
- package/dist/src/connectors/slack/threads.d.ts.map +1 -0
- package/dist/src/connectors/slack/threads.js +10 -0
- package/dist/src/connectors/slack/threads.js.map +1 -0
- package/dist/src/cron/jobs.d.ts +5 -0
- package/dist/src/cron/jobs.d.ts.map +1 -0
- package/dist/src/cron/jobs.js +23 -0
- package/dist/src/cron/jobs.js.map +1 -0
- package/dist/src/cron/runner.d.ts +3 -0
- package/dist/src/cron/runner.d.ts.map +1 -0
- package/dist/src/cron/runner.js +118 -0
- package/dist/src/cron/runner.js.map +1 -0
- package/dist/src/cron/scheduler.d.ts +5 -0
- package/dist/src/cron/scheduler.d.ts.map +1 -0
- package/dist/src/cron/scheduler.js +39 -0
- package/dist/src/cron/scheduler.js.map +1 -0
- package/dist/src/engines/claude.d.ts +14 -0
- package/dist/src/engines/claude.d.ts.map +1 -0
- package/dist/src/engines/claude.js +264 -0
- package/dist/src/engines/claude.js.map +1 -0
- package/dist/src/engines/codex.d.ts +15 -0
- package/dist/src/engines/codex.d.ts.map +1 -0
- package/dist/src/engines/codex.js +346 -0
- package/dist/src/engines/codex.js.map +1 -0
- package/dist/src/gateway/api.d.ts +13 -0
- package/dist/src/gateway/api.d.ts.map +1 -0
- package/dist/src/gateway/api.js +819 -0
- package/dist/src/gateway/api.js.map +1 -0
- package/dist/src/gateway/daemon-entry.d.ts +2 -0
- package/dist/src/gateway/daemon-entry.d.ts.map +1 -0
- package/dist/src/gateway/daemon-entry.js +12 -0
- package/dist/src/gateway/daemon-entry.js.map +1 -0
- package/dist/src/gateway/lifecycle.d.ts +10 -0
- package/dist/src/gateway/lifecycle.d.ts.map +1 -0
- package/dist/src/gateway/lifecycle.js +124 -0
- package/dist/src/gateway/lifecycle.js.map +1 -0
- package/dist/src/gateway/org.d.ts +10 -0
- package/dist/src/gateway/org.d.ts.map +1 -0
- package/dist/src/gateway/org.js +71 -0
- package/dist/src/gateway/org.js.map +1 -0
- package/dist/src/gateway/server.d.ts +4 -0
- package/dist/src/gateway/server.d.ts.map +1 -0
- package/dist/src/gateway/server.js +301 -0
- package/dist/src/gateway/server.js.map +1 -0
- package/dist/src/gateway/watcher.d.ts +14 -0
- package/dist/src/gateway/watcher.d.ts.map +1 -0
- package/dist/src/gateway/watcher.js +104 -0
- package/dist/src/gateway/watcher.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/sessions/context.d.ts +20 -0
- package/dist/src/sessions/context.d.ts.map +1 -0
- package/dist/src/sessions/context.js +532 -0
- package/dist/src/sessions/context.js.map +1 -0
- package/dist/src/sessions/manager.d.ts +38 -0
- package/dist/src/sessions/manager.d.ts.map +1 -0
- package/dist/src/sessions/manager.js +208 -0
- package/dist/src/sessions/manager.js.map +1 -0
- package/dist/src/sessions/queue.d.ts +14 -0
- package/dist/src/sessions/queue.d.ts.map +1 -0
- package/dist/src/sessions/queue.js +42 -0
- package/dist/src/sessions/queue.js.map +1 -0
- package/dist/src/sessions/registry.d.ts +46 -0
- package/dist/src/sessions/registry.d.ts.map +1 -0
- package/dist/src/sessions/registry.js +193 -0
- package/dist/src/sessions/registry.js.map +1 -0
- package/dist/src/shared/config.d.ts +3 -0
- package/dist/src/shared/config.d.ts.map +1 -0
- package/dist/src/shared/config.js +11 -0
- package/dist/src/shared/config.js.map +1 -0
- package/dist/src/shared/logger.d.ts +12 -0
- package/dist/src/shared/logger.d.ts.map +1 -0
- package/dist/src/shared/logger.js +35 -0
- package/dist/src/shared/logger.js.map +1 -0
- package/dist/src/shared/paths.d.ts +19 -0
- package/dist/src/shared/paths.d.ts.map +1 -0
- package/dist/src/shared/paths.js +31 -0
- package/dist/src/shared/paths.js.map +1 -0
- package/dist/src/shared/types.d.ts +166 -0
- package/dist/src/shared/types.d.ts.map +1 -0
- package/dist/src/shared/types.js +4 -0
- package/dist/src/shared/types.js.map +1 -0
- package/dist/src/shared/version.d.ts +15 -0
- package/dist/src/shared/version.d.ts.map +1 -0
- package/dist/src/shared/version.js +56 -0
- package/dist/src/shared/version.js.map +1 -0
- package/dist/web/404.html +1 -0
- package/dist/web/_next/static/chunks/198-fd91406a158c5c25.js +1 -0
- package/dist/web/_next/static/chunks/517.62389e8d3c929c43.js +1 -0
- package/dist/web/_next/static/chunks/534-17c49c944e0d0fe1.js +1 -0
- package/dist/web/_next/static/chunks/573-070537ec2452d03e.js +1 -0
- package/dist/web/_next/static/chunks/590-2c34156c7417317e.js +1 -0
- package/dist/web/_next/static/chunks/704-af2893821e1d18dc.js +1 -0
- package/dist/web/_next/static/chunks/7273c211.06e3b6021d90b73f.js +1 -0
- package/dist/web/_next/static/chunks/73-c226535579393e21.js +1 -0
- package/dist/web/_next/static/chunks/743-5bb03adbb0e4ddec.js +1 -0
- package/dist/web/_next/static/chunks/874.97d5a27895061057.js +1 -0
- package/dist/web/_next/static/chunks/8e6518bb-c26e82767f1faf66.js +1 -0
- package/dist/web/_next/static/chunks/app/_not-found/page-bb075b0779827928.js +1 -0
- package/dist/web/_next/static/chunks/app/chat/page-6d5bc707a45c92c6.js +1 -0
- package/dist/web/_next/static/chunks/app/costs/page-d6c03718defdb599.js +1 -0
- package/dist/web/_next/static/chunks/app/cron/page-4c563eef2b6231fe.js +1 -0
- package/dist/web/_next/static/chunks/app/kanban/page-55a73165a36f4077.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-5129b67d5f126cf0.js +1 -0
- package/dist/web/_next/static/chunks/app/logs/page-e18889d67e48c9c9.js +1 -0
- package/dist/web/_next/static/chunks/app/org/page-d5cd8d9b7864737b.js +1 -0
- package/dist/web/_next/static/chunks/app/page-b81992940fd1dbc6.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/page-2eef6ac7882a28ba.js +1 -0
- package/dist/web/_next/static/chunks/app/settings/page-4fb01b9b09500170.js +1 -0
- package/dist/web/_next/static/chunks/app/skills/page-df9465e314561bb5.js +1 -0
- package/dist/web/_next/static/chunks/framework-077b27ad7787463c.js +1 -0
- package/dist/web/_next/static/chunks/main-app-437f51faf74fbb3b.js +1 -0
- package/dist/web/_next/static/chunks/main-f1c74cefd4965abf.js +1 -0
- package/dist/web/_next/static/chunks/pages/_app-77a85fe7d6bca671.js +1 -0
- package/dist/web/_next/static/chunks/pages/_error-68febf4b34900064.js +1 -0
- package/dist/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/web/_next/static/chunks/webpack-0f39b7e91dce9791.js +1 -0
- package/dist/web/_next/static/css/4a6a5bca9238c104.css +1 -0
- package/dist/web/_next/static/vLvOwhC8JocJzSHTHKKOv/_buildManifest.js +1 -0
- package/dist/web/_next/static/vLvOwhC8JocJzSHTHKKOv/_ssgManifest.js +1 -0
- package/dist/web/chat.html +1 -0
- package/dist/web/chat.txt +20 -0
- package/dist/web/costs.html +16 -0
- package/dist/web/costs.txt +20 -0
- package/dist/web/cron.html +1 -0
- package/dist/web/cron.txt +20 -0
- package/dist/web/index.html +1 -0
- package/dist/web/index.txt +20 -0
- package/dist/web/kanban.html +1 -0
- package/dist/web/kanban.txt +20 -0
- package/dist/web/logs.html +7 -0
- package/dist/web/logs.txt +20 -0
- package/dist/web/org.html +1 -0
- package/dist/web/org.txt +20 -0
- package/dist/web/sessions.html +1 -0
- package/dist/web/sessions.txt +20 -0
- package/dist/web/settings.html +1 -0
- package/dist/web/settings.txt +20 -0
- package/dist/web/skills.html +1 -0
- package/dist/web/skills.txt +20 -0
- package/package.json +43 -0
- package/template/AGENTS.md +167 -0
- package/template/CLAUDE.md +106 -0
- package/template/config.default.yaml +27 -0
- package/template/docs/architecture.md +74 -0
- package/template/docs/connectors.md +72 -0
- package/template/docs/cron.md +137 -0
- package/template/docs/org.md +105 -0
- package/template/docs/overview.md +39 -0
- package/template/docs/self-modification.md +65 -0
- package/template/docs/skills.md +58 -0
- package/template/knowledge/.gitkeep +0 -0
- package/template/migrations/.gitkeep +0 -0
- package/template/migrations/0.1.0/MIGRATION.md +25 -0
- package/template/skills/cron-manager/SKILL.md +127 -0
- package/template/skills/find-and-install/SKILL.md +92 -0
- package/template/skills/management/SKILL.md +203 -0
- package/template/skills/migrate/SKILL.md +154 -0
- package/template/skills/new/SKILL.md +19 -0
- package/template/skills/onboarding/SKILL.md +106 -0
- package/template/skills/self-heal/SKILL.md +114 -0
- package/template/skills/skill-creator/SKILL.md +112 -0
- package/template/skills/status/SKILL.md +19 -0
- package/template/skills/sync/SKILL.md +67 -0
- 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
|