@prismshadow/penguin-server 0.2.1 → 0.2.2
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/api/types.d.ts +233 -6
- package/dist/{chunk-V6KH4FRG.js → chunk-OMBFPEGQ.js} +1 -1
- package/dist/chunk-OMBFPEGQ.js.map +1 -0
- package/dist/chunk-UUJCF46V.js +50 -0
- package/dist/chunk-UUJCF46V.js.map +1 -0
- package/dist/index.js +1081 -207
- package/dist/index.js.map +1 -1
- package/dist/initial-password.d.ts +17 -0
- package/dist/initial-password.js +15 -0
- package/dist/initial-password.js.map +1 -0
- package/dist/lock.js +1 -1
- package/package.json +8 -3
- package/web-dist/assets/{index-DTZ3P5wj.js → index-B3puu-Hr.js} +1 -1
- package/web-dist/assets/index-CaDuyrN0.css +1 -0
- package/web-dist/assets/index-rgHg3m6g.js +181 -0
- package/web-dist/index.html +2 -2
- package/dist/chunk-V6KH4FRG.js.map +0 -1
- package/web-dist/assets/index-BvejhGl5.css +0 -1
- package/web-dist/assets/index-yzk1uoHO.js +0 -172
package/dist/index.js
CHANGED
|
@@ -2,21 +2,79 @@ import {
|
|
|
2
2
|
acquireServerLock,
|
|
3
3
|
liveServerLock,
|
|
4
4
|
releaseServerLock
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OMBFPEGQ.js";
|
|
6
|
+
import {
|
|
7
|
+
clearInitialAdminPassword,
|
|
8
|
+
readInitialAdminPassword,
|
|
9
|
+
renderInitialPasswordNotice,
|
|
10
|
+
storeInitialAdminPassword
|
|
11
|
+
} from "./chunk-UUJCF46V.js";
|
|
6
12
|
|
|
7
13
|
// src/index.ts
|
|
8
|
-
import
|
|
9
|
-
import
|
|
14
|
+
import fs23 from "fs";
|
|
15
|
+
import path19 from "path";
|
|
10
16
|
import { config as loadDotenv } from "dotenv";
|
|
11
17
|
import { serve } from "@hono/node-server";
|
|
12
18
|
|
|
13
19
|
// src/app.ts
|
|
14
|
-
import
|
|
20
|
+
import fs21 from "fs";
|
|
15
21
|
import fsp from "fs/promises";
|
|
16
|
-
import
|
|
17
|
-
import { Hono as
|
|
22
|
+
import path17 from "path";
|
|
23
|
+
import { Hono as Hono25 } from "hono";
|
|
18
24
|
import { bodyLimit } from "hono/body-limit";
|
|
19
25
|
|
|
26
|
+
// src/net/proxy.ts
|
|
27
|
+
import { Agent, EnvHttpProxyAgent, fetch as undiciFetch, setGlobalDispatcher } from "undici";
|
|
28
|
+
var LOOPBACK_NO_PROXY = ["localhost", "127.0.0.1", "::1"];
|
|
29
|
+
function mergedNoProxy(env = process.env) {
|
|
30
|
+
const raw = env.no_proxy ?? env.NO_PROXY ?? "";
|
|
31
|
+
const entries = raw.split(/[,\s]/).filter((e) => e !== "");
|
|
32
|
+
const present = new Set(entries.map((e) => e.toLowerCase()));
|
|
33
|
+
for (const name of LOOPBACK_NO_PROXY) {
|
|
34
|
+
if (!present.has(name)) entries.push(name);
|
|
35
|
+
}
|
|
36
|
+
return entries.join(",");
|
|
37
|
+
}
|
|
38
|
+
function normalizeProxyUrl(raw) {
|
|
39
|
+
const trimmed = raw.trim();
|
|
40
|
+
if (trimmed === "") return null;
|
|
41
|
+
const candidate = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
42
|
+
let url;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(candidate);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
49
|
+
if (url.hostname === "" || url.username !== "" || url.password !== "") return null;
|
|
50
|
+
if (url.pathname !== "/" && url.pathname !== "" || url.search !== "" || url.hash !== "") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return url.origin;
|
|
54
|
+
}
|
|
55
|
+
function envProxyAgentOptions(proxyUrl, env) {
|
|
56
|
+
const noProxy = mergedNoProxy(env);
|
|
57
|
+
return proxyUrl === null ? { noProxy } : { httpProxy: proxyUrl, httpsProxy: proxyUrl, noProxy };
|
|
58
|
+
}
|
|
59
|
+
function buildProxyDispatcher(settings, env) {
|
|
60
|
+
if (!settings.proxyForApp) return new Agent();
|
|
61
|
+
return new EnvHttpProxyAgent(envProxyAgentOptions(settings.proxyUrl, env));
|
|
62
|
+
}
|
|
63
|
+
var current = { proxyForApp: true, proxyUrl: null };
|
|
64
|
+
var installed = false;
|
|
65
|
+
function installGlobalProxyDispatcher() {
|
|
66
|
+
installed = true;
|
|
67
|
+
setGlobalDispatcher(buildProxyDispatcher(current));
|
|
68
|
+
globalThis.fetch = undiciFetch;
|
|
69
|
+
}
|
|
70
|
+
function applyProxySettings(settings) {
|
|
71
|
+
if (settings.proxyForApp === current.proxyForApp && settings.proxyUrl === current.proxyUrl) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
current = { proxyForApp: settings.proxyForApp, proxyUrl: settings.proxyUrl };
|
|
75
|
+
if (installed) setGlobalDispatcher(buildProxyDispatcher(current));
|
|
76
|
+
}
|
|
77
|
+
|
|
20
78
|
// src/db/database.ts
|
|
21
79
|
import { mkdirSync } from "fs";
|
|
22
80
|
import path from "path";
|
|
@@ -133,6 +191,10 @@ CREATE TABLE IF NOT EXISTS ui_prefs (
|
|
|
133
191
|
user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
|
|
134
192
|
prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON
|
|
135
193
|
);
|
|
194
|
+
CREATE TABLE IF NOT EXISTS server_settings ( -- admin-level server-global settings (GET/PUT /api/admin/settings)
|
|
195
|
+
key TEXT PRIMARY KEY, -- setting name, e.g. 'proxy_for_app'
|
|
196
|
+
value TEXT NOT NULL -- JSON-encoded value; an absent row means the setting's built-in default
|
|
197
|
+
);
|
|
136
198
|
CREATE TABLE IF NOT EXISTS trace_files ( -- DERIVED CACHE of the on-disk Trace tree (services/trace-index.ts): the directories stay the single source of truth, every row is rebuildable from disk, and a row is never authority for absence \u2014 consumers reconcile + retry on a miss, so a stale index costs one extra scan, never a false 404
|
|
137
199
|
project_id TEXT NOT NULL,
|
|
138
200
|
agent_id TEXT NOT NULL,
|
|
@@ -650,6 +712,67 @@ var SchedulesRepo = class {
|
|
|
650
712
|
}
|
|
651
713
|
};
|
|
652
714
|
|
|
715
|
+
// src/db/repos/server-settings.ts
|
|
716
|
+
var PROXY_FOR_APP_KEY = "proxy_for_app";
|
|
717
|
+
var PROXY_FOR_AGENT_KEY = "proxy_for_agent";
|
|
718
|
+
var LEGACY_USE_SYSTEM_PROXY_KEY = "use_system_proxy";
|
|
719
|
+
var PROXY_URL_KEY = "proxy_url";
|
|
720
|
+
var ServerSettingsRepo = class {
|
|
721
|
+
constructor(db) {
|
|
722
|
+
this.db = db;
|
|
723
|
+
}
|
|
724
|
+
db;
|
|
725
|
+
/** Returns the raw JSON-encoded value; null if never set. */
|
|
726
|
+
get(key) {
|
|
727
|
+
const r = this.db.prepare("SELECT value FROM server_settings WHERE key = ?").get(key);
|
|
728
|
+
return r ? r.value : null;
|
|
729
|
+
}
|
|
730
|
+
set(key, value) {
|
|
731
|
+
this.db.prepare(
|
|
732
|
+
`INSERT INTO server_settings (key, value) VALUES (?, ?)
|
|
733
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
734
|
+
).run(key, value);
|
|
735
|
+
}
|
|
736
|
+
/** Shared switch read: this key if present, else the legacy single switch, else the default: on. */
|
|
737
|
+
getProxySwitch(key) {
|
|
738
|
+
const raw = this.get(key);
|
|
739
|
+
if (raw !== null) return raw !== "false";
|
|
740
|
+
return this.get(LEGACY_USE_SYSTEM_PROXY_KEY) !== "false";
|
|
741
|
+
}
|
|
742
|
+
/** The "application uses the proxy" switch (the server's own outbound dispatcher). */
|
|
743
|
+
getProxyForApp() {
|
|
744
|
+
return this.getProxySwitch(PROXY_FOR_APP_KEY);
|
|
745
|
+
}
|
|
746
|
+
setProxyForApp(value) {
|
|
747
|
+
this.set(PROXY_FOR_APP_KEY, JSON.stringify(value));
|
|
748
|
+
}
|
|
749
|
+
/** The "agent environment uses the proxy" switch (command subprocess env policy). */
|
|
750
|
+
getProxyForAgent() {
|
|
751
|
+
return this.getProxySwitch(PROXY_FOR_AGENT_KEY);
|
|
752
|
+
}
|
|
753
|
+
setProxyForAgent(value) {
|
|
754
|
+
this.set(PROXY_FOR_AGENT_KEY, JSON.stringify(value));
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The explicit proxy address (normalized at write time by the settings route); null =
|
|
758
|
+
* follow the proxy environment variables. An absent or unreadable row reads as null
|
|
759
|
+
* (the safe default: behave as before the setting existed).
|
|
760
|
+
*/
|
|
761
|
+
getProxyUrl() {
|
|
762
|
+
const raw = this.get(PROXY_URL_KEY);
|
|
763
|
+
if (raw === null) return null;
|
|
764
|
+
try {
|
|
765
|
+
const value = JSON.parse(raw);
|
|
766
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
setProxyUrl(value) {
|
|
772
|
+
this.set(PROXY_URL_KEY, JSON.stringify(value));
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
653
776
|
// src/db/repos/sessions.ts
|
|
654
777
|
function mapRow4(r) {
|
|
655
778
|
return {
|
|
@@ -1305,6 +1428,10 @@ var AuthService = class {
|
|
|
1305
1428
|
}
|
|
1306
1429
|
return password;
|
|
1307
1430
|
}
|
|
1431
|
+
/** Whether the built-in admin still runs on its initial password (drives the startup reminder notice). */
|
|
1432
|
+
adminPasswordIsInitial() {
|
|
1433
|
+
return this.deps.users.findById(ADMIN_USER_ID)?.passwordIsInitial === true;
|
|
1434
|
+
}
|
|
1308
1435
|
/** Consecutive login failures per userId (see the throttling comment on the constants). */
|
|
1309
1436
|
loginFailures = /* @__PURE__ */ new Map();
|
|
1310
1437
|
/** The wait imposed after `failures` consecutive failures (0 while within the free attempts). */
|
|
@@ -1366,6 +1493,7 @@ var AuthService = class {
|
|
|
1366
1493
|
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
|
|
1367
1494
|
}
|
|
1368
1495
|
this.deps.users.updatePassword(userId, await hashPassword(newPassword), false);
|
|
1496
|
+
this.deps.onPasswordChanged?.(userId);
|
|
1369
1497
|
}
|
|
1370
1498
|
/**
|
|
1371
1499
|
* Desktop-session password set: no old-password check. Only reachable for sessions
|
|
@@ -1378,6 +1506,7 @@ var AuthService = class {
|
|
|
1378
1506
|
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
|
|
1379
1507
|
}
|
|
1380
1508
|
this.deps.users.updatePassword(userId, await hashPassword(newPassword), false);
|
|
1509
|
+
this.deps.onPasswordChanged?.(userId);
|
|
1381
1510
|
}
|
|
1382
1511
|
logout(token) {
|
|
1383
1512
|
this.deps.authSessions.delete(sha256Hex(token));
|
|
@@ -1418,7 +1547,38 @@ var AuthService = class {
|
|
|
1418
1547
|
};
|
|
1419
1548
|
|
|
1420
1549
|
// src/http/routes/admin.ts
|
|
1550
|
+
import { Hono as Hono2 } from "hono";
|
|
1551
|
+
|
|
1552
|
+
// src/http/routes/desktop.ts
|
|
1421
1553
|
import { Hono } from "hono";
|
|
1554
|
+
function rejectInDesktopMode(deps2) {
|
|
1555
|
+
return async (_c, next) => {
|
|
1556
|
+
if (deps2.desktop !== null) {
|
|
1557
|
+
throw new HttpError(
|
|
1558
|
+
403,
|
|
1559
|
+
"desktop_single_user",
|
|
1560
|
+
"User management is disabled in the desktop app (single-user mode)."
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
await next();
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
var SHUTDOWN_DELAY_MS = 50;
|
|
1567
|
+
function desktopRoutes(deps2) {
|
|
1568
|
+
const app2 = new Hono();
|
|
1569
|
+
app2.post("/shutdown", (c) => {
|
|
1570
|
+
const desktop = deps2.desktop;
|
|
1571
|
+
if (!desktop) throw new HttpError(404, "not_found", "Desktop mode is not enabled.");
|
|
1572
|
+
const header = c.req.header("authorization") ?? "";
|
|
1573
|
+
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
1574
|
+
if (token === "" || !desktop.verifyToken(token)) {
|
|
1575
|
+
throw new HttpError(401, "unauthorized", "Invalid desktop token.");
|
|
1576
|
+
}
|
|
1577
|
+
setTimeout(() => desktop.requestShutdown(), SHUTDOWN_DELAY_MS).unref();
|
|
1578
|
+
return c.body(null, 202);
|
|
1579
|
+
});
|
|
1580
|
+
return app2;
|
|
1581
|
+
}
|
|
1422
1582
|
|
|
1423
1583
|
// src/http/validate.ts
|
|
1424
1584
|
import { isValidId } from "@prismshadow/penguin-core";
|
|
@@ -1546,7 +1706,8 @@ function optionalDateParam(value, label) {
|
|
|
1546
1706
|
|
|
1547
1707
|
// src/http/routes/admin.ts
|
|
1548
1708
|
function adminUsersRoutes(deps2) {
|
|
1549
|
-
const app2 = new
|
|
1709
|
+
const app2 = new Hono2();
|
|
1710
|
+
app2.use("*", rejectInDesktopMode(deps2));
|
|
1550
1711
|
app2.use("*", async (c, next) => {
|
|
1551
1712
|
if (!c.var.user.isAdmin) {
|
|
1552
1713
|
throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
|
|
@@ -1576,8 +1737,57 @@ function adminUsersRoutes(deps2) {
|
|
|
1576
1737
|
return app2;
|
|
1577
1738
|
}
|
|
1578
1739
|
|
|
1740
|
+
// src/http/routes/admin-settings.ts
|
|
1741
|
+
import { Hono as Hono3 } from "hono";
|
|
1742
|
+
function parseProxyUrl(value) {
|
|
1743
|
+
if (value === null) return null;
|
|
1744
|
+
if (typeof value === "string") {
|
|
1745
|
+
if (value.trim() === "") return null;
|
|
1746
|
+
const normalized = normalizeProxyUrl(value);
|
|
1747
|
+
if (normalized !== null) return normalized;
|
|
1748
|
+
}
|
|
1749
|
+
throw new HttpError(
|
|
1750
|
+
400,
|
|
1751
|
+
"invalid_proxy_url",
|
|
1752
|
+
"proxyUrl must be http://host[:port], https://host[:port], or host[:port] (empty or null clears it)."
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
function adminSettingsRoutes(deps2) {
|
|
1756
|
+
const app2 = new Hono3();
|
|
1757
|
+
app2.use("*", async (c, next) => {
|
|
1758
|
+
if (!c.var.user.isAdmin) {
|
|
1759
|
+
throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
|
|
1760
|
+
}
|
|
1761
|
+
await next();
|
|
1762
|
+
});
|
|
1763
|
+
const settings = () => ({
|
|
1764
|
+
settings: {
|
|
1765
|
+
proxyForApp: deps2.serverSettingsRepo.getProxyForApp(),
|
|
1766
|
+
proxyForAgent: deps2.serverSettingsRepo.getProxyForAgent(),
|
|
1767
|
+
proxyUrl: deps2.serverSettingsRepo.getProxyUrl()
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
app2.get("/", (c) => c.json(settings()));
|
|
1771
|
+
app2.put("/", async (c) => {
|
|
1772
|
+
const body = await readJson(c);
|
|
1773
|
+
const proxyForApp = optionalBoolean(body, "proxyForApp");
|
|
1774
|
+
const proxyForAgent = optionalBoolean(body, "proxyForAgent");
|
|
1775
|
+
const proxyUrlProvided = body.proxyUrl !== void 0;
|
|
1776
|
+
const proxyUrl = proxyUrlProvided ? parseProxyUrl(body.proxyUrl) : null;
|
|
1777
|
+
if (proxyForApp !== void 0) deps2.serverSettingsRepo.setProxyForApp(proxyForApp);
|
|
1778
|
+
if (proxyForAgent !== void 0) deps2.serverSettingsRepo.setProxyForAgent(proxyForAgent);
|
|
1779
|
+
if (proxyUrlProvided) deps2.serverSettingsRepo.setProxyUrl(proxyUrl);
|
|
1780
|
+
applyProxySettings({
|
|
1781
|
+
proxyForApp: deps2.serverSettingsRepo.getProxyForApp(),
|
|
1782
|
+
proxyUrl: deps2.serverSettingsRepo.getProxyUrl()
|
|
1783
|
+
});
|
|
1784
|
+
return c.json(settings());
|
|
1785
|
+
});
|
|
1786
|
+
return app2;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1579
1789
|
// src/http/routes/auth.ts
|
|
1580
|
-
import { Hono as
|
|
1790
|
+
import { Hono as Hono4 } from "hono";
|
|
1581
1791
|
import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
|
|
1582
1792
|
function cookieOptions(c) {
|
|
1583
1793
|
return {
|
|
@@ -1590,7 +1800,7 @@ function cookieOptions(c) {
|
|
|
1590
1800
|
};
|
|
1591
1801
|
}
|
|
1592
1802
|
function authRoutes(deps2) {
|
|
1593
|
-
const app2 = new
|
|
1803
|
+
const app2 = new Hono4();
|
|
1594
1804
|
app2.post("/login", async (c) => {
|
|
1595
1805
|
const body = await readJson(c);
|
|
1596
1806
|
const userId = requireString(body, "userId", { label: "userId" });
|
|
@@ -1620,7 +1830,7 @@ function authRoutes(deps2) {
|
|
|
1620
1830
|
}
|
|
1621
1831
|
|
|
1622
1832
|
// src/http/routes/me.ts
|
|
1623
|
-
import { Hono as
|
|
1833
|
+
import { Hono as Hono5 } from "hono";
|
|
1624
1834
|
|
|
1625
1835
|
// src/services/preview-token.ts
|
|
1626
1836
|
import { createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
@@ -1715,7 +1925,7 @@ function resolvePreviewTarget(requestUrl, hostHeader, configuredOrigin, serverBi
|
|
|
1715
1925
|
|
|
1716
1926
|
// src/http/routes/me.ts
|
|
1717
1927
|
function meRoutes(deps2) {
|
|
1718
|
-
const app2 = new
|
|
1928
|
+
const app2 = new Hono5();
|
|
1719
1929
|
app2.get("/", (c) => {
|
|
1720
1930
|
const target = resolvePreviewTarget(
|
|
1721
1931
|
c.req.url,
|
|
@@ -1757,15 +1967,15 @@ function meRoutes(deps2) {
|
|
|
1757
1967
|
app2.put("/prefs", async (c) => {
|
|
1758
1968
|
const body = await readJson(c);
|
|
1759
1969
|
const raw = deps2.prefsRepo.get(c.var.user.userId);
|
|
1760
|
-
let
|
|
1970
|
+
let current2 = {};
|
|
1761
1971
|
if (raw !== null) {
|
|
1762
1972
|
try {
|
|
1763
|
-
|
|
1973
|
+
current2 = JSON.parse(raw);
|
|
1764
1974
|
} catch {
|
|
1765
|
-
|
|
1975
|
+
current2 = {};
|
|
1766
1976
|
}
|
|
1767
1977
|
}
|
|
1768
|
-
const merged = { ...
|
|
1978
|
+
const merged = { ...current2, ...body };
|
|
1769
1979
|
deps2.prefsRepo.set(c.var.user.userId, JSON.stringify(merged));
|
|
1770
1980
|
return c.json({ prefs: merged });
|
|
1771
1981
|
});
|
|
@@ -1773,7 +1983,7 @@ function meRoutes(deps2) {
|
|
|
1773
1983
|
}
|
|
1774
1984
|
|
|
1775
1985
|
// src/http/routes/events.ts
|
|
1776
|
-
import { Hono as
|
|
1986
|
+
import { Hono as Hono6 } from "hono";
|
|
1777
1987
|
|
|
1778
1988
|
// src/http/sse.ts
|
|
1779
1989
|
import { streamSSE } from "hono/streaming";
|
|
@@ -1840,7 +2050,7 @@ function userChannelKey(userId) {
|
|
|
1840
2050
|
return `user:${userId}`;
|
|
1841
2051
|
}
|
|
1842
2052
|
function eventsRoutes(deps2) {
|
|
1843
|
-
const app2 = new
|
|
2053
|
+
const app2 = new Hono6();
|
|
1844
2054
|
app2.get("/", (c) => {
|
|
1845
2055
|
const channel = deps2.channels.get(userChannelKey(c.var.user.userId));
|
|
1846
2056
|
return sseEndpoint(c, channel, { initialEvents: [{ type: "hello" }] });
|
|
@@ -1849,9 +2059,9 @@ function eventsRoutes(deps2) {
|
|
|
1849
2059
|
}
|
|
1850
2060
|
|
|
1851
2061
|
// src/http/routes/projects.ts
|
|
1852
|
-
import { Hono as
|
|
2062
|
+
import { Hono as Hono7 } from "hono";
|
|
1853
2063
|
function projectsRoutes(deps2) {
|
|
1854
|
-
const app2 = new
|
|
2064
|
+
const app2 = new Hono7();
|
|
1855
2065
|
app2.get("/", async (c) => {
|
|
1856
2066
|
const projects = await deps2.projectService.listProjects(c.var.user.userId);
|
|
1857
2067
|
return c.json({ projects });
|
|
@@ -1879,9 +2089,10 @@ function projectsRoutes(deps2) {
|
|
|
1879
2089
|
}
|
|
1880
2090
|
|
|
1881
2091
|
// src/http/routes/members.ts
|
|
1882
|
-
import { Hono as
|
|
2092
|
+
import { Hono as Hono8 } from "hono";
|
|
1883
2093
|
function membersRoutes(deps2) {
|
|
1884
|
-
const app2 = new
|
|
2094
|
+
const app2 = new Hono8();
|
|
2095
|
+
app2.use("*", rejectInDesktopMode(deps2));
|
|
1885
2096
|
app2.get("/", (c) => {
|
|
1886
2097
|
const members = deps2.projectService.listMembers(
|
|
1887
2098
|
c.var.user.userId,
|
|
@@ -1911,7 +2122,7 @@ function membersRoutes(deps2) {
|
|
|
1911
2122
|
}
|
|
1912
2123
|
|
|
1913
2124
|
// src/http/routes/models.ts
|
|
1914
|
-
import { Hono as
|
|
2125
|
+
import { Hono as Hono9 } from "hono";
|
|
1915
2126
|
function publishCredentialsUpdated(deps2, projectId) {
|
|
1916
2127
|
const event = { type: "credentials_updated" };
|
|
1917
2128
|
for (const row of deps2.sessionsRepo.listByProject(projectId)) {
|
|
@@ -2027,7 +2238,7 @@ function parseModelsUpdate(body) {
|
|
|
2027
2238
|
return req;
|
|
2028
2239
|
}
|
|
2029
2240
|
function modelsRoutes(deps2) {
|
|
2030
|
-
const app2 = new
|
|
2241
|
+
const app2 = new Hono9();
|
|
2031
2242
|
app2.get("/", async (c) => {
|
|
2032
2243
|
const projectId = requireValidId(c, "projectId");
|
|
2033
2244
|
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
@@ -2089,13 +2300,13 @@ function modelsRoutes(deps2) {
|
|
|
2089
2300
|
}
|
|
2090
2301
|
|
|
2091
2302
|
// src/http/routes/chat-defaults.ts
|
|
2092
|
-
import { Hono as
|
|
2303
|
+
import { Hono as Hono11 } from "hono";
|
|
2093
2304
|
import { DEFAULT_CHAT_THINKING_LEVELS, isValidId as isValidId2 } from "@prismshadow/penguin-core";
|
|
2094
2305
|
|
|
2095
2306
|
// src/http/routes/sessions.ts
|
|
2096
2307
|
import fs4 from "fs/promises";
|
|
2097
2308
|
import path4 from "path";
|
|
2098
|
-
import { Hono as
|
|
2309
|
+
import { Hono as Hono10 } from "hono";
|
|
2099
2310
|
import { imageUrlMessage, scratchpadDir, userText } from "@prismshadow/penguin-core";
|
|
2100
2311
|
|
|
2101
2312
|
// src/services/message-window.ts
|
|
@@ -2323,7 +2534,7 @@ async function assertWorkspaceAllowed(args) {
|
|
|
2323
2534
|
throw new HttpError(
|
|
2324
2535
|
400,
|
|
2325
2536
|
"workspace_not_found",
|
|
2326
|
-
`Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary
|
|
2537
|
+
`Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary workspace.`
|
|
2327
2538
|
);
|
|
2328
2539
|
}
|
|
2329
2540
|
const stat = await fs.stat(ws);
|
|
@@ -2770,6 +2981,12 @@ function pageLimit(raw, name) {
|
|
|
2770
2981
|
}
|
|
2771
2982
|
return v;
|
|
2772
2983
|
}
|
|
2984
|
+
function appendPendingInputs(messages, pending) {
|
|
2985
|
+
if (pending.length === 0) return messages;
|
|
2986
|
+
const tail = new Set(messages.slice(-50).map((m) => JSON.stringify(m)));
|
|
2987
|
+
const missing = pending.filter((m) => !tail.has(JSON.stringify(m)));
|
|
2988
|
+
return missing.length > 0 ? [...messages, ...missing] : messages;
|
|
2989
|
+
}
|
|
2773
2990
|
function messagesPageQuery(c) {
|
|
2774
2991
|
const rawTail = c.req.query("tailLimit");
|
|
2775
2992
|
const rawBefore = c.req.query("before");
|
|
@@ -2883,7 +3100,7 @@ function parseGoalField(body) {
|
|
|
2883
3100
|
return { budget: budget ?? -1 };
|
|
2884
3101
|
}
|
|
2885
3102
|
function agentSessionsRoutes(deps2) {
|
|
2886
|
-
const app2 = new
|
|
3103
|
+
const app2 = new Hono10();
|
|
2887
3104
|
app2.get("/", async (c) => {
|
|
2888
3105
|
const projectId = requireValidId(c, "projectId");
|
|
2889
3106
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -2945,7 +3162,7 @@ function agentSessionsRoutes(deps2) {
|
|
|
2945
3162
|
return app2;
|
|
2946
3163
|
}
|
|
2947
3164
|
function sessionsRoutes(deps2) {
|
|
2948
|
-
const app2 = new
|
|
3165
|
+
const app2 = new Hono10();
|
|
2949
3166
|
const resolveSession = (c) => {
|
|
2950
3167
|
const sessionId = c.req.param("sessionId");
|
|
2951
3168
|
const row = sessionId ? deps2.sessionsRepo.findById(sessionId) : null;
|
|
@@ -3089,11 +3306,18 @@ function sessionsRoutes(deps2) {
|
|
|
3089
3306
|
const row = resolveSession(c);
|
|
3090
3307
|
const page = messagesPageQuery(c);
|
|
3091
3308
|
let live;
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3309
|
+
let pendingInputs = [];
|
|
3310
|
+
if (page?.kind !== "before") {
|
|
3311
|
+
if (deps2.manager.statusOf(row.sessionId) !== "idle") {
|
|
3312
|
+
live = {
|
|
3313
|
+
cursor: deps2.channels.get(row.sessionId).lastEventId,
|
|
3314
|
+
fragments: deps2.manager.liveFragments(row.sessionId)
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
pendingInputs = [
|
|
3318
|
+
...deps2.manager.pendingInputs(row.sessionId),
|
|
3319
|
+
...deps2.manager.pendingBootstrap(row.sessionId)
|
|
3320
|
+
];
|
|
3097
3321
|
}
|
|
3098
3322
|
if (page !== null) {
|
|
3099
3323
|
const result = await deps2.traceService.readMessagesPage(
|
|
@@ -3113,7 +3337,7 @@ function sessionsRoutes(deps2) {
|
|
|
3113
3337
|
}
|
|
3114
3338
|
};
|
|
3115
3339
|
return c.json({
|
|
3116
|
-
messages: result.messages,
|
|
3340
|
+
messages: appendPendingInputs(result.messages, pendingInputs),
|
|
3117
3341
|
...live !== void 0 ? { live } : {},
|
|
3118
3342
|
page: info
|
|
3119
3343
|
});
|
|
@@ -3123,7 +3347,10 @@ function sessionsRoutes(deps2) {
|
|
|
3123
3347
|
row.agentId,
|
|
3124
3348
|
row.sessionId
|
|
3125
3349
|
);
|
|
3126
|
-
return c.json({
|
|
3350
|
+
return c.json({
|
|
3351
|
+
messages: appendPendingInputs(messages, pendingInputs),
|
|
3352
|
+
...live !== void 0 ? { live } : {}
|
|
3353
|
+
});
|
|
3127
3354
|
});
|
|
3128
3355
|
app2.get("/:sessionId/stream", (c) => {
|
|
3129
3356
|
const row = resolveSession(c);
|
|
@@ -3247,6 +3474,30 @@ function sessionsRoutes(deps2) {
|
|
|
3247
3474
|
const aborted = deps2.manager.abortTask(row.sessionId);
|
|
3248
3475
|
return c.body(null, aborted ? 202 : 204);
|
|
3249
3476
|
});
|
|
3477
|
+
app2.get("/:sessionId/processes", (c) => {
|
|
3478
|
+
const row = resolveSession(c);
|
|
3479
|
+
const processes = deps2.manager.listProcesses(row.sessionId).map((p) => ({
|
|
3480
|
+
processId: p.processId,
|
|
3481
|
+
pid: p.pid,
|
|
3482
|
+
cmd: p.cmd,
|
|
3483
|
+
cwd: p.cwd,
|
|
3484
|
+
startedAt: new Date(p.startedAt).toISOString(),
|
|
3485
|
+
running: p.running
|
|
3486
|
+
}));
|
|
3487
|
+
return c.json({ processes });
|
|
3488
|
+
});
|
|
3489
|
+
app2.post("/:sessionId/processes/:processId/kill", (c) => {
|
|
3490
|
+
const row = resolveSession(c);
|
|
3491
|
+
const killed = deps2.manager.killProcess(row.sessionId, pathParam(c, "processId"));
|
|
3492
|
+
if (!killed) {
|
|
3493
|
+
throw new HttpError(
|
|
3494
|
+
404,
|
|
3495
|
+
"process_not_found",
|
|
3496
|
+
"Process does not exist or has already exited."
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
return c.body(null, 204);
|
|
3500
|
+
});
|
|
3250
3501
|
app2.post("/:sessionId/retry-now", (c) => {
|
|
3251
3502
|
const row = resolveSession(c);
|
|
3252
3503
|
const skipped = deps2.manager.retryNow(row.sessionId);
|
|
@@ -3368,7 +3619,7 @@ function sessionsRoutes(deps2) {
|
|
|
3368
3619
|
|
|
3369
3620
|
// src/http/routes/chat-defaults.ts
|
|
3370
3621
|
function chatDefaultsRoutes(deps2) {
|
|
3371
|
-
const app2 = new
|
|
3622
|
+
const app2 = new Hono11();
|
|
3372
3623
|
app2.get("/", async (c) => {
|
|
3373
3624
|
const projectId = requireValidId(c, "projectId");
|
|
3374
3625
|
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
@@ -3402,7 +3653,7 @@ function chatDefaultsRoutes(deps2) {
|
|
|
3402
3653
|
}
|
|
3403
3654
|
|
|
3404
3655
|
// src/http/routes/vault.ts
|
|
3405
|
-
import { Hono as
|
|
3656
|
+
import { Hono as Hono12 } from "hono";
|
|
3406
3657
|
function parseVaultUpdate(body) {
|
|
3407
3658
|
if (!Array.isArray(body.entries)) throw badRequest("entries must be an array.");
|
|
3408
3659
|
const entries = body.entries.map((item, i) => {
|
|
@@ -3424,7 +3675,7 @@ function parseVaultUpdate(body) {
|
|
|
3424
3675
|
return { entries };
|
|
3425
3676
|
}
|
|
3426
3677
|
function vaultRoutes(deps2) {
|
|
3427
|
-
const app2 = new
|
|
3678
|
+
const app2 = new Hono12();
|
|
3428
3679
|
app2.get("/", async (c) => {
|
|
3429
3680
|
const projectId = requireValidId(c, "projectId");
|
|
3430
3681
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -3440,12 +3691,62 @@ function vaultRoutes(deps2) {
|
|
|
3440
3691
|
deps2.manager.invalidateAgentRuntimes(projectId, agentId);
|
|
3441
3692
|
return c.json(res);
|
|
3442
3693
|
});
|
|
3694
|
+
app2.post("/template-placeholder", async (c) => {
|
|
3695
|
+
const projectId = requireValidId(c, "projectId");
|
|
3696
|
+
const agentId = requireValidId(c, "agentId");
|
|
3697
|
+
deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
|
|
3698
|
+
const view = await deps2.agentConfigService.insertTemplatePlaceholder(
|
|
3699
|
+
projectId,
|
|
3700
|
+
agentId,
|
|
3701
|
+
"vault"
|
|
3702
|
+
);
|
|
3703
|
+
return c.json(view.config.vault);
|
|
3704
|
+
});
|
|
3705
|
+
return app2;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
// src/http/routes/memory.ts
|
|
3709
|
+
import { Hono as Hono13 } from "hono";
|
|
3710
|
+
function memoryRoutes(deps2) {
|
|
3711
|
+
const app2 = new Hono13();
|
|
3712
|
+
const scope = (c) => {
|
|
3713
|
+
const projectId = requireValidId(c, "projectId");
|
|
3714
|
+
const agentId = requireValidId(c, "agentId");
|
|
3715
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
3716
|
+
return { projectId, agentId };
|
|
3717
|
+
};
|
|
3718
|
+
app2.get("/", async (c) => {
|
|
3719
|
+
const { projectId, agentId } = scope(c);
|
|
3720
|
+
return c.json(await deps2.memoryService.overview(projectId, agentId));
|
|
3721
|
+
});
|
|
3722
|
+
app2.post("/template-placeholder", async (c) => {
|
|
3723
|
+
const { projectId, agentId } = scope(c);
|
|
3724
|
+
return c.json(await deps2.memoryService.insertTemplatePlaceholder(projectId, agentId));
|
|
3725
|
+
});
|
|
3726
|
+
app2.get("/scopes/:scopeKey/files", async (c) => {
|
|
3727
|
+
const { projectId, agentId } = scope(c);
|
|
3728
|
+
const key = pathParam(c, "scopeKey");
|
|
3729
|
+
return c.json(await deps2.memoryService.listFiles(projectId, agentId, key));
|
|
3730
|
+
});
|
|
3731
|
+
app2.get("/scopes/:scopeKey/files/:fileName", async (c) => {
|
|
3732
|
+
const { projectId, agentId } = scope(c);
|
|
3733
|
+
const key = pathParam(c, "scopeKey");
|
|
3734
|
+
const name = pathParam(c, "fileName");
|
|
3735
|
+
return c.json(await deps2.memoryService.readFile(projectId, agentId, key, name));
|
|
3736
|
+
});
|
|
3737
|
+
app2.delete("/scopes/:scopeKey/files/:fileName", async (c) => {
|
|
3738
|
+
const { projectId, agentId } = scope(c);
|
|
3739
|
+
const key = pathParam(c, "scopeKey");
|
|
3740
|
+
const name = pathParam(c, "fileName");
|
|
3741
|
+
await deps2.memoryService.deleteFile(projectId, agentId, key, name);
|
|
3742
|
+
return c.body(null, 204);
|
|
3743
|
+
});
|
|
3443
3744
|
return app2;
|
|
3444
3745
|
}
|
|
3445
3746
|
|
|
3446
3747
|
// src/http/routes/schedules.ts
|
|
3447
3748
|
import { createHash as createHash2 } from "crypto";
|
|
3448
|
-
import { Hono as
|
|
3749
|
+
import { Hono as Hono14 } from "hono";
|
|
3449
3750
|
import { isValidId as isValidId3 } from "@prismshadow/penguin-core";
|
|
3450
3751
|
|
|
3451
3752
|
// src/runtime/schedule-file.ts
|
|
@@ -3827,7 +4128,7 @@ function requireScheduleName(raw) {
|
|
|
3827
4128
|
return raw;
|
|
3828
4129
|
}
|
|
3829
4130
|
function scheduleRoutes(deps2) {
|
|
3830
|
-
const app2 = new
|
|
4131
|
+
const app2 = new Hono14();
|
|
3831
4132
|
app2.get("/", async (c) => {
|
|
3832
4133
|
const projectId = requireValidId(c, "projectId");
|
|
3833
4134
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -3854,6 +4155,17 @@ function scheduleRoutes(deps2) {
|
|
|
3854
4155
|
await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
|
|
3855
4156
|
return c.json(await readItem(deps2, projectId, agentId, name), 201);
|
|
3856
4157
|
});
|
|
4158
|
+
app2.post("/template-placeholder", async (c) => {
|
|
4159
|
+
const projectId = requireValidId(c, "projectId");
|
|
4160
|
+
const agentId = requireValidId(c, "agentId");
|
|
4161
|
+
deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
|
|
4162
|
+
const view = await deps2.agentConfigService.insertTemplatePlaceholder(
|
|
4163
|
+
projectId,
|
|
4164
|
+
agentId,
|
|
4165
|
+
"schedules"
|
|
4166
|
+
);
|
|
4167
|
+
return c.json(view.config.schedules);
|
|
4168
|
+
});
|
|
3857
4169
|
app2.get("/:name", async (c) => {
|
|
3858
4170
|
const projectId = requireValidId(c, "projectId");
|
|
3859
4171
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -3915,7 +4227,7 @@ async function readItem(deps2, projectId, agentId, name) {
|
|
|
3915
4227
|
}
|
|
3916
4228
|
|
|
3917
4229
|
// src/http/routes/benchmarks.ts
|
|
3918
|
-
import { Hono as
|
|
4230
|
+
import { Hono as Hono15 } from "hono";
|
|
3919
4231
|
var TEXT_PREVIEW_BYTES = 256 * 1024;
|
|
3920
4232
|
function listCaseFiles(deps2, material) {
|
|
3921
4233
|
return async (c) => {
|
|
@@ -3968,7 +4280,7 @@ function readCaseFile(deps2, material) {
|
|
|
3968
4280
|
};
|
|
3969
4281
|
}
|
|
3970
4282
|
function benchmarksRoutes(deps2) {
|
|
3971
|
-
const app2 = new
|
|
4283
|
+
const app2 = new Hono15();
|
|
3972
4284
|
app2.get("/", async (c) => {
|
|
3973
4285
|
const projectId = requireValidId(c, "projectId");
|
|
3974
4286
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -3995,7 +4307,7 @@ function benchmarksRoutes(deps2) {
|
|
|
3995
4307
|
import fs7 from "fs/promises";
|
|
3996
4308
|
import path6 from "path";
|
|
3997
4309
|
import { unzipSync, strFromU8, zipSync } from "fflate";
|
|
3998
|
-
import { Hono as
|
|
4310
|
+
import { Hono as Hono16 } from "hono";
|
|
3999
4311
|
import {
|
|
4000
4312
|
installSkill,
|
|
4001
4313
|
listInstalledSkills,
|
|
@@ -4142,12 +4454,12 @@ function parseInstallNames(body) {
|
|
|
4142
4454
|
});
|
|
4143
4455
|
}
|
|
4144
4456
|
function skillLibraryRoutes() {
|
|
4145
|
-
const app2 = new
|
|
4457
|
+
const app2 = new Hono16();
|
|
4146
4458
|
app2.get("/", (c) => c.json(libraryResponse()));
|
|
4147
4459
|
return app2;
|
|
4148
4460
|
}
|
|
4149
4461
|
function agentSkillsRoutes(deps2) {
|
|
4150
|
-
const app2 = new
|
|
4462
|
+
const app2 = new Hono16();
|
|
4151
4463
|
const listResponse = async (projectId, agentId) => ({
|
|
4152
4464
|
skills: (await listInstalledSkills(deps2.config.root, projectId, agentId)).map(toMetadataItem)
|
|
4153
4465
|
});
|
|
@@ -4158,6 +4470,17 @@ function agentSkillsRoutes(deps2) {
|
|
|
4158
4470
|
await deps2.agentConfigService.requireExists(projectId, agentId);
|
|
4159
4471
|
return c.json(await listResponse(projectId, agentId));
|
|
4160
4472
|
});
|
|
4473
|
+
app2.post("/template-placeholder", async (c) => {
|
|
4474
|
+
const projectId = requireValidId(c, "projectId");
|
|
4475
|
+
const agentId = requireValidId(c, "agentId");
|
|
4476
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
4477
|
+
const view = await deps2.agentConfigService.insertTemplatePlaceholder(
|
|
4478
|
+
projectId,
|
|
4479
|
+
agentId,
|
|
4480
|
+
"skills"
|
|
4481
|
+
);
|
|
4482
|
+
return c.json(view.config.skills);
|
|
4483
|
+
});
|
|
4161
4484
|
app2.post("/", async (c) => {
|
|
4162
4485
|
const projectId = requireValidId(c, "projectId");
|
|
4163
4486
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -4195,11 +4518,11 @@ function agentSkillsRoutes(deps2) {
|
|
|
4195
4518
|
const skill = parseSkillArchive(archive);
|
|
4196
4519
|
const dir = path6.join(skillsDir(deps2.config.root, projectId, agentId), skill.name);
|
|
4197
4520
|
if (!overwrite) {
|
|
4198
|
-
const
|
|
4521
|
+
const installed2 = await fs7.access(path6.join(dir, "SKILL.md")).then(
|
|
4199
4522
|
() => true,
|
|
4200
4523
|
() => false
|
|
4201
4524
|
);
|
|
4202
|
-
if (
|
|
4525
|
+
if (installed2) {
|
|
4203
4526
|
throw new HttpError(409, "skill_exists", `Skill is already installed: ${skill.name}`);
|
|
4204
4527
|
}
|
|
4205
4528
|
}
|
|
@@ -4254,10 +4577,10 @@ function agentSkillsRoutes(deps2) {
|
|
|
4254
4577
|
|
|
4255
4578
|
// src/http/routes/agent-transfer.ts
|
|
4256
4579
|
import fs8 from "fs/promises";
|
|
4257
|
-
import { Hono as
|
|
4580
|
+
import { Hono as Hono17 } from "hono";
|
|
4258
4581
|
var MAX_ARCHIVE_BYTES2 = 14 * 1024 * 1024;
|
|
4259
4582
|
function agentTransferRoutes(deps2) {
|
|
4260
|
-
const app2 = new
|
|
4583
|
+
const app2 = new Hono17();
|
|
4261
4584
|
app2.get("/export", async (c) => {
|
|
4262
4585
|
const projectId = requireValidId(c, "projectId");
|
|
4263
4586
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -4296,7 +4619,7 @@ function agentTransferRoutes(deps2) {
|
|
|
4296
4619
|
}
|
|
4297
4620
|
|
|
4298
4621
|
// src/http/routes/agents.ts
|
|
4299
|
-
import { Hono as
|
|
4622
|
+
import { Hono as Hono18 } from "hono";
|
|
4300
4623
|
|
|
4301
4624
|
// src/http/settle.ts
|
|
4302
4625
|
async function settleWithin(promises, ms) {
|
|
@@ -4310,7 +4633,7 @@ async function settleWithin(promises, ms) {
|
|
|
4310
4633
|
// src/http/routes/agents.ts
|
|
4311
4634
|
var ACTIVITY_DAYS = 30;
|
|
4312
4635
|
function agentsRoutes(deps2) {
|
|
4313
|
-
const app2 = new
|
|
4636
|
+
const app2 = new Hono18();
|
|
4314
4637
|
app2.get("/", async (c) => {
|
|
4315
4638
|
const projectId = requireValidId(c, "projectId");
|
|
4316
4639
|
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
@@ -4377,9 +4700,9 @@ function agentsRoutes(deps2) {
|
|
|
4377
4700
|
import fs9 from "fs/promises";
|
|
4378
4701
|
import os from "os";
|
|
4379
4702
|
import path7 from "path";
|
|
4380
|
-
import { Hono as
|
|
4703
|
+
import { Hono as Hono19 } from "hono";
|
|
4381
4704
|
function dirsRoutes(deps2) {
|
|
4382
|
-
const app2 = new
|
|
4705
|
+
const app2 = new Hono19();
|
|
4383
4706
|
app2.get("/", async (c) => {
|
|
4384
4707
|
const projectId = requireValidId(c, "projectId");
|
|
4385
4708
|
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
@@ -4420,9 +4743,14 @@ function dirsRoutes(deps2) {
|
|
|
4420
4743
|
}
|
|
4421
4744
|
|
|
4422
4745
|
// src/http/routes/agent-config.ts
|
|
4423
|
-
import {
|
|
4746
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
4747
|
+
import { tmpdir } from "os";
|
|
4748
|
+
import { join } from "path";
|
|
4749
|
+
import { Hono as Hono20 } from "hono";
|
|
4750
|
+
import { McpToolProvider, resolveMCPServer } from "@prismshadow/penguin-core";
|
|
4751
|
+
var MCP_TEST_TIMEOUT_CAP_MS = 3e4;
|
|
4424
4752
|
function agentConfigRoutes(deps2) {
|
|
4425
|
-
const app2 = new
|
|
4753
|
+
const app2 = new Hono20();
|
|
4426
4754
|
app2.get("/", async (c) => {
|
|
4427
4755
|
const projectId = requireValidId(c, "projectId");
|
|
4428
4756
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -4454,6 +4782,62 @@ function agentConfigRoutes(deps2) {
|
|
|
4454
4782
|
activeSessionCount: deps2.manager.activeCountForAgent(projectId, agentId)
|
|
4455
4783
|
});
|
|
4456
4784
|
});
|
|
4785
|
+
app2.post("/mcp-test", async (c) => {
|
|
4786
|
+
const projectId = requireValidId(c, "projectId");
|
|
4787
|
+
const agentId = requireValidId(c, "agentId");
|
|
4788
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
4789
|
+
const body = await readJson(c);
|
|
4790
|
+
if (typeof body.name !== "string" || body.name.length === 0) {
|
|
4791
|
+
throw badRequest("name must be a non-empty string.");
|
|
4792
|
+
}
|
|
4793
|
+
if (body.config === null || typeof body.config !== "object" || Array.isArray(body.config)) {
|
|
4794
|
+
throw badRequest("config must be an object.");
|
|
4795
|
+
}
|
|
4796
|
+
const entry = { name: body.name, config: body.config };
|
|
4797
|
+
let resolved;
|
|
4798
|
+
try {
|
|
4799
|
+
resolved = resolveMCPServer(entry);
|
|
4800
|
+
} catch (err) {
|
|
4801
|
+
throw badRequest(err instanceof Error ? err.message : String(err));
|
|
4802
|
+
}
|
|
4803
|
+
if (resolved.connectTimeoutMs > MCP_TEST_TIMEOUT_CAP_MS) {
|
|
4804
|
+
entry.config = { ...entry.config, connectTimeoutMs: MCP_TEST_TIMEOUT_CAP_MS };
|
|
4805
|
+
}
|
|
4806
|
+
const warnings = [];
|
|
4807
|
+
const workspaceDir = await mkdtemp(join(tmpdir(), "penguin-mcp-test-"));
|
|
4808
|
+
const provider = new McpToolProvider([entry], {
|
|
4809
|
+
workspaceDir,
|
|
4810
|
+
warn: (m) => warnings.push(m)
|
|
4811
|
+
});
|
|
4812
|
+
const startedAt = Date.now();
|
|
4813
|
+
try {
|
|
4814
|
+
const tools = await provider.listTools();
|
|
4815
|
+
const latencyMs = Date.now() - startedAt;
|
|
4816
|
+
const outcome = provider.connectResults()[0];
|
|
4817
|
+
if (outcome === void 0 || outcome.status !== "completed") {
|
|
4818
|
+
return c.json({
|
|
4819
|
+
ok: false,
|
|
4820
|
+
error: outcome?.error ?? (warnings.length > 0 ? warnings.join("; ") : "connect failed"),
|
|
4821
|
+
latencyMs
|
|
4822
|
+
});
|
|
4823
|
+
}
|
|
4824
|
+
return c.json({
|
|
4825
|
+
ok: true,
|
|
4826
|
+
tools: tools.map((t) => t.name),
|
|
4827
|
+
latencyMs
|
|
4828
|
+
});
|
|
4829
|
+
} finally {
|
|
4830
|
+
await provider.close();
|
|
4831
|
+
void rm(workspaceDir, { recursive: true, force: true }).catch(() => void 0);
|
|
4832
|
+
}
|
|
4833
|
+
});
|
|
4834
|
+
app2.post("/kernel-update", async (c) => {
|
|
4835
|
+
const projectId = requireValidId(c, "projectId");
|
|
4836
|
+
const agentId = requireValidId(c, "agentId");
|
|
4837
|
+
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
4838
|
+
const result = await deps2.agentConfigService.kernelUpdate(projectId, agentId);
|
|
4839
|
+
return c.json(result);
|
|
4840
|
+
});
|
|
4457
4841
|
app2.post("/reset", async (c) => {
|
|
4458
4842
|
const projectId = requireValidId(c, "projectId");
|
|
4459
4843
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -4469,7 +4853,7 @@ function agentConfigRoutes(deps2) {
|
|
|
4469
4853
|
}
|
|
4470
4854
|
|
|
4471
4855
|
// src/http/routes/agent-traces.ts
|
|
4472
|
-
import { Hono as
|
|
4856
|
+
import { Hono as Hono21 } from "hono";
|
|
4473
4857
|
var MAX_TRACE_BYTES = 14 * 1024 * 1024;
|
|
4474
4858
|
var SESSION_CATEGORIES2 = [
|
|
4475
4859
|
"active",
|
|
@@ -4478,7 +4862,7 @@ var SESSION_CATEGORIES2 = [
|
|
|
4478
4862
|
"archived"
|
|
4479
4863
|
];
|
|
4480
4864
|
function agentTracesRoutes(deps2) {
|
|
4481
|
-
const app2 = new
|
|
4865
|
+
const app2 = new Hono21();
|
|
4482
4866
|
app2.get("/", async (c) => {
|
|
4483
4867
|
const projectId = requireValidId(c, "projectId");
|
|
4484
4868
|
const agentId = requireValidId(c, "agentId");
|
|
@@ -4556,10 +4940,10 @@ function agentTracesRoutes(deps2) {
|
|
|
4556
4940
|
}
|
|
4557
4941
|
|
|
4558
4942
|
// src/http/routes/usage.ts
|
|
4559
|
-
import { Hono as
|
|
4943
|
+
import { Hono as Hono22 } from "hono";
|
|
4560
4944
|
var GROUP_BYS = ["date", "agent", "model", "session"];
|
|
4561
4945
|
function usageRoutes(deps2) {
|
|
4562
|
-
const app2 = new
|
|
4946
|
+
const app2 = new Hono22();
|
|
4563
4947
|
app2.get("/", async (c) => {
|
|
4564
4948
|
const projectId = requireValidId(c, "projectId");
|
|
4565
4949
|
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
@@ -4614,7 +4998,7 @@ function usageRoutes(deps2) {
|
|
|
4614
4998
|
// src/http/routes/version.ts
|
|
4615
4999
|
import { spawn } from "child_process";
|
|
4616
5000
|
import { BUILD_DATE, VERSION } from "@prismshadow/penguin-core";
|
|
4617
|
-
import { Hono as
|
|
5001
|
+
import { Hono as Hono23 } from "hono";
|
|
4618
5002
|
var OUTPUT_CAP = 64 * 1024;
|
|
4619
5003
|
var UPDATE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
4620
5004
|
var REFUSAL_MARKERS = [
|
|
@@ -4680,7 +5064,7 @@ function runSelfUpdate(cliEntry) {
|
|
|
4680
5064
|
});
|
|
4681
5065
|
}
|
|
4682
5066
|
function versionRoutes(deps2) {
|
|
4683
|
-
const app2 = new
|
|
5067
|
+
const app2 = new Hono23();
|
|
4684
5068
|
let inflightRun = null;
|
|
4685
5069
|
app2.get("/", (c) => {
|
|
4686
5070
|
return c.json({ version: VERSION, buildDate: BUILD_DATE });
|
|
@@ -5419,13 +5803,14 @@ function compactUnavailable(why) {
|
|
|
5419
5803
|
};
|
|
5420
5804
|
return new HttpError(409, "nothing_to_compact", messages[why]);
|
|
5421
5805
|
}
|
|
5422
|
-
function createCoreSessionLoader(root, sources) {
|
|
5806
|
+
function createCoreSessionLoader(root, sources, opts = {}) {
|
|
5423
5807
|
return {
|
|
5424
5808
|
async load(row) {
|
|
5425
5809
|
const agent = await createAgent({
|
|
5426
5810
|
root,
|
|
5427
5811
|
projectId: row.projectId,
|
|
5428
|
-
agentId: row.agentId
|
|
5812
|
+
agentId: row.agentId,
|
|
5813
|
+
...opts.proxyEnv ? { proxyEnv: opts.proxyEnv } : {}
|
|
5429
5814
|
});
|
|
5430
5815
|
const located = await findLatestTraceFile(
|
|
5431
5816
|
tracesDir(root, row.projectId, row.agentId),
|
|
@@ -5570,6 +5955,20 @@ var SessionManager = class {
|
|
|
5570
5955
|
liveFragments(sessionId) {
|
|
5571
5956
|
return this.liveTail.fragments(sessionId);
|
|
5572
5957
|
}
|
|
5958
|
+
/**
|
|
5959
|
+
* The running Task's input messages as published at launch; empty when idle. The engine
|
|
5960
|
+
* writes these exact envelopes to the Trace only after the first run's bootstrap (MCP
|
|
5961
|
+
* connect + discovery), so GET /messages appends whichever of them the Trace read has
|
|
5962
|
+
* not caught up to yet — without this, a client rebuilding history during the connect
|
|
5963
|
+
* (the draft flow subscribes only after the input publish) loses the user's own message.
|
|
5964
|
+
*/
|
|
5965
|
+
pendingInputs(sessionId) {
|
|
5966
|
+
return this.entries.get(sessionId)?.pendingInputs ?? [];
|
|
5967
|
+
}
|
|
5968
|
+
/** The running Task's streamed bootstrap records (see RuntimeEntry.pendingBootstrap); empty when idle. */
|
|
5969
|
+
pendingBootstrap(sessionId) {
|
|
5970
|
+
return this.entries.get(sessionId)?.pendingBootstrap ?? [];
|
|
5971
|
+
}
|
|
5573
5972
|
/** Number of Sessions for this Agent that are currently running / compacting. */
|
|
5574
5973
|
activeCountForAgent(projectId, agentId) {
|
|
5575
5974
|
let n = 0;
|
|
@@ -5593,6 +5992,8 @@ var SessionManager = class {
|
|
|
5593
5992
|
running: null,
|
|
5594
5993
|
generation: this.generationOf(row.projectId, row.agentId),
|
|
5595
5994
|
followUps: [],
|
|
5995
|
+
pendingInputs: [],
|
|
5996
|
+
pendingBootstrap: [],
|
|
5596
5997
|
pendingSteering: [],
|
|
5597
5998
|
lastActivityMs: Date.now()
|
|
5598
5999
|
});
|
|
@@ -5696,6 +6097,8 @@ var SessionManager = class {
|
|
|
5696
6097
|
entry.status = "running";
|
|
5697
6098
|
entry.abort = ac;
|
|
5698
6099
|
entry.lastActivityMs = Date.now();
|
|
6100
|
+
entry.pendingInputs = [...entry.pendingInputs, ...args.input];
|
|
6101
|
+
entry.pendingBootstrap = [];
|
|
5699
6102
|
this.publishState(entry, "running");
|
|
5700
6103
|
const approve = makeApprove({
|
|
5701
6104
|
getMode: () => this.deps.sessions.findById(entry.sessionId)?.approvalMode ?? "always-ask",
|
|
@@ -5810,6 +6213,8 @@ var SessionManager = class {
|
|
|
5810
6213
|
entry.status = "running";
|
|
5811
6214
|
entry.abort = ac;
|
|
5812
6215
|
entry.lastActivityMs = Date.now();
|
|
6216
|
+
entry.pendingInputs = [...entry.pendingInputs, ...input];
|
|
6217
|
+
entry.pendingBootstrap = [];
|
|
5813
6218
|
for (const msg of input) channel.publish(msg);
|
|
5814
6219
|
this.publishState(entry, "running");
|
|
5815
6220
|
const approve = makeApprove({
|
|
@@ -5933,6 +6338,34 @@ var SessionManager = class {
|
|
|
5933
6338
|
entry.abort.abort();
|
|
5934
6339
|
return true;
|
|
5935
6340
|
}
|
|
6341
|
+
/**
|
|
6342
|
+
* Background command processes of a LOADED session (empty when the entry is not in
|
|
6343
|
+
* the active table — a resumed entry starts with a fresh environment and can only
|
|
6344
|
+
* ever report an empty list, so nothing is resurrected just to answer a poll).
|
|
6345
|
+
*/
|
|
6346
|
+
listProcesses(sessionId) {
|
|
6347
|
+
return this.entries.get(sessionId)?.session.listBackgroundCommands?.() ?? [];
|
|
6348
|
+
}
|
|
6349
|
+
/** Kills one background command process of a loaded session; false when the session isn't loaded or the id is unknown. */
|
|
6350
|
+
killProcess(sessionId, processId) {
|
|
6351
|
+
const entry = this.entries.get(sessionId);
|
|
6352
|
+
if (!entry) return false;
|
|
6353
|
+
const killed = entry.session.killBackgroundCommand?.(processId) ?? false;
|
|
6354
|
+
if (killed) entry.lastActivityMs = Date.now();
|
|
6355
|
+
return killed;
|
|
6356
|
+
}
|
|
6357
|
+
/**
|
|
6358
|
+
* Disposes a just-removed entry's runtime — after its in-flight drive (if any)
|
|
6359
|
+
* settles, so interrupt cleanup never races a dying environment. Deleting a Session /
|
|
6360
|
+
* Agent / Project is the one intent that must also end the background processes the
|
|
6361
|
+
* conversation started (a dev server surviving its deleted conversation is
|
|
6362
|
+
* unreachable from every UI, running forever).
|
|
6363
|
+
*/
|
|
6364
|
+
disposeRemoved(entry) {
|
|
6365
|
+
const dispose = () => entry.session.dispose?.();
|
|
6366
|
+
if (entry.running) void entry.running.then(dispose, dispose);
|
|
6367
|
+
else dispose();
|
|
6368
|
+
}
|
|
5936
6369
|
/**
|
|
5937
6370
|
* Before deleting a Project, converge all its active runs and clear them out of the
|
|
5938
6371
|
* active table. Returns the in-flight drive Promises of the affected entries: the
|
|
@@ -5947,6 +6380,7 @@ var SessionManager = class {
|
|
|
5947
6380
|
entry.abort?.abort();
|
|
5948
6381
|
if (entry.running) runnings.push(entry.running);
|
|
5949
6382
|
this.entries.delete(key);
|
|
6383
|
+
this.disposeRemoved(entry);
|
|
5950
6384
|
}
|
|
5951
6385
|
return runnings;
|
|
5952
6386
|
}
|
|
@@ -5968,6 +6402,7 @@ var SessionManager = class {
|
|
|
5968
6402
|
entry.abort?.abort();
|
|
5969
6403
|
if (entry.running) runnings.push(entry.running);
|
|
5970
6404
|
this.entries.delete(key);
|
|
6405
|
+
this.disposeRemoved(entry);
|
|
5971
6406
|
}
|
|
5972
6407
|
return runnings;
|
|
5973
6408
|
}
|
|
@@ -5992,6 +6427,7 @@ var SessionManager = class {
|
|
|
5992
6427
|
entry.approvals.denyAll();
|
|
5993
6428
|
entry.abort?.abort();
|
|
5994
6429
|
this.entries.delete(sessionId);
|
|
6430
|
+
this.disposeRemoved(entry);
|
|
5995
6431
|
return entry.running ? [entry.running] : [];
|
|
5996
6432
|
}
|
|
5997
6433
|
endSessionDeletion(sessionId) {
|
|
@@ -6027,6 +6463,7 @@ var SessionManager = class {
|
|
|
6027
6463
|
for (const [key, entry] of this.entries) {
|
|
6028
6464
|
if (entry.status !== "idle" || entry.approvals.size !== 0 || entry.running !== null) continue;
|
|
6029
6465
|
if (entry.followUps.length > 0) continue;
|
|
6466
|
+
if (entry.session.listBackgroundCommands?.().some((p) => p.running)) continue;
|
|
6030
6467
|
if (now - entry.lastActivityMs <= idleMs) continue;
|
|
6031
6468
|
this.entries.delete(key);
|
|
6032
6469
|
}
|
|
@@ -6119,6 +6556,8 @@ var SessionManager = class {
|
|
|
6119
6556
|
running: null,
|
|
6120
6557
|
generation,
|
|
6121
6558
|
followUps: [],
|
|
6559
|
+
pendingInputs: [],
|
|
6560
|
+
pendingBootstrap: [],
|
|
6122
6561
|
pendingSteering: [],
|
|
6123
6562
|
lastActivityMs: Date.now()
|
|
6124
6563
|
};
|
|
@@ -6202,6 +6641,16 @@ var SessionManager = class {
|
|
|
6202
6641
|
}
|
|
6203
6642
|
}
|
|
6204
6643
|
}
|
|
6644
|
+
if (!msg.origin || msg.origin.length === 0) {
|
|
6645
|
+
const bt = msg.payload.type;
|
|
6646
|
+
if (bt === "mcp_connect_begin" || bt === "mcp_connect_end" || bt === "tool_list_ready") {
|
|
6647
|
+
entry.pendingBootstrap.push(msg);
|
|
6648
|
+
}
|
|
6649
|
+
if (bt === "request_begin") {
|
|
6650
|
+
entry.pendingInputs = [];
|
|
6651
|
+
entry.pendingBootstrap = [];
|
|
6652
|
+
}
|
|
6653
|
+
}
|
|
6205
6654
|
this.liveTail.observe(entry.sessionId, msg);
|
|
6206
6655
|
this.deps.channels.get(entry.sessionId).publish(msg);
|
|
6207
6656
|
watcher?.observe(msg);
|
|
@@ -6849,6 +7298,7 @@ var AdminService = class {
|
|
|
6849
7298
|
}
|
|
6850
7299
|
this.deps.users.updatePassword(userId, await hashPassword(password), true);
|
|
6851
7300
|
this.deps.authSessions.deleteByUser(userId);
|
|
7301
|
+
this.deps.onPasswordChanged?.(userId);
|
|
6852
7302
|
}
|
|
6853
7303
|
/** Delete user: the built-in admin cannot be deleted; owned Projects (including data directories) are deleted along with it. */
|
|
6854
7304
|
async deleteUser(userId) {
|
|
@@ -6900,36 +7350,34 @@ var DesktopService = class {
|
|
|
6900
7350
|
}
|
|
6901
7351
|
};
|
|
6902
7352
|
|
|
6903
|
-
// src/http/routes/desktop.ts
|
|
6904
|
-
import { Hono as Hono21 } from "hono";
|
|
6905
|
-
var SHUTDOWN_DELAY_MS = 50;
|
|
6906
|
-
function desktopRoutes(deps2) {
|
|
6907
|
-
const app2 = new Hono21();
|
|
6908
|
-
app2.post("/shutdown", (c) => {
|
|
6909
|
-
const desktop = deps2.desktop;
|
|
6910
|
-
if (!desktop) throw new HttpError(404, "not_found", "Desktop mode is not enabled.");
|
|
6911
|
-
const header = c.req.header("authorization") ?? "";
|
|
6912
|
-
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
6913
|
-
if (token === "" || !desktop.verifyToken(token)) {
|
|
6914
|
-
throw new HttpError(401, "unauthorized", "Invalid desktop token.");
|
|
6915
|
-
}
|
|
6916
|
-
setTimeout(() => desktop.requestShutdown(), SHUTDOWN_DELAY_MS).unref();
|
|
6917
|
-
return c.body(null, 202);
|
|
6918
|
-
});
|
|
6919
|
-
return app2;
|
|
6920
|
-
}
|
|
6921
|
-
|
|
6922
7353
|
// src/services/agent-config-service.ts
|
|
6923
7354
|
import fs13 from "fs/promises";
|
|
6924
7355
|
import { parseDocument, parse as parseYaml } from "yaml";
|
|
6925
7356
|
import {
|
|
7357
|
+
DEFAULT_MEMORY_PROMPT,
|
|
7358
|
+
DEFAULT_MEMORY_WORKSPACE_PROMPT,
|
|
7359
|
+
DEFAULT_SCHEDULES_PROMPT,
|
|
7360
|
+
DEFAULT_SKILLS_PROMPT,
|
|
7361
|
+
DEFAULT_VAULT_PROMPT,
|
|
7362
|
+
KERNEL_VERSION,
|
|
7363
|
+
LEGACY_SKILLS_SECTION,
|
|
7364
|
+
LEGACY_VAULT_SECTION,
|
|
6926
7365
|
agentsMdPath,
|
|
7366
|
+
applyKernelUpdate,
|
|
7367
|
+
isKernelOutdated,
|
|
6927
7368
|
agentStateDir,
|
|
6928
7369
|
agentStateVersion,
|
|
7370
|
+
hasSchedulesPlaceholder,
|
|
7371
|
+
hasSkillsPlaceholder,
|
|
7372
|
+
hasVaultPlaceholder,
|
|
7373
|
+
insertSchedulesPlaceholder,
|
|
7374
|
+
insertSkillsPlaceholder,
|
|
7375
|
+
insertVaultPlaceholder,
|
|
6929
7376
|
VAULT_VALUE_MAX_LENGTH,
|
|
6930
7377
|
isValidVaultKey,
|
|
6931
7378
|
loadAgentVault,
|
|
6932
7379
|
resetSystemConfigToDefaults,
|
|
7380
|
+
resolveMCPServer as resolveMCPServer2,
|
|
6933
7381
|
saveAgentVault,
|
|
6934
7382
|
systemConfigPath
|
|
6935
7383
|
} from "@prismshadow/penguin-core";
|
|
@@ -7530,10 +7978,13 @@ var AgentConfigService = class {
|
|
|
7530
7978
|
...typeof parsed.name === "string" ? { name: parsed.name } : {},
|
|
7531
7979
|
...typeof parsed.description === "string" ? { description: parsed.description } : {},
|
|
7532
7980
|
toolCount: countOf(tools.builtin) + countOf(tools.mcpServers),
|
|
7533
|
-
version: agentStateVersion({ version: parsed.version })
|
|
7981
|
+
version: agentStateVersion({ version: parsed.version }),
|
|
7982
|
+
kernelOutdated: isKernelOutdated(
|
|
7983
|
+
typeof parsed.kernel_version === "string" ? parsed.kernel_version : null
|
|
7984
|
+
)
|
|
7534
7985
|
};
|
|
7535
7986
|
} catch {
|
|
7536
|
-
return { toolCount: 0, version: 1 };
|
|
7987
|
+
return { toolCount: 0, version: 1, kernelOutdated: true };
|
|
7537
7988
|
}
|
|
7538
7989
|
}
|
|
7539
7990
|
/** Structured config view (matching the edit form's shape) + raw text + AGENTS.md + State path. */
|
|
@@ -7544,6 +7995,10 @@ var AgentConfigService = class {
|
|
|
7544
7995
|
const parsed = asRecord(parseYaml(systemConfigYaml));
|
|
7545
7996
|
const model = asRecord(parsed.model);
|
|
7546
7997
|
const compaction = asRecord(parsed.compaction);
|
|
7998
|
+
const memory = asRecord(parsed.memory);
|
|
7999
|
+
const vault = asRecord(parsed.vault);
|
|
8000
|
+
const skills = asRecord(parsed.skills);
|
|
8001
|
+
const schedules = asRecord(parsed.schedules);
|
|
7547
8002
|
const tools = asRecord(parsed.tools);
|
|
7548
8003
|
let agentsMd = "";
|
|
7549
8004
|
try {
|
|
@@ -7561,14 +8016,45 @@ var AgentConfigService = class {
|
|
|
7561
8016
|
...compaction.mode === "summarize" || compaction.mode === "discard" ? { mode: compaction.mode } : {},
|
|
7562
8017
|
...typeof compaction.prompt === "string" ? { prompt: compaction.prompt } : {}
|
|
7563
8018
|
};
|
|
8019
|
+
const memoryDto = {
|
|
8020
|
+
enabled: memory.enabled !== false,
|
|
8021
|
+
prompt: typeof memory.prompt === "string" ? memory.prompt : DEFAULT_MEMORY_PROMPT,
|
|
8022
|
+
workspacePrompt: typeof memory.workspace_prompt === "string" ? memory.workspace_prompt : DEFAULT_MEMORY_WORKSPACE_PROMPT
|
|
8023
|
+
};
|
|
8024
|
+
const systemPrompt = typeof parsed.system_prompt === "string" ? parsed.system_prompt : "";
|
|
8025
|
+
const vaultDto = {
|
|
8026
|
+
enabled: vault.enabled !== false,
|
|
8027
|
+
prompt: typeof vault.prompt === "string" ? vault.prompt : DEFAULT_VAULT_PROMPT,
|
|
8028
|
+
templateHasPlaceholder: hasVaultPlaceholder(systemPrompt),
|
|
8029
|
+
legacySectionPresent: systemPrompt.includes(LEGACY_VAULT_SECTION)
|
|
8030
|
+
};
|
|
8031
|
+
const skillsDto = {
|
|
8032
|
+
enabled: skills.enabled !== false,
|
|
8033
|
+
prompt: typeof skills.prompt === "string" ? skills.prompt : DEFAULT_SKILLS_PROMPT,
|
|
8034
|
+
templateHasPlaceholder: hasSkillsPlaceholder(systemPrompt),
|
|
8035
|
+
legacySectionPresent: systemPrompt.includes(LEGACY_SKILLS_SECTION)
|
|
8036
|
+
};
|
|
8037
|
+
const schedulesDto = {
|
|
8038
|
+
enabled: schedules.enabled !== false,
|
|
8039
|
+
prompt: typeof schedules.prompt === "string" ? schedules.prompt : DEFAULT_SCHEDULES_PROMPT,
|
|
8040
|
+
templateHasPlaceholder: hasSchedulesPlaceholder(systemPrompt)
|
|
8041
|
+
};
|
|
8042
|
+
const kernelVersion = typeof parsed.kernel_version === "string" ? parsed.kernel_version : null;
|
|
7564
8043
|
const config2 = {
|
|
7565
8044
|
...typeof parsed.name === "string" ? { name: parsed.name } : {},
|
|
7566
8045
|
...typeof parsed.description === "string" ? { description: parsed.description } : {},
|
|
7567
8046
|
version: agentStateVersion({ version: parsed.version }),
|
|
7568
|
-
|
|
8047
|
+
kernelVersion,
|
|
8048
|
+
kernelLatest: KERNEL_VERSION,
|
|
8049
|
+
kernelOutdated: isKernelOutdated(kernelVersion),
|
|
8050
|
+
systemPrompt,
|
|
7569
8051
|
...typeof parsed.max_turns === "number" ? { maxTurns: parsed.max_turns } : {},
|
|
7570
8052
|
...Object.keys(modelDto).length > 0 ? { model: modelDto } : {},
|
|
7571
8053
|
...Object.keys(compactionDto).length > 0 ? { compaction: compactionDto } : {},
|
|
8054
|
+
memory: memoryDto,
|
|
8055
|
+
vault: vaultDto,
|
|
8056
|
+
skills: skillsDto,
|
|
8057
|
+
schedules: schedulesDto,
|
|
7572
8058
|
toolsBuiltin: Array.isArray(tools.builtin) ? tools.builtin : [],
|
|
7573
8059
|
mcpServers: Array.isArray(tools.mcpServers) ? tools.mcpServers : []
|
|
7574
8060
|
};
|
|
@@ -7605,12 +8091,22 @@ var AgentConfigService = class {
|
|
|
7605
8091
|
await this.requireExists(projectId, agentId);
|
|
7606
8092
|
await resetSystemConfigToDefaults(this.root, projectId, agentId);
|
|
7607
8093
|
}
|
|
8094
|
+
/**
|
|
8095
|
+
* Smart-merges the config up to the current defaults generation (core's applyKernelUpdate):
|
|
8096
|
+
* leaves still carrying a recorded generation's default follow the new defaults, user
|
|
8097
|
+
* customizations are kept and reported; the config is stamped with the new kernel version.
|
|
8098
|
+
* The destructive full-refresh alternative stays resetConfig.
|
|
8099
|
+
*/
|
|
8100
|
+
async kernelUpdate(projectId, agentId) {
|
|
8101
|
+
await this.requireExists(projectId, agentId);
|
|
8102
|
+
return applyKernelUpdate(this.root, projectId, agentId);
|
|
8103
|
+
}
|
|
7608
8104
|
async applyConfigUpdate(projectId, agentId, config2) {
|
|
7609
8105
|
const cfg = config2;
|
|
7610
8106
|
const yamlPath = systemConfigPath(this.root, projectId, agentId);
|
|
7611
8107
|
const doc = parseDocument(await fs13.readFile(yamlPath, "utf8"));
|
|
7612
|
-
const setIfProvided = (
|
|
7613
|
-
if (value !== void 0) doc.setIn(
|
|
8108
|
+
const setIfProvided = (path20, value) => {
|
|
8109
|
+
if (value !== void 0) doc.setIn(path20, value);
|
|
7614
8110
|
};
|
|
7615
8111
|
setIfProvided(["name"], optionalString(cfg, "name", { maxLen: 100, label: "name" }));
|
|
7616
8112
|
setIfProvided(
|
|
@@ -7653,6 +8149,27 @@ var AgentConfigService = class {
|
|
|
7653
8149
|
setIfProvided(["compaction", "mode"], optionalEnum(compaction, "mode", COMPACTION_MODES));
|
|
7654
8150
|
setIfProvided(["compaction", "prompt"], optionalString(compaction, "prompt"));
|
|
7655
8151
|
}
|
|
8152
|
+
if (cfg.memory !== void 0) {
|
|
8153
|
+
const memory = asRecord(cfg.memory);
|
|
8154
|
+
setIfProvided(["memory", "enabled"], optionalBoolean(memory, "enabled"));
|
|
8155
|
+
setIfProvided(["memory", "prompt"], optionalString(memory, "prompt"));
|
|
8156
|
+
setIfProvided(["memory", "workspace_prompt"], optionalString(memory, "workspacePrompt"));
|
|
8157
|
+
}
|
|
8158
|
+
if (cfg.vault !== void 0) {
|
|
8159
|
+
const vault = asRecord(cfg.vault);
|
|
8160
|
+
setIfProvided(["vault", "enabled"], optionalBoolean(vault, "enabled"));
|
|
8161
|
+
setIfProvided(["vault", "prompt"], optionalString(vault, "prompt"));
|
|
8162
|
+
}
|
|
8163
|
+
if (cfg.skills !== void 0) {
|
|
8164
|
+
const skills = asRecord(cfg.skills);
|
|
8165
|
+
setIfProvided(["skills", "enabled"], optionalBoolean(skills, "enabled"));
|
|
8166
|
+
setIfProvided(["skills", "prompt"], optionalString(skills, "prompt"));
|
|
8167
|
+
}
|
|
8168
|
+
if (cfg.schedules !== void 0) {
|
|
8169
|
+
const schedules = asRecord(cfg.schedules);
|
|
8170
|
+
setIfProvided(["schedules", "enabled"], optionalBoolean(schedules, "enabled"));
|
|
8171
|
+
setIfProvided(["schedules", "prompt"], optionalString(schedules, "prompt"));
|
|
8172
|
+
}
|
|
7656
8173
|
if (cfg.toolsBuiltin !== void 0) {
|
|
7657
8174
|
doc.setIn(["tools", "builtin"], validateToolsBuiltin(cfg.toolsBuiltin));
|
|
7658
8175
|
}
|
|
@@ -7661,6 +8178,26 @@ var AgentConfigService = class {
|
|
|
7661
8178
|
}
|
|
7662
8179
|
await fs13.writeFile(yamlPath, doc.toString(), "utf8");
|
|
7663
8180
|
}
|
|
8181
|
+
/**
|
|
8182
|
+
* Inserts a feature's section placeholder into the Agent's prompt template — the explicit
|
|
8183
|
+
* adoption path mirroring the Memory tab's endpoint; nothing ever inserts automatically.
|
|
8184
|
+
* For skills/vault the insert is migration-first (core's helpers): a stored template still
|
|
8185
|
+
* carrying the legacy hardcoded section verbatim gets it replaced in place by the
|
|
8186
|
+
* placeholder, otherwise the placeholder is inserted before `# Environment`. Idempotent;
|
|
8187
|
+
* returns the refreshed config view (the route picks out the feature's DTO).
|
|
8188
|
+
*/
|
|
8189
|
+
async insertTemplatePlaceholder(projectId, agentId, feature) {
|
|
8190
|
+
const view = await this.getConfig(projectId, agentId);
|
|
8191
|
+
const insert = {
|
|
8192
|
+
vault: insertVaultPlaceholder,
|
|
8193
|
+
skills: insertSkillsPlaceholder,
|
|
8194
|
+
schedules: insertSchedulesPlaceholder
|
|
8195
|
+
}[feature];
|
|
8196
|
+
const next = insert(view.config.systemPrompt);
|
|
8197
|
+
if (next === view.config.systemPrompt) return view;
|
|
8198
|
+
await this.updateConfig(projectId, agentId, { config: { systemPrompt: next } });
|
|
8199
|
+
return this.getConfig(projectId, agentId);
|
|
8200
|
+
}
|
|
7664
8201
|
/** Read the Agent vault (agent_state/.vault.toml): values are always masked, plaintext is never sent to the client. */
|
|
7665
8202
|
async getVault(projectId, agentId) {
|
|
7666
8203
|
await this.requireExists(projectId, agentId);
|
|
@@ -7743,6 +8280,7 @@ function validateToolsBuiltin(value) {
|
|
|
7743
8280
|
}
|
|
7744
8281
|
function validateMcpServers(value) {
|
|
7745
8282
|
if (!Array.isArray(value)) throw badRequest("mcpServers must be an array.");
|
|
8283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7746
8284
|
return value.map((item, i) => {
|
|
7747
8285
|
const s = asRecord(item);
|
|
7748
8286
|
if (typeof s.name !== "string" || s.name.length === 0) {
|
|
@@ -7751,13 +8289,273 @@ function validateMcpServers(value) {
|
|
|
7751
8289
|
if (s.config === null || typeof s.config !== "object" || Array.isArray(s.config)) {
|
|
7752
8290
|
throw badRequest(`mcpServers[${i}].config must be an object.`);
|
|
7753
8291
|
}
|
|
8292
|
+
try {
|
|
8293
|
+
resolveMCPServer2(s);
|
|
8294
|
+
} catch (err) {
|
|
8295
|
+
throw badRequest(`mcpServers[${i}]: ${err instanceof Error ? err.message : String(err)}`);
|
|
8296
|
+
}
|
|
8297
|
+
if (seen.has(s.name)) {
|
|
8298
|
+
throw badRequest(`mcpServers[${i}]: duplicate server name "${s.name}".`);
|
|
8299
|
+
}
|
|
8300
|
+
seen.add(s.name);
|
|
7754
8301
|
return s;
|
|
7755
8302
|
});
|
|
7756
8303
|
}
|
|
7757
8304
|
|
|
7758
|
-
// src/services/
|
|
8305
|
+
// src/services/memory-service.ts
|
|
7759
8306
|
import fs14 from "fs/promises";
|
|
7760
8307
|
import path11 from "path";
|
|
8308
|
+
import {
|
|
8309
|
+
MEMORY_INDEX_FILENAME,
|
|
8310
|
+
USER_SCOPE_KEY,
|
|
8311
|
+
hasMemoryPlaceholder,
|
|
8312
|
+
insertMemoryPlaceholder,
|
|
8313
|
+
memoryDir,
|
|
8314
|
+
memoryScopeDir,
|
|
8315
|
+
parseMemoryFrontmatter,
|
|
8316
|
+
readWorkspaceMarker
|
|
8317
|
+
} from "@prismshadow/penguin-core";
|
|
8318
|
+
var SCOPE_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
8319
|
+
function isTopicFileName(name) {
|
|
8320
|
+
return name.length > 0 && !name.startsWith(".") && !/[/\\]/.test(name) && name.toLowerCase().endsWith(".md") && name.toLowerCase() !== MEMORY_INDEX_FILENAME.toLowerCase();
|
|
8321
|
+
}
|
|
8322
|
+
var MemoryService = class {
|
|
8323
|
+
constructor(root, agentConfigService) {
|
|
8324
|
+
this.root = root;
|
|
8325
|
+
this.agentConfigService = agentConfigService;
|
|
8326
|
+
}
|
|
8327
|
+
root;
|
|
8328
|
+
agentConfigService;
|
|
8329
|
+
/** The tab's landing payload: the Agent-level switch and one entry per scope directory. */
|
|
8330
|
+
async overview(projectId, agentId) {
|
|
8331
|
+
const view = await this.agentConfigService.getConfig(projectId, agentId);
|
|
8332
|
+
return {
|
|
8333
|
+
enabled: view.config.memory.enabled,
|
|
8334
|
+
templateHasMemory: hasMemoryPlaceholder(view.config.systemPrompt),
|
|
8335
|
+
memoryDir: memoryDir(this.root, projectId, agentId),
|
|
8336
|
+
scopes: await this.listScopes(projectId, agentId)
|
|
8337
|
+
};
|
|
8338
|
+
}
|
|
8339
|
+
/**
|
|
8340
|
+
* Inserts the `{{MEMORY}}` placeholder into the Agent's prompt template — the explicit
|
|
8341
|
+
* adoption path for an Agent created before Memory shipped; nothing inserts automatically.
|
|
8342
|
+
* Idempotent: a template that already carries it is left as it is (the refreshed overview
|
|
8343
|
+
* reports `templateHasMemory` either way).
|
|
8344
|
+
*/
|
|
8345
|
+
async insertTemplatePlaceholder(projectId, agentId) {
|
|
8346
|
+
const view = await this.agentConfigService.getConfig(projectId, agentId);
|
|
8347
|
+
const next = insertMemoryPlaceholder(view.config.systemPrompt);
|
|
8348
|
+
if (next !== view.config.systemPrompt) {
|
|
8349
|
+
await this.agentConfigService.updateConfig(projectId, agentId, {
|
|
8350
|
+
config: { systemPrompt: next }
|
|
8351
|
+
});
|
|
8352
|
+
}
|
|
8353
|
+
return this.overview(projectId, agentId);
|
|
8354
|
+
}
|
|
8355
|
+
/**
|
|
8356
|
+
* The scope directories under `memory/`: the User scope first — always listed, even before it
|
|
8357
|
+
* exists on disk, so a memory can be filed there without waiting for a Session to create it —
|
|
8358
|
+
* then the Workspaces, newest activity first (one with no topic file yet sorts last, by key).
|
|
8359
|
+
*/
|
|
8360
|
+
async listScopes(projectId, agentId) {
|
|
8361
|
+
const base = memoryDir(this.root, projectId, agentId);
|
|
8362
|
+
let entries = [];
|
|
8363
|
+
try {
|
|
8364
|
+
entries = (await fs14.readdir(base, { withFileTypes: true })).filter((e) => e.isDirectory() && e.name !== USER_SCOPE_KEY).map((e) => e.name);
|
|
8365
|
+
} catch {
|
|
8366
|
+
}
|
|
8367
|
+
const workspaces = await Promise.all(
|
|
8368
|
+
entries.map((key) => this.scopeInfo(path11.join(base, key), key, "workspace"))
|
|
8369
|
+
);
|
|
8370
|
+
workspaces.sort((a, b) => {
|
|
8371
|
+
if (a.updatedAt && b.updatedAt) return b.updatedAt.localeCompare(a.updatedAt);
|
|
8372
|
+
if (a.updatedAt) return -1;
|
|
8373
|
+
if (b.updatedAt) return 1;
|
|
8374
|
+
return a.scopeKey.localeCompare(b.scopeKey);
|
|
8375
|
+
});
|
|
8376
|
+
const userScope = await this.scopeInfo(path11.join(base, USER_SCOPE_KEY), USER_SCOPE_KEY, "user");
|
|
8377
|
+
return [userScope, ...workspaces];
|
|
8378
|
+
}
|
|
8379
|
+
async scopeInfo(dir, key, kind) {
|
|
8380
|
+
const files = await this.topicFileNames(dir);
|
|
8381
|
+
let latest = 0;
|
|
8382
|
+
for (const name of files) {
|
|
8383
|
+
try {
|
|
8384
|
+
const stat = await fs14.stat(path11.join(dir, name));
|
|
8385
|
+
latest = Math.max(latest, stat.mtimeMs);
|
|
8386
|
+
} catch {
|
|
8387
|
+
}
|
|
8388
|
+
}
|
|
8389
|
+
const workspacePath = kind === "workspace" ? await readWorkspaceMarker(dir) : void 0;
|
|
8390
|
+
return {
|
|
8391
|
+
scopeKey: key,
|
|
8392
|
+
kind,
|
|
8393
|
+
...workspacePath !== void 0 ? { workspacePath } : {},
|
|
8394
|
+
fileCount: files.length,
|
|
8395
|
+
...latest > 0 ? { updatedAt: new Date(latest).toISOString() } : {}
|
|
8396
|
+
};
|
|
8397
|
+
}
|
|
8398
|
+
/** Topic files of one scope: regular Markdown files only — the index, the `.workspace` marker, stray directories and symlinks (dirents that are not regular files) all stay out. */
|
|
8399
|
+
async topicFileNames(dir) {
|
|
8400
|
+
try {
|
|
8401
|
+
return (await fs14.readdir(dir, { withFileTypes: true })).filter((e) => e.isFile() && isTopicFileName(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
|
|
8402
|
+
} catch {
|
|
8403
|
+
return [];
|
|
8404
|
+
}
|
|
8405
|
+
}
|
|
8406
|
+
async listFiles(projectId, agentId, scopeKey) {
|
|
8407
|
+
const dir = await this.requireScopeDir(projectId, agentId, scopeKey);
|
|
8408
|
+
const names = await this.topicFileNames(dir);
|
|
8409
|
+
const files = [];
|
|
8410
|
+
for (const name of names) {
|
|
8411
|
+
const info = await this.fileInfo(dir, name);
|
|
8412
|
+
if (info) files.push(info);
|
|
8413
|
+
}
|
|
8414
|
+
return { scopeKey, files };
|
|
8415
|
+
}
|
|
8416
|
+
async readFile(projectId, agentId, scopeKey, fileName) {
|
|
8417
|
+
const dir = await this.requireScopeDir(projectId, agentId, scopeKey);
|
|
8418
|
+
const target = this.resolveFile(dir, fileName);
|
|
8419
|
+
try {
|
|
8420
|
+
const stat = await fs14.lstat(target);
|
|
8421
|
+
if (!stat.isFile()) throw new Error("not a regular file");
|
|
8422
|
+
const content = await fs14.readFile(target, "utf8");
|
|
8423
|
+
return {
|
|
8424
|
+
scopeKey,
|
|
8425
|
+
file: this.describe(fileName, content, stat.size, stat.mtime),
|
|
8426
|
+
content
|
|
8427
|
+
};
|
|
8428
|
+
} catch {
|
|
8429
|
+
throw new HttpError(404, "memory_file_not_found", `Memory file not found: ${fileName}`);
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8432
|
+
/**
|
|
8433
|
+
* Deletes a topic file and mechanically drops its lines from the scope's `MEMORY.md`: any
|
|
8434
|
+
* line whose Markdown link target is exactly this file (`](<file>)`) goes; a mention of the
|
|
8435
|
+
* name in ordinary prose, or a link to a different file, survives. This is the only write the
|
|
8436
|
+
* API performs on Memory content — everything else is the model's, through a chat Session.
|
|
8437
|
+
*/
|
|
8438
|
+
async deleteFile(projectId, agentId, scopeKey, fileName) {
|
|
8439
|
+
const dir = await this.requireScopeDir(projectId, agentId, scopeKey);
|
|
8440
|
+
const target = this.resolveFile(dir, fileName);
|
|
8441
|
+
try {
|
|
8442
|
+
await fs14.unlink(target);
|
|
8443
|
+
} catch {
|
|
8444
|
+
throw new HttpError(404, "memory_file_not_found", `Memory file not found: ${fileName}`);
|
|
8445
|
+
}
|
|
8446
|
+
await this.pruneIndexLines(dir, fileName);
|
|
8447
|
+
}
|
|
8448
|
+
/** Removes index lines linking to a deleted file. Best-effort: a missing or unreadable index is left alone. */
|
|
8449
|
+
async pruneIndexLines(dir, fileName) {
|
|
8450
|
+
const indexPath = path11.join(dir, MEMORY_INDEX_FILENAME);
|
|
8451
|
+
let content;
|
|
8452
|
+
try {
|
|
8453
|
+
content = await fs14.readFile(indexPath, "utf8");
|
|
8454
|
+
} catch {
|
|
8455
|
+
return;
|
|
8456
|
+
}
|
|
8457
|
+
const escaped = fileName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8458
|
+
const link = new RegExp(`\\]\\(\\s*<?(?:\\./)?${escaped}>?\\s*(?:"[^"]*"\\s*)?\\)`);
|
|
8459
|
+
const lines = content.split("\n");
|
|
8460
|
+
const kept = lines.filter((line) => !link.test(line));
|
|
8461
|
+
if (kept.length === lines.length) return;
|
|
8462
|
+
await fs14.writeFile(indexPath, kept.join("\n"), "utf8");
|
|
8463
|
+
}
|
|
8464
|
+
async fileInfo(dir, name) {
|
|
8465
|
+
try {
|
|
8466
|
+
const [content, stat] = await Promise.all([
|
|
8467
|
+
fs14.readFile(path11.join(dir, name), "utf8"),
|
|
8468
|
+
fs14.stat(path11.join(dir, name))
|
|
8469
|
+
]);
|
|
8470
|
+
return this.describe(name, content, stat.size, stat.mtime);
|
|
8471
|
+
} catch {
|
|
8472
|
+
return null;
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
8475
|
+
/** One listing entry: frontmatter (falling back to the file name for an unparsed file) plus file stats. */
|
|
8476
|
+
describe(name, content, size, mtime) {
|
|
8477
|
+
const meta = parseMemoryFrontmatter(content, name);
|
|
8478
|
+
return {
|
|
8479
|
+
name,
|
|
8480
|
+
title: meta?.name ?? name,
|
|
8481
|
+
description: meta?.description ?? "",
|
|
8482
|
+
...meta?.updatedAt !== void 0 ? { updatedAt: meta.updatedAt } : {},
|
|
8483
|
+
size,
|
|
8484
|
+
modifiedAt: mtime.toISOString()
|
|
8485
|
+
};
|
|
8486
|
+
}
|
|
8487
|
+
/**
|
|
8488
|
+
* Validates a scope key and returns its directory, 404 if it does not exist. The key is only
|
|
8489
|
+
* ever a single directory name.
|
|
8490
|
+
*
|
|
8491
|
+
* A Workspace directory is created by a Session, so this API never creates one — a key with no
|
|
8492
|
+
* directory means no Session has run there and there is nothing to list. The User scope is the
|
|
8493
|
+
* exception: it belongs to the Agent rather than to any Session, so it is created on demand
|
|
8494
|
+
* (Agents that predate Memory have no `memory/user/` until their next Session).
|
|
8495
|
+
*/
|
|
8496
|
+
async requireScopeDir(projectId, agentId, scopeKey) {
|
|
8497
|
+
await this.agentConfigService.requireExists(projectId, agentId);
|
|
8498
|
+
if (!SCOPE_KEY_PATTERN.test(scopeKey)) {
|
|
8499
|
+
throw badRequest("scopeKey is invalid.");
|
|
8500
|
+
}
|
|
8501
|
+
const dir = memoryScopeDir(this.root, projectId, agentId, scopeKey);
|
|
8502
|
+
if (scopeKey === USER_SCOPE_KEY) {
|
|
8503
|
+
await fs14.mkdir(dir, { recursive: true });
|
|
8504
|
+
return this.requireRealScopeDir(projectId, agentId, scopeKey, dir);
|
|
8505
|
+
}
|
|
8506
|
+
try {
|
|
8507
|
+
if ((await fs14.stat(dir)).isDirectory()) {
|
|
8508
|
+
return this.requireRealScopeDir(projectId, agentId, scopeKey, dir);
|
|
8509
|
+
}
|
|
8510
|
+
} catch {
|
|
8511
|
+
}
|
|
8512
|
+
throw new HttpError(
|
|
8513
|
+
404,
|
|
8514
|
+
"memory_scope_not_found",
|
|
8515
|
+
`No Memory directory for scope: ${scopeKey}`
|
|
8516
|
+
);
|
|
8517
|
+
}
|
|
8518
|
+
/**
|
|
8519
|
+
* Symlink hardening for the scope directory itself: a scope smuggled in as a symlink (the
|
|
8520
|
+
* model can create one with its file tools) would carry every read and delete outside
|
|
8521
|
+
* `memory/`, so the resolved real path must be exactly `<real memory/>/<scopeKey>` — not a
|
|
8522
|
+
* link's target, wherever it points.
|
|
8523
|
+
*/
|
|
8524
|
+
async requireRealScopeDir(projectId, agentId, scopeKey, dir) {
|
|
8525
|
+
try {
|
|
8526
|
+
const realBase = await fs14.realpath(memoryDir(this.root, projectId, agentId));
|
|
8527
|
+
if (await fs14.realpath(dir) === path11.join(realBase, scopeKey)) return dir;
|
|
8528
|
+
} catch {
|
|
8529
|
+
}
|
|
8530
|
+
throw new HttpError(
|
|
8531
|
+
404,
|
|
8532
|
+
"memory_scope_not_found",
|
|
8533
|
+
`No Memory directory for scope: ${scopeKey}`
|
|
8534
|
+
);
|
|
8535
|
+
}
|
|
8536
|
+
/**
|
|
8537
|
+
* The absolute path of a topic file inside a scope directory. The name must pass the
|
|
8538
|
+
* character rule, must not be the reserved index name, and the joined path is checked
|
|
8539
|
+
* for containment — belt and braces, since the rule already excludes separators.
|
|
8540
|
+
*/
|
|
8541
|
+
resolveFile(dir, fileName) {
|
|
8542
|
+
if (!isTopicFileName(fileName)) {
|
|
8543
|
+
throw badRequest(
|
|
8544
|
+
`File name must be a Markdown topic file \u2014 no path, no leading dot, and not the ${MEMORY_INDEX_FILENAME} index.`
|
|
8545
|
+
);
|
|
8546
|
+
}
|
|
8547
|
+
const target = path11.join(dir, fileName);
|
|
8548
|
+
const rel = path11.relative(dir, target);
|
|
8549
|
+
if (rel !== fileName || rel.includes(path11.sep)) {
|
|
8550
|
+
throw badRequest("File name must not contain a path.");
|
|
8551
|
+
}
|
|
8552
|
+
return target;
|
|
8553
|
+
}
|
|
8554
|
+
};
|
|
8555
|
+
|
|
8556
|
+
// src/services/agent-service.ts
|
|
8557
|
+
import fs15 from "fs/promises";
|
|
8558
|
+
import path12 from "path";
|
|
7761
8559
|
import {
|
|
7762
8560
|
agentDir,
|
|
7763
8561
|
agentsDir as agentsDir2,
|
|
@@ -7766,6 +8564,7 @@ import {
|
|
|
7766
8564
|
createAgent as coreCreateAgent,
|
|
7767
8565
|
isValidId as isValidId4,
|
|
7768
8566
|
loadAgentVault as loadAgentVault2,
|
|
8567
|
+
memoryDir as memoryDir2,
|
|
7769
8568
|
scheduleDir as scheduleDir2,
|
|
7770
8569
|
skillsDir as skillsDir2,
|
|
7771
8570
|
systemConfigPath as systemConfigPath2
|
|
@@ -7784,7 +8583,7 @@ var AgentService = class {
|
|
|
7784
8583
|
const known = new Map(this.agents.list(projectId).map((r) => [r.agentId, r]));
|
|
7785
8584
|
let entries = [];
|
|
7786
8585
|
try {
|
|
7787
|
-
const dirents = await
|
|
8586
|
+
const dirents = await fs15.readdir(agentsDir2(this.root, projectId), { withFileTypes: true });
|
|
7788
8587
|
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
7789
8588
|
} catch {
|
|
7790
8589
|
}
|
|
@@ -7793,7 +8592,7 @@ var AgentService = class {
|
|
|
7793
8592
|
const configPath = systemConfigPath2(this.root, projectId, agentId);
|
|
7794
8593
|
let createdAt;
|
|
7795
8594
|
try {
|
|
7796
|
-
const stat = await
|
|
8595
|
+
const stat = await fs15.stat(configPath);
|
|
7797
8596
|
createdAt = (stat.birthtime.getTime() > 0 ? stat.birthtime : stat.mtime).toISOString();
|
|
7798
8597
|
} catch {
|
|
7799
8598
|
continue;
|
|
@@ -7807,12 +8606,13 @@ var AgentService = class {
|
|
|
7807
8606
|
);
|
|
7808
8607
|
return Promise.all(
|
|
7809
8608
|
sorted.map(async (row) => {
|
|
7810
|
-
const [meta, updatedAt, vaultKeyCount, scheduleCount, skillCount] = await Promise.all([
|
|
8609
|
+
const [meta, updatedAt, vaultKeyCount, scheduleCount, skillCount, memoryCount] = await Promise.all([
|
|
7811
8610
|
this.agentConfig.readCardMeta(projectId, row.agentId),
|
|
7812
8611
|
this.configUpdatedAt(projectId, row.agentId),
|
|
7813
8612
|
this.vaultKeyCount(projectId, row.agentId),
|
|
7814
8613
|
this.scheduleCount(projectId, row.agentId),
|
|
7815
|
-
this.skillCount(projectId, row.agentId)
|
|
8614
|
+
this.skillCount(projectId, row.agentId),
|
|
8615
|
+
this.memoryCount(projectId, row.agentId)
|
|
7816
8616
|
]);
|
|
7817
8617
|
return {
|
|
7818
8618
|
agentId: row.agentId,
|
|
@@ -7821,7 +8621,8 @@ var AgentService = class {
|
|
|
7821
8621
|
...updatedAt !== void 0 ? { updatedAt } : {},
|
|
7822
8622
|
vaultKeyCount,
|
|
7823
8623
|
scheduleCount,
|
|
7824
|
-
skillCount
|
|
8624
|
+
skillCount,
|
|
8625
|
+
memoryCount
|
|
7825
8626
|
};
|
|
7826
8627
|
})
|
|
7827
8628
|
);
|
|
@@ -7837,7 +8638,7 @@ var AgentService = class {
|
|
|
7837
8638
|
/** Number of scheduled tasks: count of .toml files under schedule/ (0 if the directory doesn't exist). */
|
|
7838
8639
|
async scheduleCount(projectId, agentId) {
|
|
7839
8640
|
try {
|
|
7840
|
-
const names = await
|
|
8641
|
+
const names = await fs15.readdir(scheduleDir2(this.root, projectId, agentId));
|
|
7841
8642
|
return names.filter((n) => n.endsWith(".toml")).length;
|
|
7842
8643
|
} catch {
|
|
7843
8644
|
return 0;
|
|
@@ -7848,14 +8649,14 @@ var AgentService = class {
|
|
|
7848
8649
|
const base = skillsDir2(this.root, projectId, agentId);
|
|
7849
8650
|
let dirents;
|
|
7850
8651
|
try {
|
|
7851
|
-
dirents = await
|
|
8652
|
+
dirents = await fs15.readdir(base, { withFileTypes: true });
|
|
7852
8653
|
} catch {
|
|
7853
8654
|
return 0;
|
|
7854
8655
|
}
|
|
7855
8656
|
const present = await Promise.all(
|
|
7856
8657
|
dirents.filter((d) => d.isDirectory()).map(async (d) => {
|
|
7857
8658
|
try {
|
|
7858
|
-
await
|
|
8659
|
+
await fs15.access(path12.join(base, d.name, "SKILL.md"));
|
|
7859
8660
|
return true;
|
|
7860
8661
|
} catch {
|
|
7861
8662
|
return false;
|
|
@@ -7864,6 +8665,27 @@ var AgentService = class {
|
|
|
7864
8665
|
);
|
|
7865
8666
|
return present.filter(Boolean).length;
|
|
7866
8667
|
}
|
|
8668
|
+
/** Number of memory topic files: regular `*.md` files (minus each scope's MEMORY.md index) summed over the scope directories under memory/ (0 if the directory doesn't exist). */
|
|
8669
|
+
async memoryCount(projectId, agentId) {
|
|
8670
|
+
const base = memoryDir2(this.root, projectId, agentId);
|
|
8671
|
+
let scopes;
|
|
8672
|
+
try {
|
|
8673
|
+
scopes = await fs15.readdir(base, { withFileTypes: true });
|
|
8674
|
+
} catch {
|
|
8675
|
+
return 0;
|
|
8676
|
+
}
|
|
8677
|
+
const counts = await Promise.all(
|
|
8678
|
+
scopes.filter((d) => d.isDirectory()).map(async (d) => {
|
|
8679
|
+
try {
|
|
8680
|
+
const files = await fs15.readdir(path12.join(base, d.name), { withFileTypes: true });
|
|
8681
|
+
return files.filter((f) => f.isFile() && isTopicFileName(f.name)).length;
|
|
8682
|
+
} catch {
|
|
8683
|
+
return 0;
|
|
8684
|
+
}
|
|
8685
|
+
})
|
|
8686
|
+
);
|
|
8687
|
+
return counts.reduce((sum, n) => sum + n, 0);
|
|
8688
|
+
}
|
|
7867
8689
|
/** Last config modification time: the later of system_config.yaml and AGENTS.md mtime; omitted if neither is readable. */
|
|
7868
8690
|
async configUpdatedAt(projectId, agentId) {
|
|
7869
8691
|
const paths = [
|
|
@@ -7873,7 +8695,7 @@ var AgentService = class {
|
|
|
7873
8695
|
const times = await Promise.all(
|
|
7874
8696
|
paths.map(async (p) => {
|
|
7875
8697
|
try {
|
|
7876
|
-
return (await
|
|
8698
|
+
return (await fs15.stat(p)).mtime.getTime();
|
|
7877
8699
|
} catch {
|
|
7878
8700
|
return 0;
|
|
7879
8701
|
}
|
|
@@ -7898,7 +8720,7 @@ var AgentService = class {
|
|
|
7898
8720
|
"Built-in Agents (default_agent) are provisioned with the Project and cannot be deleted from the web."
|
|
7899
8721
|
);
|
|
7900
8722
|
}
|
|
7901
|
-
await
|
|
8723
|
+
await fs15.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
|
|
7902
8724
|
this.agents.delete(projectId, agentId);
|
|
7903
8725
|
}
|
|
7904
8726
|
/**
|
|
@@ -7915,7 +8737,7 @@ var AgentService = class {
|
|
|
7915
8737
|
`Agent id must be 2\u201364 characters: ${SEMANTIC_ID_RULE}.`
|
|
7916
8738
|
);
|
|
7917
8739
|
}
|
|
7918
|
-
const taken = this.agents.exists(projectId, agentId) || await
|
|
8740
|
+
const taken = this.agents.exists(projectId, agentId) || await fs15.stat(agentDir(this.root, projectId, agentId)).then(
|
|
7919
8741
|
() => true,
|
|
7920
8742
|
() => false
|
|
7921
8743
|
);
|
|
@@ -7929,7 +8751,7 @@ var AgentService = class {
|
|
|
7929
8751
|
config: { name: displayName, ...description !== void 0 ? { description } : {} }
|
|
7930
8752
|
});
|
|
7931
8753
|
} catch (err) {
|
|
7932
|
-
await
|
|
8754
|
+
await fs15.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true }).catch(() => {
|
|
7933
8755
|
});
|
|
7934
8756
|
throw err;
|
|
7935
8757
|
}
|
|
@@ -7944,17 +8766,19 @@ var AgentService = class {
|
|
|
7944
8766
|
updatedAt: createdAt,
|
|
7945
8767
|
toolCount: meta.toolCount,
|
|
7946
8768
|
version: meta.version,
|
|
8769
|
+
kernelOutdated: meta.kernelOutdated,
|
|
7947
8770
|
vaultKeyCount: 0,
|
|
7948
8771
|
scheduleCount: 0,
|
|
7949
8772
|
// Read the real count: coreCreateAgent seeds the default skill set for default_agent.
|
|
7950
|
-
skillCount: await this.skillCount(projectId, agentId)
|
|
8773
|
+
skillCount: await this.skillCount(projectId, agentId),
|
|
8774
|
+
memoryCount: 0
|
|
7951
8775
|
};
|
|
7952
8776
|
}
|
|
7953
8777
|
};
|
|
7954
8778
|
|
|
7955
8779
|
// src/services/benchmark-service.ts
|
|
7956
|
-
import
|
|
7957
|
-
import
|
|
8780
|
+
import fs16 from "fs/promises";
|
|
8781
|
+
import path13 from "path";
|
|
7958
8782
|
import { parse as parseToml3 } from "smol-toml";
|
|
7959
8783
|
import { parse as parseYaml2 } from "yaml";
|
|
7960
8784
|
import { benchmarksDir } from "@prismshadow/penguin-core";
|
|
@@ -7985,15 +8809,15 @@ function stringOr(v) {
|
|
|
7985
8809
|
return typeof v === "string" && v !== "" ? v : void 0;
|
|
7986
8810
|
}
|
|
7987
8811
|
function isWithin(parent, child) {
|
|
7988
|
-
const relative =
|
|
7989
|
-
return relative !== "" && !relative.startsWith("..") && !
|
|
8812
|
+
const relative = path13.relative(parent, child);
|
|
8813
|
+
return relative !== "" && !relative.startsWith("..") && !path13.isAbsolute(relative);
|
|
7990
8814
|
}
|
|
7991
8815
|
function statementTitle(statement, fallback) {
|
|
7992
8816
|
const heading = /^#\s+(.+)$/m.exec(statement)?.[1]?.trim();
|
|
7993
8817
|
return heading?.replace(/^Case\s+\d+\s*:\s*/i, "") || fallback;
|
|
7994
8818
|
}
|
|
7995
8819
|
async function readStatementTitle(readme, fallback) {
|
|
7996
|
-
const handle = await
|
|
8820
|
+
const handle = await fs16.open(readme, "r");
|
|
7997
8821
|
try {
|
|
7998
8822
|
const buffer = Buffer.alloc(STATEMENT_TITLE_READ_BYTES);
|
|
7999
8823
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
@@ -8075,28 +8899,28 @@ var BenchmarkService = class {
|
|
|
8075
8899
|
const dir = benchmarksDir(this.root, projectId, agentId);
|
|
8076
8900
|
let items;
|
|
8077
8901
|
try {
|
|
8078
|
-
const entries = await
|
|
8902
|
+
const entries = await fs16.readdir(dir, { withFileTypes: true });
|
|
8079
8903
|
items = entries.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
|
|
8080
8904
|
} catch {
|
|
8081
8905
|
return { benchmarks: [] };
|
|
8082
8906
|
}
|
|
8083
8907
|
const benchmarks = [];
|
|
8084
8908
|
for (const item of items.filter((i) => i.isDir).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
8085
|
-
benchmarks.push(await this.readBenchmark(
|
|
8909
|
+
benchmarks.push(await this.readBenchmark(path13.join(dir, item.name), item.name));
|
|
8086
8910
|
}
|
|
8087
8911
|
return { benchmarks };
|
|
8088
8912
|
}
|
|
8089
8913
|
async listCases(projectId, agentId, benchmarkId) {
|
|
8090
8914
|
const baseDir = benchmarksDir(this.root, projectId, agentId);
|
|
8091
|
-
const benchDir =
|
|
8915
|
+
const benchDir = path13.join(baseDir, benchmarkId);
|
|
8092
8916
|
let entries;
|
|
8093
8917
|
let realBaseDir;
|
|
8094
8918
|
let realBenchDir;
|
|
8095
8919
|
try {
|
|
8096
8920
|
[entries, realBaseDir, realBenchDir] = await Promise.all([
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8921
|
+
fs16.readdir(benchDir, { withFileTypes: true }),
|
|
8922
|
+
fs16.realpath(baseDir),
|
|
8923
|
+
fs16.realpath(benchDir)
|
|
8100
8924
|
]);
|
|
8101
8925
|
} catch {
|
|
8102
8926
|
return { cases: [] };
|
|
@@ -8113,7 +8937,7 @@ var BenchmarkService = class {
|
|
|
8113
8937
|
entry.name,
|
|
8114
8938
|
"statement"
|
|
8115
8939
|
);
|
|
8116
|
-
const realReadme = await
|
|
8940
|
+
const realReadme = await fs16.realpath(path13.join(statementDir, "README.md"));
|
|
8117
8941
|
if (!isWithin(statementDir, realReadme)) throw new Error("README escapes Statement");
|
|
8118
8942
|
cases.push({
|
|
8119
8943
|
id: entry.name,
|
|
@@ -8146,16 +8970,16 @@ var BenchmarkService = class {
|
|
|
8146
8970
|
return this.workspaceFiles.read(materialRoot, rel, options);
|
|
8147
8971
|
}
|
|
8148
8972
|
async caseMaterialRoot(projectId, agentId, benchmarkId, caseId, material) {
|
|
8149
|
-
const benchDir =
|
|
8150
|
-
const caseDir =
|
|
8151
|
-
const materialRoot =
|
|
8973
|
+
const benchDir = path13.join(benchmarksDir(this.root, projectId, agentId), benchmarkId);
|
|
8974
|
+
const caseDir = path13.join(benchDir, caseId);
|
|
8975
|
+
const materialRoot = path13.join(caseDir, material);
|
|
8152
8976
|
try {
|
|
8153
8977
|
const [realBenchDir, realCaseDir, realMaterialRoot] = await Promise.all([
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8978
|
+
fs16.realpath(benchDir),
|
|
8979
|
+
fs16.realpath(caseDir),
|
|
8980
|
+
fs16.realpath(materialRoot)
|
|
8157
8981
|
]);
|
|
8158
|
-
if (!isWithin(realBenchDir, realCaseDir) ||
|
|
8982
|
+
if (!isWithin(realBenchDir, realCaseDir) || path13.dirname(realMaterialRoot) !== realCaseDir || path13.basename(realMaterialRoot) !== material) {
|
|
8159
8983
|
throw new Error("Case material is not canonical");
|
|
8160
8984
|
}
|
|
8161
8985
|
return realMaterialRoot;
|
|
@@ -8169,7 +8993,7 @@ var BenchmarkService = class {
|
|
|
8169
8993
|
let runs;
|
|
8170
8994
|
try {
|
|
8171
8995
|
const config2 = asRecord2(
|
|
8172
|
-
parseToml3(await
|
|
8996
|
+
parseToml3(await fs16.readFile(path13.join(benchDir, "benchmark_config.toml"), "utf8"))
|
|
8173
8997
|
);
|
|
8174
8998
|
if (typeof config2.title === "string" && config2.title !== "") title = config2.title;
|
|
8175
8999
|
if (typeof config2.description === "string" && config2.description !== "") {
|
|
@@ -8184,7 +9008,7 @@ var BenchmarkService = class {
|
|
|
8184
9008
|
let evaluations = [];
|
|
8185
9009
|
try {
|
|
8186
9010
|
const scoreboard = asRecord2(
|
|
8187
|
-
parseYaml2(await
|
|
9011
|
+
parseYaml2(await fs16.readFile(path13.join(benchDir, "scoreboard.yaml"), "utf8"))
|
|
8188
9012
|
);
|
|
8189
9013
|
if (Array.isArray(scoreboard.evaluations)) {
|
|
8190
9014
|
evaluations = scoreboard.evaluations.map(toEvaluation).filter((e) => e !== null);
|
|
@@ -8193,7 +9017,7 @@ var BenchmarkService = class {
|
|
|
8193
9017
|
}
|
|
8194
9018
|
let caseCount = 0;
|
|
8195
9019
|
try {
|
|
8196
|
-
const entries = await
|
|
9020
|
+
const entries = await fs16.readdir(benchDir, { withFileTypes: true });
|
|
8197
9021
|
caseCount = entries.filter((e) => e.isDirectory() && e.name.startsWith("CASE-")).length;
|
|
8198
9022
|
} catch {
|
|
8199
9023
|
}
|
|
@@ -8210,8 +9034,8 @@ var BenchmarkService = class {
|
|
|
8210
9034
|
|
|
8211
9035
|
// src/services/snapshot-service.ts
|
|
8212
9036
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
8213
|
-
import
|
|
8214
|
-
import
|
|
9037
|
+
import fs17 from "fs/promises";
|
|
9038
|
+
import path14 from "path";
|
|
8215
9039
|
import * as tar from "tar";
|
|
8216
9040
|
import { parse as parseYaml3 } from "yaml";
|
|
8217
9041
|
import {
|
|
@@ -8224,7 +9048,7 @@ import {
|
|
|
8224
9048
|
} from "@prismshadow/penguin-core";
|
|
8225
9049
|
var VAULT_BASENAME = ".vault.toml";
|
|
8226
9050
|
function isVaultEntry(entryPath) {
|
|
8227
|
-
return
|
|
9051
|
+
return path14.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
|
|
8228
9052
|
}
|
|
8229
9053
|
var SnapshotService = class {
|
|
8230
9054
|
constructor(root) {
|
|
@@ -8235,7 +9059,7 @@ var SnapshotService = class {
|
|
|
8235
9059
|
async currentVersion(projectId, agentId) {
|
|
8236
9060
|
let raw;
|
|
8237
9061
|
try {
|
|
8238
|
-
raw = await
|
|
9062
|
+
raw = await fs17.readFile(systemConfigPath3(this.root, projectId, agentId), "utf8");
|
|
8239
9063
|
} catch {
|
|
8240
9064
|
throw new HttpError(404, "agent_not_found", "Agent does not exist.");
|
|
8241
9065
|
}
|
|
@@ -8248,13 +9072,13 @@ var SnapshotService = class {
|
|
|
8248
9072
|
async ensureSnapshot(projectId, agentId) {
|
|
8249
9073
|
const version = await this.currentVersion(projectId, agentId);
|
|
8250
9074
|
const dir = snapshotsDir(this.root, projectId, agentId);
|
|
8251
|
-
const file =
|
|
9075
|
+
const file = path14.join(dir, `v${version}.tar.gz`);
|
|
8252
9076
|
try {
|
|
8253
|
-
await
|
|
9077
|
+
await fs17.access(file);
|
|
8254
9078
|
return { version, file };
|
|
8255
9079
|
} catch {
|
|
8256
9080
|
}
|
|
8257
|
-
await
|
|
9081
|
+
await fs17.mkdir(dir, { recursive: true });
|
|
8258
9082
|
const tmp = `${file}.tmp-${randomBytes6(4).toString("hex")}`;
|
|
8259
9083
|
await tar.create(
|
|
8260
9084
|
{
|
|
@@ -8266,7 +9090,7 @@ var SnapshotService = class {
|
|
|
8266
9090
|
},
|
|
8267
9091
|
["agent_state"]
|
|
8268
9092
|
);
|
|
8269
|
-
await
|
|
9093
|
+
await fs17.rename(tmp, file);
|
|
8270
9094
|
return { version, file };
|
|
8271
9095
|
}
|
|
8272
9096
|
/** Export: automatically packs a snapshot first if none exists, returns the info needed for download. */
|
|
@@ -8281,15 +9105,15 @@ var SnapshotService = class {
|
|
|
8281
9105
|
* replaces `agent_state/` while keeping the current vault.
|
|
8282
9106
|
*/
|
|
8283
9107
|
async importArchive(projectId, agentId, archive, confirm) {
|
|
8284
|
-
const
|
|
9108
|
+
const current2 = await this.currentVersion(projectId, agentId);
|
|
8285
9109
|
const base = agentDir2(this.root, projectId, agentId);
|
|
8286
|
-
const staging =
|
|
8287
|
-
await
|
|
9110
|
+
const staging = path14.join(base, `.import-${randomBytes6(6).toString("hex")}`);
|
|
9111
|
+
await fs17.mkdir(staging, { recursive: true });
|
|
8288
9112
|
try {
|
|
8289
|
-
const archiveFile =
|
|
8290
|
-
await
|
|
8291
|
-
const extractDir =
|
|
8292
|
-
await
|
|
9113
|
+
const archiveFile = path14.join(staging, "archive.tar.gz");
|
|
9114
|
+
await fs17.writeFile(archiveFile, archive);
|
|
9115
|
+
const extractDir = path14.join(staging, "extracted");
|
|
9116
|
+
await fs17.mkdir(extractDir, { recursive: true });
|
|
8293
9117
|
try {
|
|
8294
9118
|
await tar.extract({
|
|
8295
9119
|
file: archiveFile,
|
|
@@ -8299,10 +9123,10 @@ var SnapshotService = class {
|
|
|
8299
9123
|
} catch {
|
|
8300
9124
|
throw badRequest("Import failed: not a valid tar.gz snapshot package.");
|
|
8301
9125
|
}
|
|
8302
|
-
const configPath =
|
|
9126
|
+
const configPath = path14.join(extractDir, "agent_state", "system_config.yaml");
|
|
8303
9127
|
let parsed;
|
|
8304
9128
|
try {
|
|
8305
|
-
parsed = parseYaml3(await
|
|
9129
|
+
parsed = parseYaml3(await fs17.readFile(configPath, "utf8"));
|
|
8306
9130
|
} catch {
|
|
8307
9131
|
throw badRequest("Import failed: the package is missing agent_state/system_config.yaml.");
|
|
8308
9132
|
}
|
|
@@ -8316,46 +9140,46 @@ var SnapshotService = class {
|
|
|
8316
9140
|
const incoming = agentStateVersion2({
|
|
8317
9141
|
version: typeof incomingRaw === "number" ? incomingRaw : void 0
|
|
8318
9142
|
});
|
|
8319
|
-
if (incoming <=
|
|
9143
|
+
if (incoming <= current2 && !confirm) {
|
|
8320
9144
|
throw new HttpError(
|
|
8321
9145
|
409,
|
|
8322
9146
|
"version_conflict",
|
|
8323
|
-
`Package version v${incoming} is not higher than the current v${
|
|
9147
|
+
`Package version v${incoming} is not higher than the current v${current2}; confirmation is required to import.`
|
|
8324
9148
|
);
|
|
8325
9149
|
}
|
|
8326
9150
|
await this.ensureSnapshot(projectId, agentId);
|
|
8327
9151
|
const stateDir = agentStateDir2(this.root, projectId, agentId);
|
|
8328
|
-
const incomingState =
|
|
9152
|
+
const incomingState = path14.join(extractDir, "agent_state");
|
|
8329
9153
|
let vault = null;
|
|
8330
9154
|
try {
|
|
8331
|
-
vault = await
|
|
9155
|
+
vault = await fs17.readFile(agentVaultPath(this.root, projectId, agentId));
|
|
8332
9156
|
} catch {
|
|
8333
9157
|
}
|
|
8334
9158
|
if (vault !== null) {
|
|
8335
|
-
await
|
|
9159
|
+
await fs17.writeFile(path14.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
|
|
8336
9160
|
}
|
|
8337
|
-
const trash =
|
|
8338
|
-
await
|
|
9161
|
+
const trash = path14.join(staging, "replaced-agent_state");
|
|
9162
|
+
await fs17.rename(stateDir, trash);
|
|
8339
9163
|
try {
|
|
8340
|
-
await
|
|
9164
|
+
await fs17.rename(incomingState, stateDir);
|
|
8341
9165
|
} catch (err) {
|
|
8342
|
-
await
|
|
9166
|
+
await fs17.rename(trash, stateDir);
|
|
8343
9167
|
throw err;
|
|
8344
9168
|
}
|
|
8345
9169
|
return { version: incoming };
|
|
8346
9170
|
} finally {
|
|
8347
|
-
await
|
|
9171
|
+
await fs17.rm(staging, { recursive: true, force: true });
|
|
8348
9172
|
}
|
|
8349
9173
|
}
|
|
8350
9174
|
};
|
|
8351
9175
|
|
|
8352
9176
|
// src/services/project-service.ts
|
|
8353
|
-
import
|
|
9177
|
+
import fs18 from "fs/promises";
|
|
8354
9178
|
import { DEFAULT_PROJECT_ID, projectDir, provisionProjectAgents } from "@prismshadow/penguin-core";
|
|
8355
9179
|
var ABORT_SETTLE_TIMEOUT_MS = 5e3;
|
|
8356
|
-
async function dirExists(
|
|
9180
|
+
async function dirExists(path20) {
|
|
8357
9181
|
try {
|
|
8358
|
-
const stat = await
|
|
9182
|
+
const stat = await fs18.stat(path20);
|
|
8359
9183
|
return stat.isDirectory();
|
|
8360
9184
|
} catch {
|
|
8361
9185
|
return false;
|
|
@@ -8480,12 +9304,12 @@ var ProjectService = class {
|
|
|
8480
9304
|
throw err;
|
|
8481
9305
|
}
|
|
8482
9306
|
try {
|
|
8483
|
-
await
|
|
9307
|
+
await fs18.mkdir(projectDir(this.deps.root, projectId), { recursive: true });
|
|
8484
9308
|
await this.deps.projectConfig.writeInitialConfig(projectId, displayName);
|
|
8485
9309
|
await this.provisionBuiltinAgents(projectId);
|
|
8486
9310
|
} catch (err) {
|
|
8487
9311
|
this.deps.projects.delete(projectId);
|
|
8488
|
-
await
|
|
9312
|
+
await fs18.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true }).catch(() => {
|
|
8489
9313
|
});
|
|
8490
9314
|
throw err;
|
|
8491
9315
|
}
|
|
@@ -8584,7 +9408,7 @@ var ProjectService = class {
|
|
|
8584
9408
|
this.deps.schedules.deleteByProject(projectId);
|
|
8585
9409
|
this.deps.goals.deleteByProject(projectId);
|
|
8586
9410
|
this.deps.traceIndex.removeProject(projectId);
|
|
8587
|
-
await
|
|
9411
|
+
await fs18.rm(projectDir(this.deps.root, projectId), { recursive: true, force: true });
|
|
8588
9412
|
}
|
|
8589
9413
|
// —— Member authorization ——
|
|
8590
9414
|
/** Member list: owner (role=owner) + members. */
|
|
@@ -8658,8 +9482,8 @@ var ProjectService = class {
|
|
|
8658
9482
|
import { createAgent as createAgent2, isSessionMeta as isSessionMeta5 } from "@prismshadow/penguin-core";
|
|
8659
9483
|
|
|
8660
9484
|
// src/services/trace-index.ts
|
|
8661
|
-
import
|
|
8662
|
-
import
|
|
9485
|
+
import fs19 from "fs/promises";
|
|
9486
|
+
import path15 from "path";
|
|
8663
9487
|
import { agentsDir as agentsDir3, isSessionMeta as isSessionMeta4, tracesDir as tracesDir2 } from "@prismshadow/penguin-core";
|
|
8664
9488
|
|
|
8665
9489
|
// src/internal/trace-head.ts
|
|
@@ -8692,11 +9516,11 @@ async function readTraceHead(filePath) {
|
|
|
8692
9516
|
var TRACE_FILE_RE = /^(.+)_(\d{3})\.jsonl$/;
|
|
8693
9517
|
function traceFilePath(root, row) {
|
|
8694
9518
|
const name = `${row.sessionId}_${String(row.fileIndex).padStart(3, "0")}.jsonl`;
|
|
8695
|
-
return
|
|
9519
|
+
return path15.join(tracesDir2(root, row.projectId, row.agentId), row.date, name);
|
|
8696
9520
|
}
|
|
8697
9521
|
async function listDirs(dir) {
|
|
8698
9522
|
try {
|
|
8699
|
-
const entries = await
|
|
9523
|
+
const entries = await fs19.readdir(dir, { withFileTypes: true });
|
|
8700
9524
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
8701
9525
|
} catch {
|
|
8702
9526
|
return [];
|
|
@@ -8704,7 +9528,7 @@ async function listDirs(dir) {
|
|
|
8704
9528
|
}
|
|
8705
9529
|
async function listFiles(dir) {
|
|
8706
9530
|
try {
|
|
8707
|
-
const entries = await
|
|
9531
|
+
const entries = await fs19.readdir(dir, { withFileTypes: true });
|
|
8708
9532
|
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
8709
9533
|
} catch {
|
|
8710
9534
|
return [];
|
|
@@ -8789,7 +9613,7 @@ var TraceIndexService = class {
|
|
|
8789
9613
|
const newest = [...cached.dates.keys()].sort().at(-1);
|
|
8790
9614
|
if (newest === void 0) return;
|
|
8791
9615
|
this.counters.gateStats += 1;
|
|
8792
|
-
const m = await statMtime(
|
|
9616
|
+
const m = await statMtime(path15.join(dir, newest));
|
|
8793
9617
|
if (m !== null && m === cached.dates.get(newest)) return;
|
|
8794
9618
|
}
|
|
8795
9619
|
this.counters.dirScans += 1;
|
|
@@ -8803,14 +9627,14 @@ var TraceIndexService = class {
|
|
|
8803
9627
|
const newSessions = /* @__PURE__ */ new Set();
|
|
8804
9628
|
const dateDirs = await listDirs(dir);
|
|
8805
9629
|
for (const date of dateDirs) {
|
|
8806
|
-
const m = await statMtime(
|
|
9630
|
+
const m = await statMtime(path15.join(dir, date));
|
|
8807
9631
|
if (m === null) continue;
|
|
8808
9632
|
next.dates.set(date, cacheable(m));
|
|
8809
9633
|
if (!force && cached?.dates.get(date) === m) continue;
|
|
8810
9634
|
const known = new Map(
|
|
8811
9635
|
(knownByDate.get(date) ?? []).map((r) => [`${r.sessionId}\0${r.fileIndex}`, r])
|
|
8812
9636
|
);
|
|
8813
|
-
for (const file of await listFiles(
|
|
9637
|
+
for (const file of await listFiles(path15.join(dir, date))) {
|
|
8814
9638
|
const match = TRACE_FILE_RE.exec(file);
|
|
8815
9639
|
if (!match) continue;
|
|
8816
9640
|
const sessionId = match[1];
|
|
@@ -8818,7 +9642,7 @@ var TraceIndexService = class {
|
|
|
8818
9642
|
const fileKey = `${sessionId}\0${fileIndex}`;
|
|
8819
9643
|
let size = 0;
|
|
8820
9644
|
try {
|
|
8821
|
-
size = (await
|
|
9645
|
+
size = (await fs19.stat(path15.join(dir, date, file))).size;
|
|
8822
9646
|
} catch {
|
|
8823
9647
|
continue;
|
|
8824
9648
|
}
|
|
@@ -9113,7 +9937,8 @@ var SessionService = class {
|
|
|
9113
9937
|
const agent = await createAgent2({
|
|
9114
9938
|
root: this.deps.root,
|
|
9115
9939
|
projectId: args.projectId,
|
|
9116
|
-
agentId: args.agentId
|
|
9940
|
+
agentId: args.agentId,
|
|
9941
|
+
...this.deps.proxyEnv ? { proxyEnv: this.deps.proxyEnv } : {}
|
|
9117
9942
|
});
|
|
9118
9943
|
let session;
|
|
9119
9944
|
try {
|
|
@@ -9230,8 +10055,8 @@ function localDate(d) {
|
|
|
9230
10055
|
}
|
|
9231
10056
|
|
|
9232
10057
|
// src/services/trace-service.ts
|
|
9233
|
-
import
|
|
9234
|
-
import
|
|
10058
|
+
import fs20 from "fs/promises";
|
|
10059
|
+
import path16 from "path";
|
|
9235
10060
|
import {
|
|
9236
10061
|
isSessionMeta as isSessionMeta6,
|
|
9237
10062
|
parseTraceLines as parseTraceLines2,
|
|
@@ -9277,15 +10102,15 @@ var TraceService = class {
|
|
|
9277
10102
|
}));
|
|
9278
10103
|
}
|
|
9279
10104
|
/** All shard reads funnel through here (deps.observeShardRead is the windowed-read tests' proof of which files were touched). */
|
|
9280
|
-
async readShard(
|
|
9281
|
-
this.deps.observeShardRead?.(
|
|
9282
|
-
return readTraceTolerant2(
|
|
10105
|
+
async readShard(path20) {
|
|
10106
|
+
this.deps.observeShardRead?.(path20);
|
|
10107
|
+
return readTraceTolerant2(path20);
|
|
9283
10108
|
}
|
|
9284
10109
|
/** Deletes all of this Session's Trace files (called when the Session is deleted); the index rows go with them. */
|
|
9285
10110
|
async deleteSessionTraces(projectId, agentId, sessionId) {
|
|
9286
10111
|
const files = await this.locateAll(projectId, agentId, sessionId);
|
|
9287
10112
|
for (const file of files) {
|
|
9288
|
-
await
|
|
10113
|
+
await fs20.rm(file.path, { force: true });
|
|
9289
10114
|
}
|
|
9290
10115
|
this.deps.index.removeSession(projectId, agentId, sessionId);
|
|
9291
10116
|
}
|
|
@@ -9534,7 +10359,7 @@ var TraceService = class {
|
|
|
9534
10359
|
const files = await this.locateAll(projectId, agentId, sessionId);
|
|
9535
10360
|
const out = [];
|
|
9536
10361
|
for (const file of files) {
|
|
9537
|
-
const stat = await
|
|
10362
|
+
const stat = await fs20.stat(file.path);
|
|
9538
10363
|
out.push({
|
|
9539
10364
|
index: file.index,
|
|
9540
10365
|
date: file.date,
|
|
@@ -9566,7 +10391,9 @@ var TraceService = class {
|
|
|
9566
10391
|
const usageTrend = [];
|
|
9567
10392
|
const modelSegments = [];
|
|
9568
10393
|
const toolSpans = [];
|
|
10394
|
+
const otherSpans = [];
|
|
9569
10395
|
const openSpansById = /* @__PURE__ */ new Map();
|
|
10396
|
+
let openMcpConnect = null;
|
|
9570
10397
|
let prevSerialTs = null;
|
|
9571
10398
|
let taskIndex = -1;
|
|
9572
10399
|
let continuation = false;
|
|
@@ -9665,6 +10492,22 @@ var TraceService = class {
|
|
|
9665
10492
|
}
|
|
9666
10493
|
openRequest = null;
|
|
9667
10494
|
}
|
|
10495
|
+
} else if (p.type === "mcp_connect_begin") {
|
|
10496
|
+
if (!hasOrigin) openMcpConnect = { beginTs: msg.timestamp };
|
|
10497
|
+
} else if (p.type === "mcp_connect_end") {
|
|
10498
|
+
if (!hasOrigin && openMcpConnect) {
|
|
10499
|
+
const q = p;
|
|
10500
|
+
const failed = q.status !== "completed";
|
|
10501
|
+
otherSpans.push({
|
|
10502
|
+
key: `mcp-connect-${openMcpConnect.beginTs}`,
|
|
10503
|
+
name: "mcp connect",
|
|
10504
|
+
startTs: openMcpConnect.beginTs,
|
|
10505
|
+
endTs: msg.timestamp,
|
|
10506
|
+
taskIndex: taskIndex + 1,
|
|
10507
|
+
...failed ? { failed: true } : {}
|
|
10508
|
+
});
|
|
10509
|
+
openMcpConnect = null;
|
|
10510
|
+
}
|
|
9668
10511
|
} else if (p.type === "compaction_begin") {
|
|
9669
10512
|
compactionCount++;
|
|
9670
10513
|
if (!hasOrigin) {
|
|
@@ -9785,6 +10628,7 @@ var TraceService = class {
|
|
|
9785
10628
|
toolCalls,
|
|
9786
10629
|
modelSegments,
|
|
9787
10630
|
toolSpans,
|
|
10631
|
+
otherSpans,
|
|
9788
10632
|
reconnectCount,
|
|
9789
10633
|
compactionCount,
|
|
9790
10634
|
usageTrend
|
|
@@ -9883,7 +10727,7 @@ var TraceService = class {
|
|
|
9883
10727
|
shard.map(async (f) => {
|
|
9884
10728
|
let sizeBytes = f.sizeBytes;
|
|
9885
10729
|
try {
|
|
9886
|
-
sizeBytes = (await
|
|
10730
|
+
sizeBytes = (await fs20.stat(traceFilePath(this.root, f))).size;
|
|
9887
10731
|
if (sizeBytes !== f.sizeBytes) {
|
|
9888
10732
|
this.deps.index.repo.updateFileSize(
|
|
9889
10733
|
projectId,
|
|
@@ -9929,7 +10773,7 @@ var TraceService = class {
|
|
|
9929
10773
|
/** Raw bytes of the Trace file at the given index (export/download: the file is served verbatim). */
|
|
9930
10774
|
async readFileRaw(projectId, agentId, sessionId, index) {
|
|
9931
10775
|
const file = await this.locateByIndex(projectId, agentId, sessionId, index);
|
|
9932
|
-
return
|
|
10776
|
+
return fs20.readFile(file.path);
|
|
9933
10777
|
}
|
|
9934
10778
|
/**
|
|
9935
10779
|
* Imports an uploaded Trace file (raw JSONL content). Validates the content itself —
|
|
@@ -9947,7 +10791,7 @@ var TraceService = class {
|
|
|
9947
10791
|
const invalid = (message) => new HttpError(400, "invalid_trace", message);
|
|
9948
10792
|
let records;
|
|
9949
10793
|
try {
|
|
9950
|
-
records = parseTraceLines2(content);
|
|
10794
|
+
records = parseTraceLines2(content, { onMalformed: "throw" });
|
|
9951
10795
|
} catch {
|
|
9952
10796
|
throw invalid("The file is not valid Trace JSONL.");
|
|
9953
10797
|
}
|
|
@@ -9968,12 +10812,12 @@ var TraceService = class {
|
|
|
9968
10812
|
const ts = Date.parse(first.timestamp);
|
|
9969
10813
|
const date = formatLocalDate(Number.isNaN(ts) ? /* @__PURE__ */ new Date() : new Date(ts));
|
|
9970
10814
|
const index = 1;
|
|
9971
|
-
const dir =
|
|
9972
|
-
await
|
|
9973
|
-
const file =
|
|
10815
|
+
const dir = path16.join(tracesDir3(this.root, projectId, agentId), date);
|
|
10816
|
+
await fs20.mkdir(dir, { recursive: true });
|
|
10817
|
+
const file = path16.join(dir, `${sessionId}_${String(index).padStart(3, "0")}.jsonl`);
|
|
9974
10818
|
const body = content.replace(/\n+$/, "") + "\n";
|
|
9975
10819
|
try {
|
|
9976
|
-
await
|
|
10820
|
+
await fs20.writeFile(file, body, { encoding: "utf8", flag: "wx" });
|
|
9977
10821
|
} catch (err) {
|
|
9978
10822
|
if (err.code === "EEXIST") throw duplicate();
|
|
9979
10823
|
throw err;
|
|
@@ -10289,10 +11133,10 @@ var UsageService = class {
|
|
|
10289
11133
|
};
|
|
10290
11134
|
|
|
10291
11135
|
// src/http/routes/preview.ts
|
|
10292
|
-
import { Hono as
|
|
11136
|
+
import { Hono as Hono24 } from "hono";
|
|
10293
11137
|
var DIRECTORY_INDEX = "index.html";
|
|
10294
11138
|
function previewRoutes(deps2) {
|
|
10295
|
-
const app2 = new
|
|
11139
|
+
const app2 = new Hono24();
|
|
10296
11140
|
app2.get("/:token/*", async (c) => {
|
|
10297
11141
|
const token = c.req.param("token");
|
|
10298
11142
|
const payload = token ? deps2.previewTokens.verify(token) : null;
|
|
@@ -10349,11 +11193,18 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
10349
11193
|
const usageRepo = new UsageRepo(db);
|
|
10350
11194
|
const errorsRepo = new ErrorsRepo(db);
|
|
10351
11195
|
const prefsRepo = new UiPrefsRepo(db);
|
|
11196
|
+
const serverSettingsRepo = new ServerSettingsRepo(db);
|
|
11197
|
+
const proxyEnv = () => {
|
|
11198
|
+
if (!serverSettingsRepo.getProxyForAgent()) return { mode: "strip" };
|
|
11199
|
+
const url = serverSettingsRepo.getProxyUrl();
|
|
11200
|
+
return url === null ? null : { mode: "inject", url, noProxy: mergedNoProxy() };
|
|
11201
|
+
};
|
|
10352
11202
|
const schedulesRepo = new SchedulesRepo(db);
|
|
10353
11203
|
const goalsRepo = new GoalsRepo(db);
|
|
10354
11204
|
const projectConfigService = new ProjectConfigService(config2.root);
|
|
10355
11205
|
const agentConfigService = new AgentConfigService(config2.root);
|
|
10356
11206
|
const agentService = new AgentService(config2.root, agentsRepo, agentConfigService);
|
|
11207
|
+
const memoryService = new MemoryService(config2.root, agentConfigService);
|
|
10357
11208
|
const sessionSources = new SessionSources();
|
|
10358
11209
|
const traceIndexRepo = new TraceIndexRepo(db);
|
|
10359
11210
|
const traceIndex = new TraceIndexService(config2.root, traceIndexRepo, sessionSources);
|
|
@@ -10383,7 +11234,7 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
10383
11234
|
const manager = new SessionManager({
|
|
10384
11235
|
sessions: sessionsRepo,
|
|
10385
11236
|
channels,
|
|
10386
|
-
loader: overrides.loader ?? createCoreSessionLoader(config2.root, sessionSources),
|
|
11237
|
+
loader: overrides.loader ?? createCoreSessionLoader(config2.root, sessionSources, { proxyEnv }),
|
|
10387
11238
|
sources: sessionSources,
|
|
10388
11239
|
recorder,
|
|
10389
11240
|
errors,
|
|
@@ -10407,11 +11258,15 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
10407
11258
|
manager,
|
|
10408
11259
|
traceIndex
|
|
10409
11260
|
});
|
|
11261
|
+
const onPasswordChanged = (userId) => {
|
|
11262
|
+
if (userId === ADMIN_USER_ID) clearInitialAdminPassword(config2.root);
|
|
11263
|
+
};
|
|
10410
11264
|
const authService = new AuthService({
|
|
10411
11265
|
users: usersRepo,
|
|
10412
11266
|
authSessions: authSessionsRepo,
|
|
10413
11267
|
provisionInitialProject: (user, isAdmin) => projectService.provisionInitialProject(user, isAdmin),
|
|
10414
11268
|
seedAdminPassword: config2.seedAdminPassword,
|
|
11269
|
+
onPasswordChanged,
|
|
10415
11270
|
sessionTtlMs: config2.authSessionTtlMs,
|
|
10416
11271
|
sessionRenewMs: config2.authSessionRenewMs,
|
|
10417
11272
|
...overrides.now ? { now: overrides.now } : {}
|
|
@@ -10421,6 +11276,7 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
10421
11276
|
authSessions: authSessionsRepo,
|
|
10422
11277
|
projects: projectsRepo,
|
|
10423
11278
|
projectService,
|
|
11279
|
+
onPasswordChanged,
|
|
10424
11280
|
...overrides.now ? { now: overrides.now } : {}
|
|
10425
11281
|
});
|
|
10426
11282
|
const sessionService = new SessionService({
|
|
@@ -10429,7 +11285,8 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
10429
11285
|
manager,
|
|
10430
11286
|
projectConfig: projectConfigService,
|
|
10431
11287
|
sources: sessionSources,
|
|
10432
|
-
traceIndex
|
|
11288
|
+
traceIndex,
|
|
11289
|
+
proxyEnv
|
|
10433
11290
|
});
|
|
10434
11291
|
const scheduler = new Scheduler({
|
|
10435
11292
|
root: config2.root,
|
|
@@ -10450,12 +11307,14 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
10450
11307
|
db,
|
|
10451
11308
|
sessionsRepo,
|
|
10452
11309
|
prefsRepo,
|
|
11310
|
+
serverSettingsRepo,
|
|
10453
11311
|
authService,
|
|
10454
11312
|
adminService,
|
|
10455
11313
|
projectService,
|
|
10456
11314
|
projectConfigService,
|
|
10457
11315
|
agentService,
|
|
10458
11316
|
agentConfigService,
|
|
11317
|
+
memoryService,
|
|
10459
11318
|
sessionService,
|
|
10460
11319
|
traceService,
|
|
10461
11320
|
traceIndex,
|
|
@@ -10478,7 +11337,7 @@ function buildAppDeps(config2, overrides = {}) {
|
|
|
10478
11337
|
};
|
|
10479
11338
|
}
|
|
10480
11339
|
function createApp(deps2) {
|
|
10481
|
-
const app2 = new
|
|
11340
|
+
const app2 = new Hono25();
|
|
10482
11341
|
app2.onError((err, c) => {
|
|
10483
11342
|
const projectId = attributedProjectId(c, deps2);
|
|
10484
11343
|
deps2.errors.record({
|
|
@@ -10531,6 +11390,7 @@ function createApp(deps2) {
|
|
|
10531
11390
|
app2.route("/api/me", meRoutes(deps2));
|
|
10532
11391
|
app2.route("/api/version", versionRoutes(deps2));
|
|
10533
11392
|
app2.route("/api/admin/users", adminUsersRoutes(deps2));
|
|
11393
|
+
app2.route("/api/admin/settings", adminSettingsRoutes(deps2));
|
|
10534
11394
|
app2.route("/api/events", eventsRoutes(deps2));
|
|
10535
11395
|
app2.route("/api/skills", skillLibraryRoutes());
|
|
10536
11396
|
app2.route("/api/projects", projectsRoutes(deps2));
|
|
@@ -10541,6 +11401,7 @@ function createApp(deps2) {
|
|
|
10541
11401
|
app2.route("/api/projects/:projectId/dirs", dirsRoutes(deps2));
|
|
10542
11402
|
app2.route("/api/projects/:projectId/agents/:agentId/config", agentConfigRoutes(deps2));
|
|
10543
11403
|
app2.route("/api/projects/:projectId/agents/:agentId/vault", vaultRoutes(deps2));
|
|
11404
|
+
app2.route("/api/projects/:projectId/agents/:agentId/memory", memoryRoutes(deps2));
|
|
10544
11405
|
app2.route("/api/projects/:projectId/agents/:agentId/schedules", scheduleRoutes(deps2));
|
|
10545
11406
|
app2.route("/api/projects/:projectId/agents/:agentId/benchmarks", benchmarksRoutes(deps2));
|
|
10546
11407
|
app2.route("/api/projects/:projectId/agents/:agentId/skills", agentSkillsRoutes(deps2));
|
|
@@ -10550,7 +11411,7 @@ function createApp(deps2) {
|
|
|
10550
11411
|
app2.route("/api/projects/:projectId/usage", usageRoutes(deps2));
|
|
10551
11412
|
app2.route("/api/sessions", sessionsRoutes(deps2));
|
|
10552
11413
|
app2.route("/preview", previewRoutes(deps2));
|
|
10553
|
-
if (
|
|
11414
|
+
if (fs21.existsSync(deps2.config.webDist)) {
|
|
10554
11415
|
registerStaticRoutes(app2, deps2.config.webDist);
|
|
10555
11416
|
}
|
|
10556
11417
|
return app2;
|
|
@@ -10586,16 +11447,16 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
10586
11447
|
return c.json(errorBody("not_found", "Endpoint does not exist."), 404);
|
|
10587
11448
|
}
|
|
10588
11449
|
const rel = reqPath.replace(/^\/+/, "");
|
|
10589
|
-
const resolved =
|
|
10590
|
-
const base =
|
|
10591
|
-
const target = resolved === base || resolved.startsWith(base +
|
|
11450
|
+
const resolved = path17.resolve(webDist, rel === "" ? "index.html" : rel);
|
|
11451
|
+
const base = path17.resolve(webDist);
|
|
11452
|
+
const target = resolved === base || resolved.startsWith(base + path17.sep) ? resolved : path17.join(base, "index.html");
|
|
10592
11453
|
let file = target;
|
|
10593
11454
|
try {
|
|
10594
11455
|
const stat = await fsp.stat(file);
|
|
10595
|
-
if (stat.isDirectory()) file =
|
|
11456
|
+
if (stat.isDirectory()) file = path17.join(file, "index.html");
|
|
10596
11457
|
await fsp.access(file);
|
|
10597
11458
|
} catch {
|
|
10598
|
-
file =
|
|
11459
|
+
file = path17.join(base, "index.html");
|
|
10599
11460
|
}
|
|
10600
11461
|
let content;
|
|
10601
11462
|
try {
|
|
@@ -10603,7 +11464,7 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
10603
11464
|
} catch {
|
|
10604
11465
|
return c.json(errorBody("not_found", "Resource does not exist."), 404);
|
|
10605
11466
|
}
|
|
10606
|
-
const type = CONTENT_TYPES2[
|
|
11467
|
+
const type = CONTENT_TYPES2[path17.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
10607
11468
|
return new Response(new Uint8Array(content), {
|
|
10608
11469
|
status: 200,
|
|
10609
11470
|
headers: { "Content-Type": type }
|
|
@@ -10613,16 +11474,16 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
10613
11474
|
|
|
10614
11475
|
// src/config.ts
|
|
10615
11476
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
10616
|
-
import
|
|
10617
|
-
import
|
|
11477
|
+
import fs22 from "fs";
|
|
11478
|
+
import path18 from "path";
|
|
10618
11479
|
import { fileURLToPath } from "url";
|
|
10619
11480
|
import { DEFAULT_SERVER_PORT, resolveRoot } from "@prismshadow/penguin-core";
|
|
10620
11481
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
10621
11482
|
function defaultWebDist() {
|
|
10622
|
-
const here =
|
|
10623
|
-
const bundled =
|
|
10624
|
-
if (
|
|
10625
|
-
return
|
|
11483
|
+
const here = path18.dirname(fileURLToPath(import.meta.url));
|
|
11484
|
+
const bundled = path18.resolve(here, "..", "web-dist");
|
|
11485
|
+
if (fs22.existsSync(bundled)) return bundled;
|
|
11486
|
+
return path18.resolve(here, "..", "..", "web", "dist");
|
|
10626
11487
|
}
|
|
10627
11488
|
function normalizePreviewOrigin(raw) {
|
|
10628
11489
|
if (!raw || raw.trim() === "") return null;
|
|
@@ -10652,7 +11513,7 @@ function resolveServerConfig(env = process.env) {
|
|
|
10652
11513
|
root,
|
|
10653
11514
|
host,
|
|
10654
11515
|
port,
|
|
10655
|
-
dbPath: env.PENGUIN_WEB_DB ??
|
|
11516
|
+
dbPath: env.PENGUIN_WEB_DB ?? path18.join(root, "web.db"),
|
|
10656
11517
|
webDist: env.PENGUIN_WEB_DIST ?? defaultWebDist(),
|
|
10657
11518
|
previewOrigin: normalizePreviewOrigin(env.PENGUIN_PREVIEW_ORIGIN),
|
|
10658
11519
|
// An empty/whitespace value is treated as unset (→ random seed password). Desktop
|
|
@@ -10669,6 +11530,7 @@ function resolveServerConfig(env = process.env) {
|
|
|
10669
11530
|
|
|
10670
11531
|
// src/index.ts
|
|
10671
11532
|
loadDotenv({ quiet: true });
|
|
11533
|
+
installGlobalProxyDispatcher();
|
|
10672
11534
|
var EXIT_ALREADY_RUNNING = 3;
|
|
10673
11535
|
var config = resolveServerConfig();
|
|
10674
11536
|
var existingLock = await liveServerLock(config.root);
|
|
@@ -10680,23 +11542,35 @@ if (existingLock) {
|
|
|
10680
11542
|
process.exit(EXIT_ALREADY_RUNNING);
|
|
10681
11543
|
}
|
|
10682
11544
|
var deps = buildAppDeps(config);
|
|
11545
|
+
applyProxySettings({
|
|
11546
|
+
proxyForApp: deps.serverSettingsRepo.getProxyForApp(),
|
|
11547
|
+
proxyUrl: deps.serverSettingsRepo.getProxyUrl()
|
|
11548
|
+
});
|
|
10683
11549
|
var app = createApp(deps);
|
|
10684
11550
|
var seededAdminPassword = await deps.authService.seedAdmin();
|
|
10685
|
-
if (
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
11551
|
+
if (config.desktopToken === null) {
|
|
11552
|
+
if (seededAdminPassword !== null) {
|
|
11553
|
+
storeInitialAdminPassword(config.root, seededAdminPassword);
|
|
11554
|
+
}
|
|
11555
|
+
if (deps.authService.adminPasswordIsInitial()) {
|
|
11556
|
+
const initialPassword = seededAdminPassword ?? readInitialAdminPassword(config.root);
|
|
11557
|
+
if (initialPassword !== null) {
|
|
11558
|
+
console.log(renderInitialPasswordNotice(ADMIN_USER_ID, initialPassword));
|
|
11559
|
+
}
|
|
11560
|
+
} else {
|
|
11561
|
+
clearInitialAdminPassword(config.root);
|
|
11562
|
+
}
|
|
10689
11563
|
}
|
|
10690
11564
|
await deps.scheduler.start();
|
|
10691
11565
|
deps.goalsRepo.abortOrphanedActive();
|
|
10692
11566
|
var appHost = loopbackHostRoles(config.host)?.app ?? config.host;
|
|
10693
11567
|
var ipv6Loopback = null;
|
|
10694
11568
|
function writePortFile(file, port) {
|
|
10695
|
-
|
|
11569
|
+
fs23.mkdirSync(path19.dirname(file), { recursive: true });
|
|
10696
11570
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
10697
|
-
|
|
11571
|
+
fs23.writeFileSync(tmp, `${port}
|
|
10698
11572
|
`);
|
|
10699
|
-
|
|
11573
|
+
fs23.renameSync(tmp, file);
|
|
10700
11574
|
}
|
|
10701
11575
|
var server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
|
|
10702
11576
|
console.log(`penguin-server started: http://${appHost}:${info.port}`);
|
|
@@ -10723,7 +11597,7 @@ function cleanupInstanceFiles() {
|
|
|
10723
11597
|
releaseServerLock(config.root);
|
|
10724
11598
|
if (config.portFile !== null) {
|
|
10725
11599
|
try {
|
|
10726
|
-
|
|
11600
|
+
fs23.rmSync(config.portFile, { force: true });
|
|
10727
11601
|
} catch {
|
|
10728
11602
|
}
|
|
10729
11603
|
}
|