@goodfish/jarvy 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/THIRD_PARTY_NOTICE.md +1 -1
- package/dist/server/cli.js +1443 -324
- package/dist/web/assets/index-Ds2S6THj.js +536 -0
- package/dist/web/assets/index-TrYYVs4D.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -3
- package/dist/web/assets/index-DoVuF8GU.js +0 -534
- package/dist/web/assets/index-Dsjq2IH9.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/server/cli.ts
|
|
4
4
|
import { existsSync as existsSync2 } from "node:fs";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { dirname as
|
|
6
|
+
import { dirname as dirname8, join as join11 } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
// src/server/api/server.ts
|
|
@@ -12,7 +12,7 @@ import { getRequestListener } from "@hono/node-server";
|
|
|
12
12
|
|
|
13
13
|
// src/server/api/app.ts
|
|
14
14
|
import { Hono as Hono10 } from "hono";
|
|
15
|
-
import { HTTPException as
|
|
15
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
16
16
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
17
17
|
|
|
18
18
|
// src/server/api/middleware/security.ts
|
|
@@ -184,6 +184,7 @@ function installAuthentication(api, account) {
|
|
|
184
184
|
|
|
185
185
|
// src/server/api/routes/bots.ts
|
|
186
186
|
import { Hono as Hono4 } from "hono";
|
|
187
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
187
188
|
|
|
188
189
|
// src/server/api/views/bot-status.ts
|
|
189
190
|
function botStatusView(status) {
|
|
@@ -379,7 +380,18 @@ function botRoutes(bots) {
|
|
|
379
380
|
routes.get("/", (c) => c.json({ bots: bots.list() }));
|
|
380
381
|
routes.post("/", jsonBody, async (c) => {
|
|
381
382
|
const input = await readJson(c);
|
|
382
|
-
return c.json(bots.create(textField(input, "name")), 201);
|
|
383
|
+
return c.json(bots.create(textField(input, "name"), input.environment === void 0 ? void 0 : environmentKind(input.environment)), 201);
|
|
384
|
+
});
|
|
385
|
+
routes.put("/:id/environment", jsonBody, async (c) => {
|
|
386
|
+
const input = await readJson(c), id = c.req.param("id");
|
|
387
|
+
if (input.hostGranted === void 0) return c.json(bots.setHome(id, environmentKind(input.home)));
|
|
388
|
+
if (typeof input.hostGranted !== "boolean") throw new HTTPException7(400, { message: "hostGranted must be true or false" });
|
|
389
|
+
return c.json(bots.grantHost(id, input.hostGranted));
|
|
390
|
+
});
|
|
391
|
+
routes.post("/:id/computer/:action", async (c) => {
|
|
392
|
+
const action = c.req.param("action");
|
|
393
|
+
if (action !== "start" && action !== "stop" && action !== "reset") throw new HTTPException7(404, { message: "Not found" });
|
|
394
|
+
return c.json(await bots.computer(c.req.param("id"), action));
|
|
383
395
|
});
|
|
384
396
|
routes.get("/:id", (c) => c.json(bots.describe(c.req.param("id"))));
|
|
385
397
|
routes.delete("/:id", async (c) => {
|
|
@@ -428,10 +440,14 @@ function botRoutes(bots) {
|
|
|
428
440
|
routes.route("/", activityRoutes(bots));
|
|
429
441
|
return routes;
|
|
430
442
|
}
|
|
443
|
+
function environmentKind(value2) {
|
|
444
|
+
if (value2 !== "computer" && value2 !== "host") throw new HTTPException7(400, { message: "environment must be computer or host" });
|
|
445
|
+
return value2;
|
|
446
|
+
}
|
|
431
447
|
|
|
432
448
|
// src/server/api/routes/chats.ts
|
|
433
449
|
import { Hono as Hono5 } from "hono";
|
|
434
|
-
import { HTTPException as
|
|
450
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
435
451
|
|
|
436
452
|
// src/server/api/views/download.ts
|
|
437
453
|
var MEDIA_TYPES = /* @__PURE__ */ new Set(["video/webm", "video/mp4", "video/quicktime", "audio/mpeg", "audio/wav", "audio/x-wav", "audio/ogg", "audio/webm", "audio/mp4"]);
|
|
@@ -484,16 +500,16 @@ function chatRoutes({ chats, bots }) {
|
|
|
484
500
|
try {
|
|
485
501
|
name2 = decodeURIComponent(c.req.header("x-file-name") ?? "");
|
|
486
502
|
} catch {
|
|
487
|
-
throw new
|
|
503
|
+
throw new HTTPException8(400, { message: "Invalid filename" });
|
|
488
504
|
}
|
|
489
505
|
const data = Buffer.from(await c.req.arrayBuffer());
|
|
490
|
-
if (!data.length) throw new
|
|
506
|
+
if (!data.length) throw new HTTPException8(400, { message: "Empty attachment" });
|
|
491
507
|
const mime = (c.req.header("content-type") ?? "application/octet-stream").slice(0, 100);
|
|
492
508
|
return c.json(chats.upload(c.get("actorId"), c.req.param("id"), name2, mime, data), 201);
|
|
493
509
|
});
|
|
494
510
|
routes.delete("/:id", async (c) => {
|
|
495
511
|
const id = c.req.param("id");
|
|
496
|
-
if (!await chats.delete(c.get("actorId"), id)) throw new
|
|
512
|
+
if (!await chats.delete(c.get("actorId"), id)) throw new HTTPException8(404, { message: "Chat not found" });
|
|
497
513
|
await bots.chatDeleted(id);
|
|
498
514
|
return c.json({ ok: true });
|
|
499
515
|
});
|
|
@@ -503,7 +519,7 @@ function attachmentRoutes(chats) {
|
|
|
503
519
|
const routes = new Hono5();
|
|
504
520
|
routes.get("/:id", (c) => {
|
|
505
521
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
506
|
-
if (!attachment) throw new
|
|
522
|
+
if (!attachment) throw new HTTPException8(404, { message: "Attachment not found" });
|
|
507
523
|
const size = attachment.data.length, range = byteRange(c.req.header("Range"), size);
|
|
508
524
|
const headers2 = {
|
|
509
525
|
"Content-Type": downloadType(attachment.file.mimeType),
|
|
@@ -520,14 +536,14 @@ function attachmentRoutes(chats) {
|
|
|
520
536
|
});
|
|
521
537
|
routes.get("/:id/run", (c) => {
|
|
522
538
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
523
|
-
if (!attachment) throw new
|
|
539
|
+
if (!attachment) throw new HTTPException8(404, { message: "Attachment not found" });
|
|
524
540
|
const { name: name2, mimeType } = attachment.file;
|
|
525
|
-
if (!/\.html?$/i.test(name2) && mimeType !== "text/html") throw new
|
|
541
|
+
if (!/\.html?$/i.test(name2) && mimeType !== "text/html") throw new HTTPException8(415, { message: "Only HTML attachments can run" });
|
|
526
542
|
c.set("sandboxedDocument", true);
|
|
527
543
|
return c.body(new Uint8Array(attachment.data), 200, { "Content-Type": "text/html; charset=utf-8", "Content-Length": String(attachment.data.length) });
|
|
528
544
|
});
|
|
529
545
|
routes.delete("/:id", (c) => {
|
|
530
|
-
if (!chats.removePendingAttachment(c.get("actorId"), c.req.param("id"))) throw new
|
|
546
|
+
if (!chats.removePendingAttachment(c.get("actorId"), c.req.param("id"))) throw new HTTPException8(404, { message: "Pending attachment not found" });
|
|
531
547
|
return c.json({ ok: true });
|
|
532
548
|
});
|
|
533
549
|
return routes;
|
|
@@ -638,13 +654,13 @@ data: ${JSON.stringify(data)}
|
|
|
638
654
|
|
|
639
655
|
// src/server/api/routes/channels.ts
|
|
640
656
|
import { Hono as Hono8 } from "hono";
|
|
641
|
-
import { HTTPException as
|
|
657
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
642
658
|
function channelRoutes({ chats }) {
|
|
643
659
|
const routes = new Hono8();
|
|
644
660
|
const channels = chats.channels;
|
|
645
661
|
const chat = (actorId, id) => {
|
|
646
662
|
const found = chats.get(actorId, id);
|
|
647
|
-
if (!found) throw new
|
|
663
|
+
if (!found) throw new HTTPException9(404, { message: "Chat not found" });
|
|
648
664
|
return chatView(found, (member) => chats.member(member));
|
|
649
665
|
};
|
|
650
666
|
routes.get("/members", (c) => c.json({ members: channels.everyone(c.get("actorId")) }));
|
|
@@ -665,7 +681,7 @@ function channelRoutes({ chats }) {
|
|
|
665
681
|
routes.post("/sections", jsonBody, async (c) => c.json(channels.createSection(c.get("actorId"), await readJson(c)), 201));
|
|
666
682
|
routes.patch("/sections/:id", jsonBody, async (c) => c.json(channels.updateSection(c.get("actorId"), c.req.param("id"), await readJson(c))));
|
|
667
683
|
routes.delete("/sections/:id", (c) => {
|
|
668
|
-
if (!channels.deleteSection(c.get("actorId"), c.req.param("id"))) throw new
|
|
684
|
+
if (!channels.deleteSection(c.get("actorId"), c.req.param("id"))) throw new HTTPException9(404, { message: "Section not found" });
|
|
669
685
|
return c.json({ ok: true });
|
|
670
686
|
});
|
|
671
687
|
routes.post("/channels", jsonBody, async (c) => {
|
|
@@ -681,7 +697,7 @@ function channelRoutes({ chats }) {
|
|
|
681
697
|
routes.get("/chats/:id", (c) => c.json(chat(c.get("actorId"), c.req.param("id"))));
|
|
682
698
|
routes.get("/messages/:id", (c) => {
|
|
683
699
|
const found = chats.message(c.get("actorId"), c.req.param("id"));
|
|
684
|
-
if (!found) throw new
|
|
700
|
+
if (!found) throw new HTTPException9(404, { message: "Message not found" });
|
|
685
701
|
return c.json(messageView(found));
|
|
686
702
|
});
|
|
687
703
|
routes.post("/messages/:id/thread", (c) => {
|
|
@@ -786,7 +802,7 @@ function createHttpApp(services, webRoot) {
|
|
|
786
802
|
api.route("/", eventRoutes(services));
|
|
787
803
|
api.route("/", liveRoutes(services));
|
|
788
804
|
api.all("*", () => {
|
|
789
|
-
throw new
|
|
805
|
+
throw new HTTPException10(404, { message: "API route not found" });
|
|
790
806
|
});
|
|
791
807
|
app.route("/api", api);
|
|
792
808
|
app.get("*", serveStatic({ root: webRoot, onFound: (path, c) => {
|
|
@@ -796,8 +812,8 @@ function createHttpApp(services, webRoot) {
|
|
|
796
812
|
c.header("Cache-Control", "no-store");
|
|
797
813
|
} }));
|
|
798
814
|
app.all("*", (c) => {
|
|
799
|
-
if (c.req.method !== "GET" && c.req.method !== "HEAD") throw new
|
|
800
|
-
throw new
|
|
815
|
+
if (c.req.method !== "GET" && c.req.method !== "HEAD") throw new HTTPException10(405, { message: "Method not allowed" });
|
|
816
|
+
throw new HTTPException10(404, { message: "Web assets missing; run npm run build" });
|
|
801
817
|
});
|
|
802
818
|
return app;
|
|
803
819
|
}
|
|
@@ -813,10 +829,10 @@ var BOT_COLORS = ["gray", "blue", "green", "amber", "rose", "violet"];
|
|
|
813
829
|
var AGENT_BOT_ID = "00000000-0000-4000-8000-000000000001";
|
|
814
830
|
|
|
815
831
|
// src/server/infra/storage/store.ts
|
|
816
|
-
import { createHash, randomBytes, randomInt as randomInt3, randomUUID as
|
|
832
|
+
import { createHash, randomBytes, randomInt as randomInt3, randomUUID as randomUUID8, scryptSync, timingSafeEqual } from "node:crypto";
|
|
817
833
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
818
834
|
import { homedir } from "node:os";
|
|
819
|
-
import { dirname, join, resolve } from "node:path";
|
|
835
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
820
836
|
import { DatabaseSync } from "node:sqlite";
|
|
821
837
|
|
|
822
838
|
// src/server/infra/storage/migrations/composition-schema.ts
|
|
@@ -938,13 +954,13 @@ function migrateChannels(db) {
|
|
|
938
954
|
SELECT cm.chat_id, cm.member_id, COALESCE((SELECT MAX(sequence) FROM messages m WHERE m.chat_id=cm.chat_id), 0) FROM chat_members cm;
|
|
939
955
|
`);
|
|
940
956
|
const taken = new Set(db.prepare("SELECT username FROM account").all().map((row) => row.username.toLowerCase()));
|
|
941
|
-
const
|
|
957
|
+
const rename3 = db.prepare("UPDATE bots SET name=? WHERE id=?");
|
|
942
958
|
for (const bot of db.prepare("SELECT id,name FROM bots ORDER BY created_at,id").all()) {
|
|
943
959
|
const base = bot.name.replace(/@/g, "").trim() || "Bot";
|
|
944
960
|
let name2 = base;
|
|
945
961
|
for (let suffix = 2; taken.has(name2.toLowerCase()); suffix++) name2 = `${base} ${suffix}`;
|
|
946
962
|
taken.add(name2.toLowerCase());
|
|
947
|
-
if (name2 !== bot.name)
|
|
963
|
+
if (name2 !== bot.name) rename3.run(name2, bot.id);
|
|
948
964
|
}
|
|
949
965
|
db.exec("PRAGMA user_version=12; COMMIT;");
|
|
950
966
|
} catch (error) {
|
|
@@ -1123,6 +1139,33 @@ function migrateAgent(db) {
|
|
|
1123
1139
|
}
|
|
1124
1140
|
}
|
|
1125
1141
|
|
|
1142
|
+
// src/server/infra/storage/migrations/environment-schema.ts
|
|
1143
|
+
function migrateEnvironments(db) {
|
|
1144
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1145
|
+
if (version >= 20) return;
|
|
1146
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1147
|
+
try {
|
|
1148
|
+
db.exec(`
|
|
1149
|
+
CREATE TABLE environments(id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES members(id),
|
|
1150
|
+
kind TEXT NOT NULL CHECK(kind IN ('host','computer')), name TEXT NOT NULL DEFAULT '',
|
|
1151
|
+
connection TEXT NOT NULL DEFAULT '{}', directory TEXT, bot_id TEXT REFERENCES bots(id),
|
|
1152
|
+
created_at INTEGER NOT NULL, deleted_at INTEGER);
|
|
1153
|
+
CREATE UNIQUE INDEX environments_host ON environments(owner_id) WHERE kind='host' AND deleted_at IS NULL;
|
|
1154
|
+
CREATE UNIQUE INDEX environments_computer ON environments(bot_id) WHERE kind='computer' AND deleted_at IS NULL;
|
|
1155
|
+
CREATE TABLE environment_grants(environment_id TEXT NOT NULL REFERENCES environments(id),
|
|
1156
|
+
bot_id TEXT NOT NULL REFERENCES bots(id), granted_by TEXT NOT NULL REFERENCES members(id),
|
|
1157
|
+
granted_at INTEGER NOT NULL, PRIMARY KEY(environment_id, bot_id));
|
|
1158
|
+
ALTER TABLE bots ADD COLUMN home_environment_id TEXT REFERENCES environments(id);
|
|
1159
|
+
ALTER TABLE bots ADD COLUMN current_environment_id TEXT REFERENCES environments(id);
|
|
1160
|
+
PRAGMA user_version=20;
|
|
1161
|
+
`);
|
|
1162
|
+
db.exec("COMMIT");
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
db.exec("ROLLBACK");
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1126
1169
|
// src/server/infra/storage/migrations/schema.ts
|
|
1127
1170
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1128
1171
|
|
|
@@ -1245,7 +1288,7 @@ function migrateRuntime(db) {
|
|
|
1245
1288
|
// src/server/infra/storage/migrations/schema.ts
|
|
1246
1289
|
function migrateDatabase(db) {
|
|
1247
1290
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1248
|
-
if (version >
|
|
1291
|
+
if (version > 20) throw new Error(`Unsupported database version: ${version}`);
|
|
1249
1292
|
migratePrototype(db);
|
|
1250
1293
|
if (version >= 5) {
|
|
1251
1294
|
migrateRuntime(db);
|
|
@@ -1262,6 +1305,7 @@ function migrateDatabase(db) {
|
|
|
1262
1305
|
migrateQuotes(db);
|
|
1263
1306
|
migrateTitles(db);
|
|
1264
1307
|
migrateAgent(db);
|
|
1308
|
+
migrateEnvironments(db);
|
|
1265
1309
|
return;
|
|
1266
1310
|
}
|
|
1267
1311
|
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
@@ -1336,6 +1380,7 @@ function migrateDatabase(db) {
|
|
|
1336
1380
|
migrateQuotes(db);
|
|
1337
1381
|
migrateTitles(db);
|
|
1338
1382
|
migrateAgent(db);
|
|
1383
|
+
migrateEnvironments(db);
|
|
1339
1384
|
}
|
|
1340
1385
|
|
|
1341
1386
|
// src/server/infra/storage/chat-repository.ts
|
|
@@ -1915,7 +1960,7 @@ var ChatRepository = class {
|
|
|
1915
1960
|
return rows2.map(({ historyId, toolCallId, ...row }) => ({ ...row, source: historyId === null ? null : { historyId, toolCallId } }));
|
|
1916
1961
|
}
|
|
1917
1962
|
messageFromRow(row) {
|
|
1918
|
-
const attachments = this.db.prepare("SELECT id,name,mime_type AS mimeType,size FROM attachments WHERE message_id=? ORDER BY created_at,
|
|
1963
|
+
const attachments = this.db.prepare("SELECT id,name,mime_type AS mimeType,size FROM attachments WHERE message_id=? ORDER BY created_at,rowid").all(row.id);
|
|
1919
1964
|
return {
|
|
1920
1965
|
id: row.id,
|
|
1921
1966
|
chatId: row.chatId,
|
|
@@ -2176,9 +2221,112 @@ var RuntimeRepository = class {
|
|
|
2176
2221
|
};
|
|
2177
2222
|
|
|
2178
2223
|
// src/server/infra/pi/catalog.ts
|
|
2179
|
-
import {
|
|
2180
|
-
|
|
2181
|
-
|
|
2224
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2225
|
+
import { dirname, join } from "node:path";
|
|
2226
|
+
import { builtinProviders, getBuiltinModelDataGeneratedAt } from "@earendil-works/pi-ai/providers/all";
|
|
2227
|
+
var CATALOG_URL = "https://pi.dev/api/models";
|
|
2228
|
+
var REFRESH_MS = 4 * 60 * 60 * 1e3;
|
|
2229
|
+
var TIMEOUT_MS = 15e3;
|
|
2230
|
+
var modelApis = new Set(builtinProviders().flatMap((provider) => provider.getModels().map((model) => model.api)));
|
|
2231
|
+
var ModelCatalog = class {
|
|
2232
|
+
path;
|
|
2233
|
+
kept;
|
|
2234
|
+
loaded = false;
|
|
2235
|
+
overlaid;
|
|
2236
|
+
loading;
|
|
2237
|
+
constructor(dataDir) {
|
|
2238
|
+
this.path = join(dataDir, "pi", "models.json");
|
|
2239
|
+
}
|
|
2240
|
+
providers() {
|
|
2241
|
+
this.refresh();
|
|
2242
|
+
return this.overlaid ?? builtinProviders();
|
|
2243
|
+
}
|
|
2244
|
+
/** Newer entries for one provider's models (a connection's catalog comes from Pi with its credentials). */
|
|
2245
|
+
overlay(providerId, models) {
|
|
2246
|
+
return merge(models, this.entries(providerId));
|
|
2247
|
+
}
|
|
2248
|
+
/** Loads the kept catalog, then checks pi.dev when the last check is 4 hours old; never waits. */
|
|
2249
|
+
refresh() {
|
|
2250
|
+
const due = process.env.PI_OFFLINE === void 0 && Date.now() - (this.kept?.checkedAt ?? 0) >= REFRESH_MS;
|
|
2251
|
+
if (this.loading || this.loaded && !due) return;
|
|
2252
|
+
this.loading = (async () => {
|
|
2253
|
+
if (!this.loaded) {
|
|
2254
|
+
this.kept = await this.read();
|
|
2255
|
+
this.loaded = true;
|
|
2256
|
+
this.apply();
|
|
2257
|
+
}
|
|
2258
|
+
if (!due) return;
|
|
2259
|
+
await this.fetch();
|
|
2260
|
+
this.apply();
|
|
2261
|
+
})().catch((error) => console.warn("[models] catalog refresh failed", { error: error instanceof Error ? error.message : String(error) })).finally(() => {
|
|
2262
|
+
this.loading = void 0;
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
async fetch() {
|
|
2266
|
+
const previous = this.kept;
|
|
2267
|
+
const checkedAt = Date.now();
|
|
2268
|
+
try {
|
|
2269
|
+
const response = await fetch(CATALOG_URL, {
|
|
2270
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
2271
|
+
headers: { accept: "application/json", "user-agent": "Jarvy", ...previous?.etag ? { "if-none-match": previous.etag } : {} }
|
|
2272
|
+
});
|
|
2273
|
+
if (response.status === 304 && previous) {
|
|
2274
|
+
this.kept = { ...previous, checkedAt };
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2278
|
+
const body = await response.json();
|
|
2279
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("not a catalog");
|
|
2280
|
+
const lastModified = Date.parse(response.headers.get("last-modified") ?? "");
|
|
2281
|
+
this.kept = { models: body, etag: response.headers.get("etag") ?? void 0, lastModified: Number.isNaN(lastModified) ? 0 : lastModified, checkedAt };
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
this.kept = { ...previous ?? { models: {}, lastModified: 0 }, checkedAt };
|
|
2284
|
+
throw error;
|
|
2285
|
+
} finally {
|
|
2286
|
+
await this.write().catch((error) => console.warn("[models] catalog not saved", { error: error instanceof Error ? error.message : String(error) }));
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
apply() {
|
|
2290
|
+
this.overlaid = builtinProviders().map((provider) => {
|
|
2291
|
+
const entries = this.entries(provider.id);
|
|
2292
|
+
if (!entries.length) return provider;
|
|
2293
|
+
const models = merge(provider.getModels(), entries);
|
|
2294
|
+
return { ...provider, getModels: () => models };
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
entries(providerId) {
|
|
2298
|
+
const kept = this.kept, generatedAt = getBuiltinModelDataGeneratedAt();
|
|
2299
|
+
if (!kept || generatedAt !== void 0 && kept.lastModified <= generatedAt) return [];
|
|
2300
|
+
const models = kept.models[providerId];
|
|
2301
|
+
if (!models || typeof models !== "object") return [];
|
|
2302
|
+
return Object.values(models).filter((model) => typeof model?.id === "string" && typeof model.api === "string" && modelApis.has(model.api) && (model.type === void 0 || model.type === "chat")).map((model) => ({ ...model, provider: providerId }));
|
|
2303
|
+
}
|
|
2304
|
+
async read() {
|
|
2305
|
+
try {
|
|
2306
|
+
const kept = JSON.parse(await readFile(this.path, "utf8"));
|
|
2307
|
+
return kept && typeof kept.models === "object" ? kept : void 0;
|
|
2308
|
+
} catch {
|
|
2309
|
+
return void 0;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
async write() {
|
|
2313
|
+
await mkdir(dirname(this.path), { recursive: true, mode: 448 });
|
|
2314
|
+
const next = `${this.path}.tmp`;
|
|
2315
|
+
await writeFile(next, JSON.stringify(this.kept), { mode: 384 });
|
|
2316
|
+
await rename(next, this.path);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
function merge(models, newer) {
|
|
2320
|
+
const merged = [...models];
|
|
2321
|
+
for (const model of newer) {
|
|
2322
|
+
const index = merged.findIndex((entry) => entry.id === model.id);
|
|
2323
|
+
if (index >= 0) merged[index] = model;
|
|
2324
|
+
else merged.push(model);
|
|
2325
|
+
}
|
|
2326
|
+
return merged;
|
|
2327
|
+
}
|
|
2328
|
+
function providerCatalog(providers) {
|
|
2329
|
+
return providers.flatMap((provider) => {
|
|
2182
2330
|
const models = provider.getModels();
|
|
2183
2331
|
if (!models.length && provider.getAllModels?.().length) return [];
|
|
2184
2332
|
const common = { id: provider.id, api: models[0]?.api ?? "openai-completions", modelCount: models.length };
|
|
@@ -2188,7 +2336,6 @@ function providerCatalog() {
|
|
|
2188
2336
|
];
|
|
2189
2337
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2190
2338
|
}
|
|
2191
|
-
var modelApis = new Set(builtinProviders().flatMap((provider) => provider.getModels().map((model) => model.api)));
|
|
2192
2339
|
|
|
2193
2340
|
// src/server/infra/storage/credential-repository.ts
|
|
2194
2341
|
function credentialValue(value2) {
|
|
@@ -2273,8 +2420,92 @@ var AgentRepository = class {
|
|
|
2273
2420
|
}
|
|
2274
2421
|
};
|
|
2275
2422
|
|
|
2423
|
+
// src/server/infra/storage/environment-repository.ts
|
|
2424
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2425
|
+
var COLUMNS = "id, owner_id AS ownerId, kind, bot_id AS botId, connection";
|
|
2426
|
+
var record = (row) => ({ id: row.id, ownerId: row.ownerId, kind: row.kind, botId: row.botId, spec: JSON.parse(row.connection) });
|
|
2427
|
+
var EnvironmentRepository = class {
|
|
2428
|
+
constructor(db, ownerId) {
|
|
2429
|
+
this.db = db;
|
|
2430
|
+
this.ownerId = ownerId;
|
|
2431
|
+
}
|
|
2432
|
+
get(id) {
|
|
2433
|
+
const row = this.db.prepare(`SELECT ${COLUMNS} FROM environments WHERE id=? AND deleted_at IS NULL`).get(id);
|
|
2434
|
+
return row ? record(row) : null;
|
|
2435
|
+
}
|
|
2436
|
+
/** The owner's computer. */
|
|
2437
|
+
host() {
|
|
2438
|
+
const owner = this.ownerId();
|
|
2439
|
+
const find = () => this.db.prepare(`SELECT ${COLUMNS} FROM environments WHERE owner_id=? AND kind='host' AND deleted_at IS NULL`).get(owner);
|
|
2440
|
+
const found = find();
|
|
2441
|
+
if (found) return record(found);
|
|
2442
|
+
this.db.prepare("INSERT OR IGNORE INTO environments(id,owner_id,kind,created_at) VALUES(?,?,'host',?)").run(randomUUID7(), owner, Date.now());
|
|
2443
|
+
return record(find());
|
|
2444
|
+
}
|
|
2445
|
+
computerOf(botId) {
|
|
2446
|
+
const row = this.db.prepare(`SELECT ${COLUMNS} FROM environments WHERE bot_id=? AND kind='computer' AND deleted_at IS NULL`).get(botId);
|
|
2447
|
+
return row ? record(row) : null;
|
|
2448
|
+
}
|
|
2449
|
+
/** Every live Bot's computer. */
|
|
2450
|
+
computers() {
|
|
2451
|
+
return this.db.prepare(`SELECT e.id, e.owner_id AS ownerId, e.kind, e.bot_id AS botId, e.connection FROM environments e JOIN bots b ON b.id=e.bot_id
|
|
2452
|
+
WHERE e.kind='computer' AND e.deleted_at IS NULL AND b.deleted_at IS NULL`).all().map(record);
|
|
2453
|
+
}
|
|
2454
|
+
/** Gives the Bot a computer, reached by the container's name. */
|
|
2455
|
+
addComputer(botId, container) {
|
|
2456
|
+
const existing = this.computerOf(botId);
|
|
2457
|
+
if (existing) return existing;
|
|
2458
|
+
const id = randomUUID7();
|
|
2459
|
+
this.db.prepare("INSERT INTO environments(id,owner_id,kind,connection,directory,bot_id,created_at) VALUES(?,?,'computer',?,'/workspace',?,?)").run(id, this.ownerId(), JSON.stringify({ container }), botId, Date.now());
|
|
2460
|
+
return this.get(id);
|
|
2461
|
+
}
|
|
2462
|
+
/** The Bot's computer is gone: whoever was placed on it is back on the owner's computer. */
|
|
2463
|
+
dropComputer(botId) {
|
|
2464
|
+
const computer = this.computerOf(botId);
|
|
2465
|
+
if (!computer) return;
|
|
2466
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2467
|
+
try {
|
|
2468
|
+
this.db.prepare("UPDATE bots SET home_environment_id=NULL WHERE home_environment_id=?").run(computer.id);
|
|
2469
|
+
this.db.prepare("UPDATE bots SET current_environment_id=NULL WHERE current_environment_id=?").run(computer.id);
|
|
2470
|
+
this.db.prepare("DELETE FROM environment_grants WHERE environment_id=?").run(computer.id);
|
|
2471
|
+
this.db.prepare("UPDATE environments SET deleted_at=? WHERE id=?").run(Date.now(), computer.id);
|
|
2472
|
+
this.db.exec("COMMIT");
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
this.db.exec("ROLLBACK");
|
|
2475
|
+
throw error;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
/** The Bot's default environment and the one it works in now. */
|
|
2479
|
+
placement(botId) {
|
|
2480
|
+
const row = this.db.prepare("SELECT home_environment_id AS home, current_environment_id AS current FROM bots WHERE id=?").get(botId);
|
|
2481
|
+
const home = row?.home && this.get(row.home) || this.host();
|
|
2482
|
+
const current = row?.current && this.get(row.current) || home;
|
|
2483
|
+
return { home, current };
|
|
2484
|
+
}
|
|
2485
|
+
/** Makes an environment the Bot's default and moves the Bot there. */
|
|
2486
|
+
setHome(botId, environmentId) {
|
|
2487
|
+
this.db.prepare("UPDATE bots SET home_environment_id=?, current_environment_id=? WHERE id=?").run(environmentId, environmentId, botId);
|
|
2488
|
+
}
|
|
2489
|
+
/** Moves the Bot without changing its default. */
|
|
2490
|
+
setCurrent(botId, environmentId) {
|
|
2491
|
+
this.db.prepare("UPDATE bots SET current_environment_id=? WHERE id=?").run(environmentId, botId);
|
|
2492
|
+
}
|
|
2493
|
+
/** The environments the owner granted the Bot, oldest grant first. */
|
|
2494
|
+
grants(botId) {
|
|
2495
|
+
return this.db.prepare(`SELECT e.id, e.owner_id AS ownerId, e.kind, e.bot_id AS botId, e.connection FROM environment_grants g JOIN environments e ON e.id=g.environment_id
|
|
2496
|
+
WHERE g.bot_id=? AND e.deleted_at IS NULL ORDER BY g.granted_at, e.id`).all(botId).map(record);
|
|
2497
|
+
}
|
|
2498
|
+
/** A grant lasts until it is revoked (A50); false when nothing changed. */
|
|
2499
|
+
grant(environmentId, botId, by) {
|
|
2500
|
+
return Number(this.db.prepare("INSERT OR IGNORE INTO environment_grants(environment_id,bot_id,granted_by,granted_at) VALUES(?,?,?,?)").run(environmentId, botId, by, Date.now()).changes) > 0;
|
|
2501
|
+
}
|
|
2502
|
+
revoke(environmentId, botId) {
|
|
2503
|
+
return Number(this.db.prepare("DELETE FROM environment_grants WHERE environment_id=? AND bot_id=?").run(environmentId, botId).changes) > 0;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2276
2507
|
// src/server/infra/storage/store.ts
|
|
2277
|
-
var defaultDataDir = () => process.env.JARVY_DATA_DIR ||
|
|
2508
|
+
var defaultDataDir = () => process.env.JARVY_DATA_DIR || join2(homedir(), ".jarvy");
|
|
2278
2509
|
var DATABASE_FILENAME = "jarvy.db";
|
|
2279
2510
|
var SESSION_LIFETIME_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2280
2511
|
function passwordHash(password, salt) {
|
|
@@ -2343,10 +2574,11 @@ var Store = class {
|
|
|
2343
2574
|
runtime;
|
|
2344
2575
|
credentials;
|
|
2345
2576
|
agent;
|
|
2577
|
+
environments;
|
|
2346
2578
|
constructor(dataDir = defaultDataDir()) {
|
|
2347
2579
|
const root = resolve(dataDir);
|
|
2348
2580
|
mkdirSync(root, { recursive: true, mode: 448 });
|
|
2349
|
-
this.path =
|
|
2581
|
+
this.path = join2(root, DATABASE_FILENAME);
|
|
2350
2582
|
this.db = new DatabaseSync(this.path);
|
|
2351
2583
|
chmodSync(this.path, 384);
|
|
2352
2584
|
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;");
|
|
@@ -2360,6 +2592,7 @@ var Store = class {
|
|
|
2360
2592
|
this.runtime = new RuntimeRepository(this.db);
|
|
2361
2593
|
this.credentials = new CredentialRepository(this.db);
|
|
2362
2594
|
this.agent = new AgentRepository(this.db, () => this.userMemberId());
|
|
2595
|
+
this.environments = new EnvironmentRepository(this.db, () => this.userMemberId());
|
|
2363
2596
|
this.agent.ensure();
|
|
2364
2597
|
}
|
|
2365
2598
|
close() {
|
|
@@ -2369,7 +2602,7 @@ var Store = class {
|
|
|
2369
2602
|
installationDeviceId() {
|
|
2370
2603
|
const row = this.db.prepare("SELECT device_id FROM installation WHERE id=1").get();
|
|
2371
2604
|
if (row) return row.device_id;
|
|
2372
|
-
const id =
|
|
2605
|
+
const id = randomUUID8();
|
|
2373
2606
|
this.db.prepare("INSERT INTO installation(id,device_id) VALUES(1,?)").run(id);
|
|
2374
2607
|
return id;
|
|
2375
2608
|
}
|
|
@@ -2417,8 +2650,8 @@ var Store = class {
|
|
|
2417
2650
|
if (token) this.db.prepare("DELETE FROM auth_sessions WHERE token_hash=?").run(tokenHash(token));
|
|
2418
2651
|
}
|
|
2419
2652
|
botFromRow(row) {
|
|
2420
|
-
const workspacePath =
|
|
2421
|
-
mkdirSync(
|
|
2653
|
+
const workspacePath = join2(dirname2(this.path), "bots", row.id, "workspace");
|
|
2654
|
+
mkdirSync(join2(workspacePath, "temp"), { recursive: true, mode: 448 });
|
|
2422
2655
|
return { ...row, workspacePath };
|
|
2423
2656
|
}
|
|
2424
2657
|
listBots() {
|
|
@@ -2445,6 +2678,7 @@ var Store = class {
|
|
|
2445
2678
|
this.db.prepare("DELETE FROM chats WHERE type='dm' AND id IN (SELECT chat_id FROM chat_members WHERE member_id=?)").run(id);
|
|
2446
2679
|
for (const table of ["chat_members", "chat_states", "inbox"]) this.db.prepare(`DELETE FROM ${table} WHERE member_id=?`).run(id);
|
|
2447
2680
|
this.db.prepare("DELETE FROM bot_sessions WHERE bot_id=?").run(id);
|
|
2681
|
+
this.db.prepare("DELETE FROM environment_grants WHERE bot_id=?").run(id);
|
|
2448
2682
|
this.db.exec("COMMIT");
|
|
2449
2683
|
} catch (error) {
|
|
2450
2684
|
this.db.exec("ROLLBACK");
|
|
@@ -2461,7 +2695,7 @@ var Store = class {
|
|
|
2461
2695
|
/** The caller names the Bot (A45: the interface picks a free default in its own language). */
|
|
2462
2696
|
createBot(name2) {
|
|
2463
2697
|
const clean = this.botName(name2);
|
|
2464
|
-
const id =
|
|
2698
|
+
const id = randomUUID8(), createdAt = Date.now();
|
|
2465
2699
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2466
2700
|
try {
|
|
2467
2701
|
this.db.prepare("INSERT INTO members VALUES(?,'bot')").run(id);
|
|
@@ -2533,7 +2767,7 @@ var Store = class {
|
|
|
2533
2767
|
}
|
|
2534
2768
|
const key = input.apiKey === void 0 ? null : requireText(input.apiKey, "API Key", 8192);
|
|
2535
2769
|
if (key && authType !== "api_key") throw new ValidationError("Invalid credential type");
|
|
2536
|
-
const id =
|
|
2770
|
+
const id = randomUUID8(), now = Date.now();
|
|
2537
2771
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2538
2772
|
try {
|
|
2539
2773
|
this.db.prepare("INSERT INTO provider_connections(id,provider_name,display_name,kind,auth_type,base_url,api,created_at,updated_at,owner_id) VALUES(?,?,?,?,?,?,?,?,?,?)").run(id, providerName, displayName, kind, authType, baseUrl, api, now, now, this.userMemberId());
|
|
@@ -2667,7 +2901,6 @@ var AccountService = class {
|
|
|
2667
2901
|
};
|
|
2668
2902
|
|
|
2669
2903
|
// src/server/infra/pi/model.ts
|
|
2670
|
-
import { builtinProviders as builtinProviders2 } from "@earendil-works/pi-ai/providers/all";
|
|
2671
2904
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2672
2905
|
var FORMATS = /* @__PURE__ */ new Set(["openai", "openrouter", "deepseek", "together", "baseten", "zai", "qwen", "chat-template", "qwen-chat-template", "string-thinking", "ant-ling"]);
|
|
2673
2906
|
function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
@@ -2702,12 +2935,12 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2702
2935
|
model.headers = { ...model.headers, ...headers2 };
|
|
2703
2936
|
return model;
|
|
2704
2937
|
}
|
|
2705
|
-
async function modelCatalog(credentials, providerName) {
|
|
2938
|
+
async function modelCatalog(credentials, providerName, catalog) {
|
|
2706
2939
|
const runtime = await ModelRuntime.create({ credentials, modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
|
|
2707
|
-
return runtime.getModels(providerName).map(publicModel);
|
|
2940
|
+
return catalog.overlay(providerName, runtime.getModels(providerName)).map(publicModel);
|
|
2708
2941
|
}
|
|
2709
|
-
function
|
|
2710
|
-
return (
|
|
2942
|
+
function providerModels(providers, providerName) {
|
|
2943
|
+
return (providers.find((provider) => provider.id === providerName)?.getModels() ?? []).map(publicModel);
|
|
2711
2944
|
}
|
|
2712
2945
|
function catalogCompat(model) {
|
|
2713
2946
|
const { thinkingFormat: _format, ...compat } = model.compat ?? {};
|
|
@@ -2749,12 +2982,12 @@ var CredentialCoordinator = class {
|
|
|
2749
2982
|
if (this.tails.get(connectionId) === tail) this.tails.delete(connectionId);
|
|
2750
2983
|
});
|
|
2751
2984
|
if (!signal) return operation;
|
|
2752
|
-
return new Promise((
|
|
2985
|
+
return new Promise((resolve5, reject) => {
|
|
2753
2986
|
const aborted = () => reject(signal.reason);
|
|
2754
2987
|
signal.addEventListener("abort", aborted, { once: true });
|
|
2755
2988
|
operation.then((value2) => {
|
|
2756
2989
|
signal.removeEventListener("abort", aborted);
|
|
2757
|
-
|
|
2990
|
+
resolve5(value2);
|
|
2758
2991
|
}, (error) => {
|
|
2759
2992
|
signal.removeEventListener("abort", aborted);
|
|
2760
2993
|
reject(error);
|
|
@@ -2837,7 +3070,6 @@ function resolveThinking(model, choice) {
|
|
|
2837
3070
|
}
|
|
2838
3071
|
|
|
2839
3072
|
// src/server/infra/pi/autoconfig.ts
|
|
2840
|
-
import { builtinProviders as builtinProviders3 } from "@earendil-works/pi-ai/providers/all";
|
|
2841
3073
|
var VENDORS = [
|
|
2842
3074
|
[/^glm/i, ["zai"]],
|
|
2843
3075
|
[/^deepseek/i, ["deepseek"]],
|
|
@@ -2858,23 +3090,20 @@ function officialScore(provider, id) {
|
|
|
2858
3090
|
return (id.includes("/") ? 0 : 1) + (words.some((word) => provider.includes(word)) ? 1 : 0);
|
|
2859
3091
|
}
|
|
2860
3092
|
var strip = (id) => id.toLowerCase().replace(/^.*\//, "").replace(/-(\d{6,8})$/, "");
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
rows ??= builtinProviders3().flatMap((provider) => provider.getModels().map((model) => ({ provider: provider.id, model, official: officialScore(provider.id, model.id) })));
|
|
2864
|
-
return rows;
|
|
3093
|
+
function rows(providers) {
|
|
3094
|
+
return providers.flatMap((provider) => provider.getModels().map((model) => ({ provider: provider.id, model, official: officialScore(provider.id, model.id) })));
|
|
2865
3095
|
}
|
|
2866
3096
|
var source = (row) => ({ provider: row.provider, modelId: row.model.id, name: row.model.name, api: row.model.api, official: row.official === 2 });
|
|
2867
3097
|
var completeness = (row) => Object.values(row.model.thinkingLevelMap ?? {}).filter((value2) => value2 !== void 0).length + Object.keys(row.model.compat ?? {}).length + (row.model.cost.input > 0 ? 1 : 0);
|
|
2868
|
-
function candidates(query) {
|
|
3098
|
+
function candidates(all, query) {
|
|
2869
3099
|
const q = query.trim().toLowerCase();
|
|
2870
3100
|
if (!q) return [];
|
|
2871
|
-
const all = catalog();
|
|
2872
3101
|
const same3 = all.filter((row) => row.model.id.toLowerCase() === q || strip(row.model.id) === strip(q));
|
|
2873
3102
|
return same3.length ? same3 : all.filter((row) => row.model.id.toLowerCase().includes(strip(q)));
|
|
2874
3103
|
}
|
|
2875
|
-
function similar(query) {
|
|
3104
|
+
function similar(all, query) {
|
|
2876
3105
|
const q = strip(query);
|
|
2877
|
-
return
|
|
3106
|
+
return all.map((row) => ({ row, ...similarity(strip(row.model.id), q) })).filter((item) => item.prefix >= 3).sort((a, b) => b.score - a.score || b.row.official - a.row.official).slice(0, 5).map((item) => item.row);
|
|
2878
3107
|
}
|
|
2879
3108
|
function similarity(a, b) {
|
|
2880
3109
|
let prefix = 0;
|
|
@@ -2911,14 +3140,14 @@ function matched(row, api, alternatives, reseller) {
|
|
|
2911
3140
|
alternatives: alternatives.filter((item) => item !== row).slice(0, 8).map(source)
|
|
2912
3141
|
};
|
|
2913
3142
|
}
|
|
2914
|
-
function autoconfigure(modelId, api, choice) {
|
|
2915
|
-
const hits = candidates(modelId);
|
|
3143
|
+
function autoconfigure(providers, modelId, api, choice) {
|
|
3144
|
+
const all = rows(providers), hits = candidates(all, modelId);
|
|
2916
3145
|
if (choice) {
|
|
2917
|
-
const row =
|
|
3146
|
+
const row = all.find((item) => item.provider === choice.provider && item.model.id === choice.modelId);
|
|
2918
3147
|
if (row) return matched(row, api, hits, row.official < 2);
|
|
2919
3148
|
}
|
|
2920
3149
|
if (!hits.length) {
|
|
2921
|
-
const options = similar(modelId);
|
|
3150
|
+
const options = similar(all, modelId);
|
|
2922
3151
|
return options.length ? { status: "choose", reason: "similar", options: options.map(source) } : { status: "none" };
|
|
2923
3152
|
}
|
|
2924
3153
|
const official = hits.filter((row) => row.official === 2);
|
|
@@ -3002,11 +3231,12 @@ async function discoverModels(input, signal) {
|
|
|
3002
3231
|
|
|
3003
3232
|
// src/server/service/models/service.ts
|
|
3004
3233
|
var ModelService = class {
|
|
3005
|
-
constructor(store, credentials, logins, changed) {
|
|
3234
|
+
constructor(store, credentials, logins, changed, models) {
|
|
3006
3235
|
this.store = store;
|
|
3007
3236
|
this.credentials = credentials;
|
|
3008
3237
|
this.logins = logins;
|
|
3009
3238
|
this.changed = changed;
|
|
3239
|
+
this.models = models;
|
|
3010
3240
|
}
|
|
3011
3241
|
required(value2) {
|
|
3012
3242
|
if (!value2) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
@@ -3041,15 +3271,19 @@ var ModelService = class {
|
|
|
3041
3271
|
return this.required(this.store.deleteConnectionModel(id, modelId));
|
|
3042
3272
|
}
|
|
3043
3273
|
providers() {
|
|
3044
|
-
return providerCatalog();
|
|
3274
|
+
return providerCatalog(this.models.providers());
|
|
3275
|
+
}
|
|
3276
|
+
/** Brings pi.dev's newer catalog entries in the background (A49). */
|
|
3277
|
+
refreshCatalog() {
|
|
3278
|
+
this.models.refresh();
|
|
3045
3279
|
}
|
|
3046
3280
|
builtinModels(provider) {
|
|
3047
|
-
return
|
|
3281
|
+
return providerModels(this.models.providers(), provider);
|
|
3048
3282
|
}
|
|
3049
3283
|
async catalog(id) {
|
|
3050
3284
|
const connection = this.required(this.store.getModelConnection(id));
|
|
3051
3285
|
const credentials = new ConnectionCredentials(this.store.credentials, this.credentials, this.store.userMemberId(), id, connection.providerName);
|
|
3052
|
-
return modelCatalog(credentials, connection.providerName);
|
|
3286
|
+
return modelCatalog(credentials, connection.providerName, this.models);
|
|
3053
3287
|
}
|
|
3054
3288
|
discover(input, signal) {
|
|
3055
3289
|
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
@@ -3062,7 +3296,7 @@ var ModelService = class {
|
|
|
3062
3296
|
if (!key) throw new RuntimeFault("discovery_key", "Enter the API key.", 400);
|
|
3063
3297
|
return discoverModels({ api: String(input.api ?? "openai-completions"), baseUrl: input.baseUrl, key }, signal);
|
|
3064
3298
|
}
|
|
3065
|
-
/** Suggests model fields from
|
|
3299
|
+
/** Suggests model fields from Pi's catalog (with pi.dev's newer entries, A49); the caller decides whether to save them. */
|
|
3066
3300
|
autoconfigure(input) {
|
|
3067
3301
|
const text3 = (value2) => typeof value2 === "string" && value2.trim() && value2.length <= 200 ? value2.trim() : null;
|
|
3068
3302
|
const modelId = text3(input.modelId), api = text3(input.api);
|
|
@@ -3074,7 +3308,7 @@ var ModelService = class {
|
|
|
3074
3308
|
if (!provider || !chosen) throw new RuntimeFault("autoconfig_source", "The setup source isn't valid.", 400);
|
|
3075
3309
|
choice = { provider, modelId: chosen };
|
|
3076
3310
|
}
|
|
3077
|
-
return autoconfigure(modelId, api, choice);
|
|
3311
|
+
return autoconfigure(this.models.providers(), modelId, api, choice);
|
|
3078
3312
|
}
|
|
3079
3313
|
options() {
|
|
3080
3314
|
return this.list().filter((connection) => connection.models.length).map((connection) => ({
|
|
@@ -3101,7 +3335,7 @@ var ModelService = class {
|
|
|
3101
3335
|
};
|
|
3102
3336
|
|
|
3103
3337
|
// src/server/infra/pi/auth.ts
|
|
3104
|
-
import { randomUUID as
|
|
3338
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
3105
3339
|
import { ModelRuntime as ModelRuntime2 } from "@earendil-works/pi-coding-agent";
|
|
3106
3340
|
var CredentialLogins = class {
|
|
3107
3341
|
constructor(store, credentials, changed) {
|
|
@@ -3149,7 +3383,7 @@ var CredentialLogins = class {
|
|
|
3149
3383
|
prompt(flow, prompt) {
|
|
3150
3384
|
const signal = prompt.signal ? AbortSignal.any([flow.controller.signal, prompt.signal]) : flow.controller.signal;
|
|
3151
3385
|
signal.throwIfAborted();
|
|
3152
|
-
const id =
|
|
3386
|
+
const id = randomUUID9();
|
|
3153
3387
|
flow.state.prompt = {
|
|
3154
3388
|
id,
|
|
3155
3389
|
type: prompt.type,
|
|
@@ -3157,7 +3391,7 @@ var CredentialLogins = class {
|
|
|
3157
3391
|
..."placeholder" in prompt ? { placeholder: prompt.placeholder } : {},
|
|
3158
3392
|
...prompt.type === "select" ? { options: prompt.options } : {}
|
|
3159
3393
|
};
|
|
3160
|
-
return new Promise((
|
|
3394
|
+
return new Promise((resolve5, reject) => {
|
|
3161
3395
|
const clear = () => {
|
|
3162
3396
|
signal.removeEventListener("abort", aborted);
|
|
3163
3397
|
flow.answer = void 0;
|
|
@@ -3169,7 +3403,7 @@ var CredentialLogins = class {
|
|
|
3169
3403
|
};
|
|
3170
3404
|
flow.answer = (value2) => {
|
|
3171
3405
|
clear();
|
|
3172
|
-
|
|
3406
|
+
resolve5(value2);
|
|
3173
3407
|
};
|
|
3174
3408
|
signal.addEventListener("abort", aborted, { once: true });
|
|
3175
3409
|
if (signal.aborted) aborted();
|
|
@@ -3234,9 +3468,9 @@ var User = class extends Member {
|
|
|
3234
3468
|
};
|
|
3235
3469
|
|
|
3236
3470
|
// src/server/application.ts
|
|
3237
|
-
import { createHash as
|
|
3238
|
-
import { mkdir as
|
|
3239
|
-
import { join as
|
|
3471
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
3472
|
+
import { mkdir as mkdir5, realpath } from "node:fs/promises";
|
|
3473
|
+
import { join as join9, resolve as resolve3 } from "node:path";
|
|
3240
3474
|
import lockfile from "proper-lockfile";
|
|
3241
3475
|
|
|
3242
3476
|
// src/server/service/chat/mentions.ts
|
|
@@ -3244,12 +3478,12 @@ var WORD = /[\p{Script=Latin}\p{Nd}_]/u;
|
|
|
3244
3478
|
function findMentions(source2, candidates2) {
|
|
3245
3479
|
const text3 = source2.replace(/```[\s\S]*?```|`[^`\n]*`/g, (code) => " ".repeat(code.length));
|
|
3246
3480
|
const lower = text3.toLowerCase(), sorted = [...candidates2].sort((a, b) => b.name.length - a.name.length), found = [];
|
|
3247
|
-
for (let
|
|
3248
|
-
if (
|
|
3481
|
+
for (let at2 = lower.indexOf("@"); at2 >= 0; at2 = lower.indexOf("@", at2 + 1)) {
|
|
3482
|
+
if (at2 > 0 && WORD.test(text3[at2 - 1])) continue;
|
|
3249
3483
|
const match = sorted.find((candidate) => {
|
|
3250
3484
|
const name2 = candidate.name.toLowerCase();
|
|
3251
|
-
if (!name2 || !lower.startsWith(name2,
|
|
3252
|
-
const next = text3[
|
|
3485
|
+
if (!name2 || !lower.startsWith(name2, at2 + 1)) return false;
|
|
3486
|
+
const next = text3[at2 + 1 + name2.length];
|
|
3253
3487
|
return !(next && WORD.test(next) && WORD.test(name2.at(-1)));
|
|
3254
3488
|
});
|
|
3255
3489
|
if (match && !found.includes(match)) found.push(match);
|
|
@@ -3288,8 +3522,10 @@ var POLICY = {
|
|
|
3288
3522
|
note: { member: "queue", follower: "queue" },
|
|
3289
3523
|
// The owner just gave the agent Bot its first model: it should welcome them now (A48).
|
|
3290
3524
|
onboard: { setup: "interrupt" },
|
|
3291
|
-
// What the agent Bot changed about a Bot rides along with that Bot's next batch, without waking it.
|
|
3292
|
-
|
|
3525
|
+
// What the agent Bot changed about a Bot rides along with that Bot's next batch, without waking it. The owner moving
|
|
3526
|
+
// a Bot to another work environment stops its running turn like an @, so it doesn't go on in the old place (A50); a
|
|
3527
|
+
// Bot not running, and a change of access alone, hear of it with their next batch instead (fish m1012-m1015).
|
|
3528
|
+
change: { agent: "digest", owner: "interrupt" }
|
|
3293
3529
|
};
|
|
3294
3530
|
function urgencyOf(kind, reason) {
|
|
3295
3531
|
return POLICY[kind][reason];
|
|
@@ -3513,7 +3749,7 @@ var ChatQuestions = class {
|
|
|
3513
3749
|
if (question.state === "answered") return Promise.resolve(resultOf(question, question.answer));
|
|
3514
3750
|
if (question.state === "dismissed") return Promise.resolve({ status: "dismissed" });
|
|
3515
3751
|
if (question.state === "ignored") return Promise.reject(new ChatError(409, "The question has expired", "question_expired"));
|
|
3516
|
-
return new Promise((
|
|
3752
|
+
return new Promise((resolve5, reject) => {
|
|
3517
3753
|
const stop = () => {
|
|
3518
3754
|
this.waiting.delete(message.id);
|
|
3519
3755
|
if (this.repository.settleQuestion(message.id, "ignored", null)) this.changed(message.chatId);
|
|
@@ -3526,7 +3762,7 @@ var ChatQuestions = class {
|
|
|
3526
3762
|
signal?.addEventListener("abort", stop, { once: true });
|
|
3527
3763
|
this.waiting.set(message.id, (result) => {
|
|
3528
3764
|
signal?.removeEventListener("abort", stop);
|
|
3529
|
-
|
|
3765
|
+
resolve5(result);
|
|
3530
3766
|
});
|
|
3531
3767
|
});
|
|
3532
3768
|
}
|
|
@@ -3884,13 +4120,13 @@ var ChatService = class {
|
|
|
3884
4120
|
return this.repository.getChat(actorId, chatId);
|
|
3885
4121
|
}
|
|
3886
4122
|
/** The message with up to `context` messages before and after it (read position unchanged). */
|
|
3887
|
-
around(actorId, messageId,
|
|
4123
|
+
around(actorId, messageId, context2) {
|
|
3888
4124
|
const message = this.repository.message(actorId, messageId);
|
|
3889
4125
|
if (!message) throw new ChatError(404, "Message not found");
|
|
3890
4126
|
return [
|
|
3891
|
-
...this.repository.messagesBefore(actorId, message.chatId, message.sequence,
|
|
4127
|
+
...this.repository.messagesBefore(actorId, message.chatId, message.sequence, context2),
|
|
3892
4128
|
message,
|
|
3893
|
-
...this.repository.messages(actorId, message.chatId, message.sequence,
|
|
4129
|
+
...this.repository.messages(actorId, message.chatId, message.sequence, context2)
|
|
3894
4130
|
];
|
|
3895
4131
|
}
|
|
3896
4132
|
/** Exactly the unread messages of a chat, oldest first; the caller moves the read position. */
|
|
@@ -3907,10 +4143,10 @@ var ChatService = class {
|
|
|
3907
4143
|
find(actorId, query, { within, limit = 50 } = {}) {
|
|
3908
4144
|
const messages = this.searchAll(actorId, query, limit, within);
|
|
3909
4145
|
const chats = within ? [] : this.repository.searchChatIds(actorId, query.trim(), 30).map((id) => this.repository.getChat(actorId, id)).filter((chat) => chat !== null);
|
|
3910
|
-
const
|
|
4146
|
+
const where2 = /* @__PURE__ */ new Map();
|
|
3911
4147
|
const chatOf = (id) => {
|
|
3912
|
-
if (!
|
|
3913
|
-
return
|
|
4148
|
+
if (!where2.has(id)) where2.set(id, this.repository.getChat(actorId, id));
|
|
4149
|
+
return where2.get(id);
|
|
3914
4150
|
};
|
|
3915
4151
|
return { chats, messages: messages.flatMap((message) => {
|
|
3916
4152
|
const chat = chatOf(message.chatId);
|
|
@@ -4002,70 +4238,11 @@ var ChatService = class {
|
|
|
4002
4238
|
}
|
|
4003
4239
|
};
|
|
4004
4240
|
|
|
4005
|
-
// src/server/infra/files/attachments.ts
|
|
4006
|
-
import { constants } from "node:fs";
|
|
4007
|
-
import { mkdir, open, realpath, rename, rm } from "node:fs/promises";
|
|
4008
|
-
import { join as join2, resolve as resolve2 } from "node:path";
|
|
4009
|
-
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
4010
|
-
var MAX_FILE = 10 * 1024 * 1024;
|
|
4011
|
-
var AttachmentFiles = class {
|
|
4012
|
-
constructor(dataDir) {
|
|
4013
|
-
this.dataDir = dataDir;
|
|
4014
|
-
}
|
|
4015
|
-
async materialize(file, signal) {
|
|
4016
|
-
const dir = join2(this.dataDir, "attachments", file.id);
|
|
4017
|
-
await mkdir(dir, { recursive: true, mode: 448 });
|
|
4018
|
-
if (await realpath(dir) !== resolve2(dir)) throw new ChatError(400, "Invalid attachment directory");
|
|
4019
|
-
const path = join2(dir, file.name), temporary = join2(dir, `.${randomUUID9()}.tmp`);
|
|
4020
|
-
try {
|
|
4021
|
-
const handle = await open(temporary, "wx", 384);
|
|
4022
|
-
try {
|
|
4023
|
-
await handle.writeFile(file.data, { signal });
|
|
4024
|
-
} finally {
|
|
4025
|
-
await handle.close();
|
|
4026
|
-
}
|
|
4027
|
-
signal?.throwIfAborted();
|
|
4028
|
-
await rename(temporary, path);
|
|
4029
|
-
} finally {
|
|
4030
|
-
await rm(temporary, { force: true });
|
|
4031
|
-
}
|
|
4032
|
-
return { path, name: file.name };
|
|
4033
|
-
}
|
|
4034
|
-
async readOutgoing(workspace, items, signal) {
|
|
4035
|
-
if (items.length > 5) throw new ChatError(400, "Too many attachments");
|
|
4036
|
-
const result = [];
|
|
4037
|
-
for (const item of items) {
|
|
4038
|
-
signal?.throwIfAborted();
|
|
4039
|
-
const path = await realpath(resolve2(workspace, item.path));
|
|
4040
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
4041
|
-
try {
|
|
4042
|
-
const stat = await handle.stat();
|
|
4043
|
-
if (!stat.isFile() || !stat.size || stat.size > MAX_FILE) throw new ChatError(400, "Invalid attachment size or type");
|
|
4044
|
-
const data = Buffer.alloc(Math.min(MAX_FILE + 1, stat.size + 1));
|
|
4045
|
-
let bytes = 0;
|
|
4046
|
-
while (bytes < data.length) {
|
|
4047
|
-
signal?.throwIfAborted();
|
|
4048
|
-
const read = await handle.read(data, bytes, data.length - bytes, bytes);
|
|
4049
|
-
if (!read.bytesRead) break;
|
|
4050
|
-
bytes += read.bytesRead;
|
|
4051
|
-
}
|
|
4052
|
-
if (bytes !== stat.size) throw new ChatError(409, "Attachment changed while reading");
|
|
4053
|
-
result.push({ name: item.name, mimeType: "application/octet-stream", data: data.subarray(0, bytes) });
|
|
4054
|
-
} finally {
|
|
4055
|
-
await handle.close();
|
|
4056
|
-
}
|
|
4057
|
-
}
|
|
4058
|
-
return result;
|
|
4059
|
-
}
|
|
4060
|
-
};
|
|
4061
|
-
|
|
4062
4241
|
// src/server/service/chat/files.ts
|
|
4063
4242
|
var ChatFiles = class {
|
|
4064
|
-
constructor(
|
|
4243
|
+
constructor(chats) {
|
|
4065
4244
|
this.chats = chats;
|
|
4066
|
-
this.files = new AttachmentFiles(dataDir);
|
|
4067
4245
|
}
|
|
4068
|
-
files;
|
|
4069
4246
|
async envelope(botId, messageIds, signal) {
|
|
4070
4247
|
const output = [];
|
|
4071
4248
|
for (const id of messageIds) {
|
|
@@ -4089,7 +4266,7 @@ var ChatFiles = class {
|
|
|
4089
4266
|
sender: message.sender,
|
|
4090
4267
|
content: expandQuotes(message.content, message.quotes, (memberId) => this.chats.member(memberId)?.name ?? memberId),
|
|
4091
4268
|
createdAt: message.createdAt,
|
|
4092
|
-
attachments:
|
|
4269
|
+
attachments: listing(message),
|
|
4093
4270
|
...this.reply(message)
|
|
4094
4271
|
});
|
|
4095
4272
|
}
|
|
@@ -4100,20 +4277,21 @@ var ChatFiles = class {
|
|
|
4100
4277
|
const sender = message.replyTo ? this.chats.member(message.replyTo.senderId) : null;
|
|
4101
4278
|
return message.replyTo && sender ? { replyTo: { id: message.replyTo.id, sender, text: replyExcerpt(message.replyTo) } } : {};
|
|
4102
4279
|
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
if (!attachment) throw new ChatError(404, "Attachment not found");
|
|
4109
|
-
paths.push(await this.files.materialize({ ...attachment, id: item.id, name: item.name }, signal));
|
|
4110
|
-
}
|
|
4111
|
-
return paths;
|
|
4112
|
-
}
|
|
4113
|
-
readOutgoing(workspace, items, signal) {
|
|
4114
|
-
return this.files.readOutgoing(workspace, items, signal);
|
|
4280
|
+
/** A file of a message the actor can read, with its contents. */
|
|
4281
|
+
open(actorId, attachmentId) {
|
|
4282
|
+
const found = this.chats.download(actorId, attachmentId);
|
|
4283
|
+
if (!found) throw new ChatError(404, "File not found, or in a message you can't read");
|
|
4284
|
+
return found;
|
|
4115
4285
|
}
|
|
4116
4286
|
};
|
|
4287
|
+
function listing(message) {
|
|
4288
|
+
return message.attachments.map((file) => ({ id: file.id, name: file.name, type: file.mimeType || "application/octet-stream", size: byteSize(file.size) }));
|
|
4289
|
+
}
|
|
4290
|
+
function byteSize(bytes) {
|
|
4291
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
4292
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
4293
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
4294
|
+
}
|
|
4117
4295
|
|
|
4118
4296
|
// src/server/service/events.ts
|
|
4119
4297
|
var Changes = class {
|
|
@@ -4138,8 +4316,8 @@ var Changes = class {
|
|
|
4138
4316
|
// src/server/service/bot/activity.ts
|
|
4139
4317
|
function activityPage(messages, history, limit) {
|
|
4140
4318
|
const candidates2 = [
|
|
4141
|
-
...messages.map((
|
|
4142
|
-
...history.map((
|
|
4319
|
+
...messages.map((record3) => ({ kind: "chat", record: record3 })),
|
|
4320
|
+
...history.map((record3) => ({ kind: "history", record: record3 }))
|
|
4143
4321
|
];
|
|
4144
4322
|
candidates2.sort((a, b) => {
|
|
4145
4323
|
if (a.record.timestamp !== b.record.timestamp) return b.record.timestamp - a.record.timestamp;
|
|
@@ -4152,8 +4330,8 @@ function activityPage(messages, history, limit) {
|
|
|
4152
4330
|
}
|
|
4153
4331
|
|
|
4154
4332
|
// src/server/service/bot/service.ts
|
|
4155
|
-
import { dirname as
|
|
4156
|
-
import { rm as
|
|
4333
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
4334
|
+
import { rm as rm3 } from "node:fs/promises";
|
|
4157
4335
|
|
|
4158
4336
|
// src/server/infra/pi/history.ts
|
|
4159
4337
|
var value = (input) => JSON.parse(JSON.stringify(input));
|
|
@@ -4270,7 +4448,7 @@ var PiHistoryAdapter = class {
|
|
|
4270
4448
|
// src/server/infra/pi/session.ts
|
|
4271
4449
|
import { existsSync, readFileSync } from "node:fs";
|
|
4272
4450
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
4273
|
-
import { dirname as
|
|
4451
|
+
import { dirname as dirname3, join as join3, resolve as resolve2, sep } from "node:path";
|
|
4274
4452
|
import { createAgentSession, createCodemodeExtension, DefaultResourceLoader, ModelRuntime as ModelRuntime3, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
4275
4453
|
|
|
4276
4454
|
// src/server/infra/pi/http.ts
|
|
@@ -4296,6 +4474,9 @@ function configureHttp(timeoutMs) {
|
|
|
4296
4474
|
// src/server/infra/pi/session.ts
|
|
4297
4475
|
import { Type } from "typebox";
|
|
4298
4476
|
|
|
4477
|
+
// src/server/shared/tools.ts
|
|
4478
|
+
var isToolMedia = (value2) => !!value2 && typeof value2 === "object" && value2.kind === "media" && Array.isArray(value2.images);
|
|
4479
|
+
|
|
4299
4480
|
// src/server/infra/pi/live.ts
|
|
4300
4481
|
var LiveTranslator = class {
|
|
4301
4482
|
message = 0;
|
|
@@ -4358,6 +4539,130 @@ function resultText(result) {
|
|
|
4358
4539
|
}
|
|
4359
4540
|
}
|
|
4360
4541
|
|
|
4542
|
+
// src/server/infra/pi/environment-tools.ts
|
|
4543
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4544
|
+
import { readFile as readLocal, rm } from "node:fs/promises";
|
|
4545
|
+
import { basename } from "node:path";
|
|
4546
|
+
import { createBashToolDefinition, createEditToolDefinition, createReadToolDefinition, createWriteToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4547
|
+
|
|
4548
|
+
// src/server/infra/pi/image-kind.ts
|
|
4549
|
+
var IMAGE_SNIFF_BYTES = 4100;
|
|
4550
|
+
function imageKind(bytes) {
|
|
4551
|
+
if (startsWith(bytes, [255, 216, 255])) return bytes[3] === 247 ? null : "image/jpeg";
|
|
4552
|
+
if (startsWith(bytes, PNG)) return isPng(bytes) && !isAnimatedPng(bytes) ? "image/png" : null;
|
|
4553
|
+
if (ascii(bytes, 0, "GIF87a") || ascii(bytes, 0, "GIF89a")) return "image/gif";
|
|
4554
|
+
if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WEBP")) return "image/webp";
|
|
4555
|
+
if (ascii(bytes, 0, "BM") && isBmp(bytes)) return "image/bmp";
|
|
4556
|
+
return null;
|
|
4557
|
+
}
|
|
4558
|
+
var PNG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
4559
|
+
var isPng = (bytes) => bytes.length >= 16 && u32be(bytes, PNG.length) === 13 && ascii(bytes, 12, "IHDR");
|
|
4560
|
+
function isAnimatedPng(bytes) {
|
|
4561
|
+
let offset = PNG.length;
|
|
4562
|
+
while (offset + 8 <= bytes.length) {
|
|
4563
|
+
const length = u32be(bytes, offset), type = offset + 4;
|
|
4564
|
+
if (ascii(bytes, type, "acTL")) return true;
|
|
4565
|
+
if (ascii(bytes, type, "IDAT")) return false;
|
|
4566
|
+
const next = offset + 8 + length + 4;
|
|
4567
|
+
if (next <= offset || next > bytes.length) return false;
|
|
4568
|
+
offset = next;
|
|
4569
|
+
}
|
|
4570
|
+
return false;
|
|
4571
|
+
}
|
|
4572
|
+
function isBmp(bytes) {
|
|
4573
|
+
if (bytes.length < 26) return false;
|
|
4574
|
+
const size = u32le(bytes, 2), pixels = u32le(bytes, 10), header = u32le(bytes, 14);
|
|
4575
|
+
if (size !== 0 && size < 26) return false;
|
|
4576
|
+
if (pixels < 14 + header) return false;
|
|
4577
|
+
if (size !== 0 && pixels >= size) return false;
|
|
4578
|
+
let planes, bits;
|
|
4579
|
+
if (header === 12) {
|
|
4580
|
+
planes = u16le(bytes, 22);
|
|
4581
|
+
bits = u16le(bytes, 24);
|
|
4582
|
+
} else if (header >= 40 && header <= 124) {
|
|
4583
|
+
if (bytes.length < 30) return false;
|
|
4584
|
+
planes = u16le(bytes, 26);
|
|
4585
|
+
bits = u16le(bytes, 28);
|
|
4586
|
+
} else return false;
|
|
4587
|
+
return planes === 1 && [1, 4, 8, 16, 24, 32].includes(bits);
|
|
4588
|
+
}
|
|
4589
|
+
var at = (bytes, offset) => bytes[offset] ?? 0;
|
|
4590
|
+
var u16le = (bytes, offset) => at(bytes, offset) + (at(bytes, offset + 1) << 8);
|
|
4591
|
+
var u32be = (bytes, offset) => at(bytes, offset) * 16777216 + (at(bytes, offset + 1) << 16) + (at(bytes, offset + 2) << 8) + at(bytes, offset + 3);
|
|
4592
|
+
var u32le = (bytes, offset) => at(bytes, offset) + (at(bytes, offset + 1) << 8) + (at(bytes, offset + 2) << 16) + at(bytes, offset + 3) * 16777216;
|
|
4593
|
+
var startsWith = (bytes, prefix) => bytes.length >= prefix.length && prefix.every((byte, index) => bytes[index] === byte);
|
|
4594
|
+
function ascii(bytes, offset, text3) {
|
|
4595
|
+
if (bytes.length < offset + text3.length) return false;
|
|
4596
|
+
for (let index = 0; index < text3.length; index++) if (bytes[offset + index] !== text3.charCodeAt(index)) return false;
|
|
4597
|
+
return true;
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
// src/server/infra/pi/environment-tools.ts
|
|
4601
|
+
function environmentTools(lookup) {
|
|
4602
|
+
const pinned = new AsyncLocalStorage();
|
|
4603
|
+
const current = () => pinned.getStore() ?? lookup();
|
|
4604
|
+
const access = async (path) => {
|
|
4605
|
+
if (!await current().exists(path)) throw Object.assign(new Error(`ENOENT: no such file or directory, access '${path}'`), { code: "ENOENT" });
|
|
4606
|
+
};
|
|
4607
|
+
const readFile3 = (path) => current().readFile(path), writeFile4 = (path, content2) => current().writeFile(path, content2);
|
|
4608
|
+
const cwd = current().cwd;
|
|
4609
|
+
const definitions2 = [
|
|
4610
|
+
relocated(createBashToolDefinition(cwd, { operations: {
|
|
4611
|
+
exec: (command, dir, { onData, signal, timeout, env }) => current().exec(command, {
|
|
4612
|
+
cwd: dir,
|
|
4613
|
+
onData,
|
|
4614
|
+
...signal ? { signal } : {},
|
|
4615
|
+
...timeout ? { timeout } : {},
|
|
4616
|
+
...env ? { env: defined(env) } : {}
|
|
4617
|
+
})
|
|
4618
|
+
} }), current),
|
|
4619
|
+
// Pictures go to the model as pictures; the kind comes from a file's first bytes, as Pi's local read does.
|
|
4620
|
+
createReadToolDefinition(cwd, { operations: { readFile: readFile3, access, detectImageMimeType: async (path) => imageKind(await current().head(path, IMAGE_SNIFF_BYTES)) } }),
|
|
4621
|
+
createWriteToolDefinition(cwd, { operations: { writeFile: writeFile4, mkdir: (path) => current().mkdir(path) } }),
|
|
4622
|
+
createEditToolDefinition(cwd, { operations: { readFile: readFile3, writeFile: writeFile4, access } })
|
|
4623
|
+
];
|
|
4624
|
+
return definitions2.map((definition) => inside(definition, lookup, pinned));
|
|
4625
|
+
}
|
|
4626
|
+
function inside(definition, lookup, pinned) {
|
|
4627
|
+
const execute = definition.execute.bind(definition);
|
|
4628
|
+
return { ...definition, execute: ((...args) => {
|
|
4629
|
+
const env = lookup(), ctx = args[4];
|
|
4630
|
+
if (ctx && typeof ctx === "object") args[4] = new Proxy(ctx, { get: (target2, key) => key === "cwd" ? env.cwd : Reflect.get(target2, key, target2) });
|
|
4631
|
+
return pinned.run(env, () => execute(...args));
|
|
4632
|
+
}) };
|
|
4633
|
+
}
|
|
4634
|
+
function relocated(definition, current) {
|
|
4635
|
+
const execute = definition.execute.bind(definition);
|
|
4636
|
+
return { ...definition, execute: (async (...args) => {
|
|
4637
|
+
const env = current();
|
|
4638
|
+
if (env.kind === "host") return execute(...args);
|
|
4639
|
+
try {
|
|
4640
|
+
const result = await execute(...args), path = result.details?.fullOutputPath;
|
|
4641
|
+
if (typeof path !== "string") return result;
|
|
4642
|
+
const target2 = await moveOut(path, env), swap = (text3) => text3?.replaceAll(path, target2);
|
|
4643
|
+
return {
|
|
4644
|
+
...result,
|
|
4645
|
+
content: result.content.map((part) => part.type === "text" ? { ...part, text: swap(part.text) } : part),
|
|
4646
|
+
details: { ...result.details, fullOutputPath: target2 },
|
|
4647
|
+
...result.structuredContent?.full_output_path ? { structuredContent: { ...result.structuredContent, full_output_path: target2 } } : {}
|
|
4648
|
+
};
|
|
4649
|
+
} catch (error) {
|
|
4650
|
+
const path = error instanceof Error ? /Full output: (\S+?)\]/.exec(error.message)?.[1] : void 0;
|
|
4651
|
+
if (!path) throw error;
|
|
4652
|
+
throw new Error(error.message.replaceAll(path, await moveOut(path, env)));
|
|
4653
|
+
}
|
|
4654
|
+
}) };
|
|
4655
|
+
}
|
|
4656
|
+
async function moveOut(path, env) {
|
|
4657
|
+
const target2 = `${env.cwd}/temp/${basename(path)}`;
|
|
4658
|
+
await env.writeFile(target2, await readLocal(path));
|
|
4659
|
+
await rm(path, { force: true });
|
|
4660
|
+
return target2;
|
|
4661
|
+
}
|
|
4662
|
+
function defined(vars) {
|
|
4663
|
+
return Object.fromEntries(Object.entries(vars).filter((entry) => entry[1] !== void 0));
|
|
4664
|
+
}
|
|
4665
|
+
|
|
4361
4666
|
// src/server/infra/pi/session.ts
|
|
4362
4667
|
var HTTP_IDLE_TIMEOUT_MS = 9e5;
|
|
4363
4668
|
function compactionReserve(contextWindow) {
|
|
@@ -4398,10 +4703,10 @@ var PiSession = class _PiSession {
|
|
|
4398
4703
|
}
|
|
4399
4704
|
static async create(options) {
|
|
4400
4705
|
const { bot, store, reference, selection } = options;
|
|
4401
|
-
const agentDir = join3(
|
|
4706
|
+
const agentDir = join3(dirname3(bot.workspacePath), "agent", "pi"), sessionsDir = join3(agentDir, "sessions");
|
|
4402
4707
|
await mkdir2(sessionsDir, { recursive: true, mode: 448 });
|
|
4403
|
-
const expected =
|
|
4404
|
-
if (!
|
|
4708
|
+
const expected = resolve2(sessionsDir) + sep;
|
|
4709
|
+
if (!resolve2(reference.file).startsWith(expected) || dirname3(resolve2(reference.file)) !== resolve2(sessionsDir))
|
|
4405
4710
|
throw new RuntimeFault("session_path", "The session file path isn't valid.");
|
|
4406
4711
|
const exists = existsSync(reference.file);
|
|
4407
4712
|
if (!exists && (reference.persisted || store.runtime.runs(bot.id).some((run) => run.state === "unknown")))
|
|
@@ -4452,6 +4757,10 @@ var PiSession = class _PiSession {
|
|
|
4452
4757
|
executionMode: "sequential",
|
|
4453
4758
|
execute: async (toolCallId, args, signal) => {
|
|
4454
4759
|
const output = await tool.execute(args, { toolCallId, signal });
|
|
4760
|
+
if (isToolMedia(output)) return { content: [
|
|
4761
|
+
{ type: "text", text: output.text },
|
|
4762
|
+
...output.images.map((image) => ({ type: "image", data: image.data, mimeType: image.mimeType }))
|
|
4763
|
+
], details: {} };
|
|
4455
4764
|
return { content: [{ type: "text", text: typeof output === "string" ? output : JSON.stringify(output) }], details: {} };
|
|
4456
4765
|
}
|
|
4457
4766
|
});
|
|
@@ -4468,7 +4777,8 @@ var PiSession = class _PiSession {
|
|
|
4468
4777
|
modelRuntime: models,
|
|
4469
4778
|
model,
|
|
4470
4779
|
thinkingLevel: resolveThinking(model, selection.thinkingLevel),
|
|
4471
|
-
tools
|
|
4780
|
+
tools,
|
|
4781
|
+
customTools: environmentTools(options.environment)
|
|
4472
4782
|
});
|
|
4473
4783
|
adapter.session = result.session;
|
|
4474
4784
|
adapter.settings = settingsManager;
|
|
@@ -4485,8 +4795,8 @@ var PiSession = class _PiSession {
|
|
|
4485
4795
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
4486
4796
|
const entry = entries[index];
|
|
4487
4797
|
if (entry.type !== "message" || entry.message !== event.message) continue;
|
|
4488
|
-
const
|
|
4489
|
-
if (
|
|
4798
|
+
const record3 = history.convert(entry);
|
|
4799
|
+
if (record3) await options.onHistory(record3);
|
|
4490
4800
|
break;
|
|
4491
4801
|
}
|
|
4492
4802
|
});
|
|
@@ -4533,7 +4843,7 @@ var PiSession = class _PiSession {
|
|
|
4533
4843
|
}
|
|
4534
4844
|
/** The last assistant message decides the prompt (a later retry success replaces an earlier error).
|
|
4535
4845
|
* pi-ai providers and Agent.handleRunFailure report a failure as "aborted" when the run's signal was
|
|
4536
|
-
* cancelled, but Pi
|
|
4846
|
+
* cancelled, but Pi 1.2.0's lazyStream reports request-setup failures as "error" regardless — e.g. the
|
|
4537
4847
|
* request that follows a tool cancelled mid-run. Apply the same signal rule to those. */
|
|
4538
4848
|
classify(message) {
|
|
4539
4849
|
if (message.stopReason === "aborted" || message.stopReason === "error" && this.session.agent.signal?.aborted) return { outcome: "interrupted" };
|
|
@@ -4569,8 +4879,8 @@ var PiSession = class _PiSession {
|
|
|
4569
4879
|
this.wantsAbort = false;
|
|
4570
4880
|
this.abortFailure = void 0;
|
|
4571
4881
|
this.result = { outcome: "completed" };
|
|
4572
|
-
const settled = new Promise((
|
|
4573
|
-
this.settledResolve =
|
|
4882
|
+
const settled = new Promise((resolve5) => {
|
|
4883
|
+
this.settledResolve = resolve5;
|
|
4574
4884
|
});
|
|
4575
4885
|
this.options.live?.({ type: "start" });
|
|
4576
4886
|
let outcome = "failed";
|
|
@@ -4625,10 +4935,617 @@ var PiSession = class _PiSession {
|
|
|
4625
4935
|
}
|
|
4626
4936
|
};
|
|
4627
4937
|
|
|
4938
|
+
// src/server/infra/env/docker.ts
|
|
4939
|
+
import { execFile, spawn } from "node:child_process";
|
|
4940
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4941
|
+
import { createWriteStream } from "node:fs";
|
|
4942
|
+
import { mkdir as mkdir3, mkdtemp, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
4943
|
+
import { tmpdir } from "node:os";
|
|
4944
|
+
import { join as join4 } from "node:path";
|
|
4945
|
+
import { Connection, RemoteExecutionEnv, packagedDaemon } from "@earendil-works/pi-env";
|
|
4946
|
+
|
|
4947
|
+
// src/server/infra/env/computer-image.ts
|
|
4948
|
+
var COMPUTER_DOCKERFILE = `FROM mcr.microsoft.com/playwright:v1.61.1-noble
|
|
4949
|
+
ARG UID=1000
|
|
4950
|
+
ARG GID=1000
|
|
4951
|
+
RUN apt-get update \\
|
|
4952
|
+
&& apt-get install -y --no-install-recommends build-essential ca-certificates curl fonts-noto-cjk git python3 python3-dev python3-pip python3-venv sudo \\
|
|
4953
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
4954
|
+
&& npm install -g playwright@1.61.1 && npm cache clean --force \\
|
|
4955
|
+
&& taken="$(getent passwd "$UID" | cut -d: -f1)"; if [ -n "$taken" ]; then userdel -r "$taken" 2>/dev/null || userdel "$taken"; fi \\
|
|
4956
|
+
&& (getent group "$GID" > /dev/null || groupadd -g "$GID" bot) \\
|
|
4957
|
+
&& useradd -m -d /workspace -u "$UID" -g "$GID" -s /bin/bash bot \\
|
|
4958
|
+
&& install -d -o "$UID" -g "$GID" /home/bot/.npm-global /home/bot/.npm-global/bin /home/bot/.npm-global/lib \\
|
|
4959
|
+
&& echo "bot ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/bot && chmod 0440 /etc/sudoers.d/bot \\
|
|
4960
|
+
&& mkdir -p /workspace && chown "$UID:$GID" /workspace
|
|
4961
|
+
ENV NPM_CONFIG_PREFIX=/home/bot/.npm-global PATH=/home/bot/.npm-global/bin:$PATH \\
|
|
4962
|
+
NODE_PATH=/home/bot/.npm-global/lib/node_modules:/usr/lib/node_modules
|
|
4963
|
+
USER bot
|
|
4964
|
+
WORKDIR /workspace
|
|
4965
|
+
`;
|
|
4966
|
+
|
|
4967
|
+
// src/server/infra/env/durable.ts
|
|
4968
|
+
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
|
4969
|
+
function overDurable(id, kind, env, options) {
|
|
4970
|
+
let closed = false;
|
|
4971
|
+
return {
|
|
4972
|
+
id,
|
|
4973
|
+
kind,
|
|
4974
|
+
cwd: env.cwd,
|
|
4975
|
+
async exec(command, exec) {
|
|
4976
|
+
const result = await env.exec(command, {
|
|
4977
|
+
cwd: exec.cwd,
|
|
4978
|
+
...options.passEnv && exec.env ? { env: exec.env, inheritEnv: false } : { inheritEnv: true },
|
|
4979
|
+
...exec.timeout ? { timeout: exec.timeout } : {},
|
|
4980
|
+
onOutput: (text3) => exec.onData(Buffer.from(text3))
|
|
4981
|
+
}, context(exec.signal));
|
|
4982
|
+
if (result.ok) return { exitCode: result.value.exitCode };
|
|
4983
|
+
if (result.error.code === "aborted") throw new Error("aborted");
|
|
4984
|
+
if (result.error.code === "timeout") throw new Error(`timeout:${exec.timeout}`);
|
|
4985
|
+
throw fault(result.error);
|
|
4986
|
+
},
|
|
4987
|
+
async readFile(path) {
|
|
4988
|
+
return Buffer.from(unwrap(await env.readBinaryFile(path, BACKGROUND_CONTEXT)));
|
|
4989
|
+
},
|
|
4990
|
+
async head(path, bytes) {
|
|
4991
|
+
const reader = unwrap(await env.openBinaryReader(path, void 0, BACKGROUND_CONTEXT));
|
|
4992
|
+
try {
|
|
4993
|
+
return Buffer.from(unwrap(await reader.read(0, bytes, BACKGROUND_CONTEXT)));
|
|
4994
|
+
} finally {
|
|
4995
|
+
await reader.close(BACKGROUND_CONTEXT);
|
|
4996
|
+
}
|
|
4997
|
+
},
|
|
4998
|
+
async writeFile(path, content2) {
|
|
4999
|
+
unwrap(await env.writeFile(path, content2, BACKGROUND_CONTEXT));
|
|
5000
|
+
},
|
|
5001
|
+
async mkdir(path) {
|
|
5002
|
+
unwrap(await env.createDir(path, { recursive: true }, BACKGROUND_CONTEXT));
|
|
5003
|
+
},
|
|
5004
|
+
async exists(path) {
|
|
5005
|
+
return unwrap(await env.exists(path, BACKGROUND_CONTEXT));
|
|
5006
|
+
},
|
|
5007
|
+
async fileSize(path) {
|
|
5008
|
+
const real = await env.canonicalPath(path, BACKGROUND_CONTEXT);
|
|
5009
|
+
if (!real.ok) {
|
|
5010
|
+
if (real.error.code === "not_found") return null;
|
|
5011
|
+
throw fault(real.error);
|
|
5012
|
+
}
|
|
5013
|
+
const info = unwrap(await env.fileInfo(real.value, BACKGROUND_CONTEXT));
|
|
5014
|
+
return info.kind === "file" ? info.size : null;
|
|
5015
|
+
},
|
|
5016
|
+
async close() {
|
|
5017
|
+
if (closed) return;
|
|
5018
|
+
closed = true;
|
|
5019
|
+
await options.close?.();
|
|
5020
|
+
}
|
|
5021
|
+
};
|
|
5022
|
+
}
|
|
5023
|
+
var context = (signal) => signal ? withAbortSignal(signal, BACKGROUND_CONTEXT) : BACKGROUND_CONTEXT;
|
|
5024
|
+
function unwrap(result) {
|
|
5025
|
+
if (result.ok) return result.value;
|
|
5026
|
+
throw fault(result.error);
|
|
5027
|
+
}
|
|
5028
|
+
function fault(error) {
|
|
5029
|
+
return Object.assign(new Error(error.message), { code: error.code });
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
// src/server/infra/env/docker.ts
|
|
5033
|
+
var COMPUTER_CWD = "/workspace";
|
|
5034
|
+
var DAEMON_DIR = "/opt/jarvy";
|
|
5035
|
+
function docker(args, timeoutMs = 6e4) {
|
|
5036
|
+
return new Promise((resolve5, reject) => execFile("docker", args, { timeout: timeoutMs, maxBuffer: 8 << 20 }, (error, stdout, stderr) => {
|
|
5037
|
+
if (error) reject(Object.assign(new Error((stderr || error.message).trim().split("\n").at(-1) ?? "docker failed"), { code: "docker_failed" }));
|
|
5038
|
+
else resolve5(stdout.trim());
|
|
5039
|
+
}));
|
|
5040
|
+
}
|
|
5041
|
+
async function dockerAvailable() {
|
|
5042
|
+
try {
|
|
5043
|
+
await docker(["version", "--format", "{{.Server.Version}}"], 1e4);
|
|
5044
|
+
return true;
|
|
5045
|
+
} catch {
|
|
5046
|
+
return false;
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
5049
|
+
function computerImage(uid, gid) {
|
|
5050
|
+
return `jarvy-computer:${createHash3("sha256").update(`${COMPUTER_DOCKERFILE}
|
|
5051
|
+
${uid}:${gid}`).digest("hex").slice(0, 12)}`;
|
|
5052
|
+
}
|
|
5053
|
+
async function pruneImages(keep) {
|
|
5054
|
+
const tags = (await docker(["images", "--filter", "label=jarvy.computer=1", "--format", "{{.Repository}}:{{.Tag}}"])).split("\n").filter(Boolean);
|
|
5055
|
+
const removed = [];
|
|
5056
|
+
for (const tag of tags) if (tag !== keep) {
|
|
5057
|
+
try {
|
|
5058
|
+
await docker(["image", "rm", tag], 12e4);
|
|
5059
|
+
removed.push(tag);
|
|
5060
|
+
} catch {
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
return removed;
|
|
5064
|
+
}
|
|
5065
|
+
async function imageExists(tag) {
|
|
5066
|
+
try {
|
|
5067
|
+
await docker(["image", "inspect", "--format", "{{.Id}}", tag]);
|
|
5068
|
+
return true;
|
|
5069
|
+
} catch {
|
|
5070
|
+
return false;
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
async function buildImage(tag, uid, gid, logFile) {
|
|
5074
|
+
const dir = await mkdtemp(join4(tmpdir(), "jarvy-computer-"));
|
|
5075
|
+
try {
|
|
5076
|
+
await writeFile2(join4(dir, "Dockerfile"), COMPUTER_DOCKERFILE);
|
|
5077
|
+
const log = createWriteStream(logFile, { flags: "w", mode: 384 });
|
|
5078
|
+
const code = await new Promise((resolve5, reject) => {
|
|
5079
|
+
const child = spawn(
|
|
5080
|
+
"docker",
|
|
5081
|
+
["build", "-t", tag, "--label", "jarvy.computer=1", "--build-arg", `UID=${uid}`, "--build-arg", `GID=${gid}`, dir],
|
|
5082
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
5083
|
+
);
|
|
5084
|
+
child.stdout.pipe(log, { end: false });
|
|
5085
|
+
child.stderr.pipe(log, { end: false });
|
|
5086
|
+
child.on("error", reject);
|
|
5087
|
+
child.on("close", resolve5);
|
|
5088
|
+
}).finally(() => log.end());
|
|
5089
|
+
if (code !== 0) throw Object.assign(new Error(`docker build exited with ${code}`), { code: "docker_failed" });
|
|
5090
|
+
} finally {
|
|
5091
|
+
await rm2(dir, { recursive: true, force: true });
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
async function containerStates(label) {
|
|
5095
|
+
const out = await docker(["ps", "-a", "--filter", `label=${label}`, "--format", "{{.Names}} {{.State}}"]);
|
|
5096
|
+
const states = /* @__PURE__ */ new Map();
|
|
5097
|
+
for (const line of out.split("\n").filter(Boolean)) {
|
|
5098
|
+
const [name2, state] = line.split(" ");
|
|
5099
|
+
if (name2) states.set(name2, state === "running" ? "running" : "stopped");
|
|
5100
|
+
}
|
|
5101
|
+
return states;
|
|
5102
|
+
}
|
|
5103
|
+
async function containerState(name2) {
|
|
5104
|
+
try {
|
|
5105
|
+
return await docker(["inspect", "--format", "{{.State.Running}}", name2]) === "true" ? "running" : "stopped";
|
|
5106
|
+
} catch (error) {
|
|
5107
|
+
if (/no such (object|container)/i.test(error.message)) return "missing";
|
|
5108
|
+
throw error;
|
|
5109
|
+
}
|
|
5110
|
+
}
|
|
5111
|
+
async function runContainer(spec) {
|
|
5112
|
+
await docker([
|
|
5113
|
+
"run",
|
|
5114
|
+
"-d",
|
|
5115
|
+
"--name",
|
|
5116
|
+
spec.name,
|
|
5117
|
+
"--init",
|
|
5118
|
+
"--user",
|
|
5119
|
+
`${spec.uid}:${spec.gid}`,
|
|
5120
|
+
"--shm-size",
|
|
5121
|
+
"1g",
|
|
5122
|
+
...Object.entries(spec.labels).flatMap(([key, value2]) => ["--label", `${key}=${value2}`]),
|
|
5123
|
+
"-v",
|
|
5124
|
+
`${spec.workspace}:${COMPUTER_CWD}`,
|
|
5125
|
+
"-v",
|
|
5126
|
+
`${spec.daemonDir}:${DAEMON_DIR}:ro`,
|
|
5127
|
+
"-w",
|
|
5128
|
+
COMPUTER_CWD,
|
|
5129
|
+
spec.image,
|
|
5130
|
+
"sleep",
|
|
5131
|
+
"infinity"
|
|
5132
|
+
], 12e4);
|
|
5133
|
+
}
|
|
5134
|
+
async function startContainer(name2) {
|
|
5135
|
+
await docker(["start", name2], 6e4);
|
|
5136
|
+
}
|
|
5137
|
+
async function homeAtWorkspace(name2) {
|
|
5138
|
+
const home = await docker(["exec", name2, "sh", "-c", "getent passwd bot | cut -d: -f6"], 3e4);
|
|
5139
|
+
if (home !== COMPUTER_CWD) await docker(["exec", "-u", "root", name2, "usermod", "-d", COMPUTER_CWD, "bot"], 3e4);
|
|
5140
|
+
}
|
|
5141
|
+
async function stopContainer(name2) {
|
|
5142
|
+
await docker(["stop", "--time", "5", name2], 6e4);
|
|
5143
|
+
}
|
|
5144
|
+
async function removeContainer(name2) {
|
|
5145
|
+
try {
|
|
5146
|
+
await docker(["rm", "--force", name2], 6e4);
|
|
5147
|
+
} catch (error) {
|
|
5148
|
+
if (!/no such (object|container)/i.test(error.message)) throw error;
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
async function syncDaemon(dir) {
|
|
5152
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
5153
|
+
const wanted = await readFile2(packagedDaemon({ platform: "linux", arch }));
|
|
5154
|
+
const target2 = join4(dir, "pi-env");
|
|
5155
|
+
const current = await readFile2(target2).catch(() => null);
|
|
5156
|
+
if (current?.equals(wanted)) return;
|
|
5157
|
+
await mkdir3(dir, { recursive: true, mode: 448 });
|
|
5158
|
+
const temp = `${target2}.${process.pid}.tmp`;
|
|
5159
|
+
await writeFile2(temp, wanted, { mode: 493 });
|
|
5160
|
+
await rename2(temp, target2);
|
|
5161
|
+
}
|
|
5162
|
+
function containerEnvironment(id, container, log) {
|
|
5163
|
+
const connection = new Connection({ command: ["docker", "exec", "-i", container, `${DAEMON_DIR}/pi-env`], onLog: (text3) => log(text3.trim()) });
|
|
5164
|
+
return overDurable(id, "computer", new RemoteExecutionEnv({ connection, id, cwd: COMPUTER_CWD }), { passEnv: false, close: () => connection.close() });
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5167
|
+
// src/server/infra/env/host.ts
|
|
5168
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
5169
|
+
function hostEnvironment(cwd) {
|
|
5170
|
+
return overDurable("host", "host", new NodeExecutionEnv({ cwd }), { passEnv: true });
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
// src/server/service/bot/environments.ts
|
|
5174
|
+
var ABOUT = {
|
|
5175
|
+
computer: "A Linux container only you use, with Node, Python, git, build tools, and Playwright with its browsers. You can install software there with sudo; it stays until the owner resets your sandbox, and /workspace, your home there, is kept when it is reset.",
|
|
5176
|
+
host: "The machine running Jarvy. Your tools run there as your owner and can reach their files: work in your directory, and leave other members' workspaces, live data directories (such as ~/.jarvy), main repository checkouts and system directories alone unless the task needs it; say in your reply what you changed there."
|
|
5177
|
+
};
|
|
5178
|
+
var BotEnvironments = class {
|
|
5179
|
+
constructor(store, computers) {
|
|
5180
|
+
this.store = store;
|
|
5181
|
+
this.computers = computers;
|
|
5182
|
+
}
|
|
5183
|
+
open = /* @__PURE__ */ new Map();
|
|
5184
|
+
current(botId, workspacePath) {
|
|
5185
|
+
const target2 = this.store.environments.placement(botId).current;
|
|
5186
|
+
const open = this.open.get(botId);
|
|
5187
|
+
if (open?.id === target2.id) return open.view;
|
|
5188
|
+
if (open) this.retire(open);
|
|
5189
|
+
const opened = this.track(target2.id, target2.kind === "computer" ? this.computers.environment(target2.botId ?? botId, target2) : hostEnvironment(workspacePath));
|
|
5190
|
+
this.open.set(botId, opened);
|
|
5191
|
+
return opened.view;
|
|
5192
|
+
}
|
|
5193
|
+
placement(botId) {
|
|
5194
|
+
const environments = this.store.environments, { home, current } = environments.placement(botId);
|
|
5195
|
+
const hostId = environments.host().id;
|
|
5196
|
+
return {
|
|
5197
|
+
home: home.kind,
|
|
5198
|
+
current: current.kind,
|
|
5199
|
+
computer: this.computers.status(botId),
|
|
5200
|
+
docker: this.computers.available,
|
|
5201
|
+
hostGranted: environments.grants(botId).some((grant) => grant.id === hostId)
|
|
5202
|
+
};
|
|
5203
|
+
}
|
|
5204
|
+
/** The environments the Bot may be in: its sandbox, its default, and the ones its owner granted it. */
|
|
5205
|
+
usable(botId) {
|
|
5206
|
+
const environments = this.store.environments;
|
|
5207
|
+
const all = [environments.computerOf(botId), environments.placement(botId).home, ...environments.grants(botId)];
|
|
5208
|
+
return [...new Map(all.filter((record3) => !!record3).map((record3) => [record3.id, record3])).values()];
|
|
5209
|
+
}
|
|
5210
|
+
place(botId, record3, workspacePath) {
|
|
5211
|
+
if (record3.kind === "host") return { id: record3.id, kind: "host", name: "your owner's computer", directory: workspacePath };
|
|
5212
|
+
return { id: record3.id, kind: "sandbox", name: record3.botId === botId ? "your sandbox" : "another Bot's sandbox", directory: COMPUTER_CWD };
|
|
5213
|
+
}
|
|
5214
|
+
/** A Bot's environments as other Bots see them in bots.list. */
|
|
5215
|
+
summary(botId) {
|
|
5216
|
+
const name2 = (record3) => record3.kind === "host" ? "owner's computer" : record3.botId === botId ? "own sandbox" : "another Bot's sandbox";
|
|
5217
|
+
const { home, current } = this.store.environments.placement(botId);
|
|
5218
|
+
return { default: name2(home), current: name2(current), usable: this.usable(botId).map(name2) };
|
|
5219
|
+
}
|
|
5220
|
+
/** The Bot's default environment, which its instructions describe. */
|
|
5221
|
+
home(botId, workspacePath) {
|
|
5222
|
+
return this.place(botId, this.store.environments.placement(botId).home, workspacePath);
|
|
5223
|
+
}
|
|
5224
|
+
/** Where the Bot works now, for the end of each batch. */
|
|
5225
|
+
here(botId, workspacePath) {
|
|
5226
|
+
return this.place(botId, this.store.environments.placement(botId).current, workspacePath);
|
|
5227
|
+
}
|
|
5228
|
+
/** environments.list: where the Bot is and where else it may go. */
|
|
5229
|
+
list(botId, workspacePath) {
|
|
5230
|
+
const { home, current } = this.store.environments.placement(botId);
|
|
5231
|
+
return { current: current.id, environments: this.usable(botId).map((record3) => ({
|
|
5232
|
+
...this.place(botId, record3, workspacePath),
|
|
5233
|
+
...record3.id === home.id ? { default: true } : {},
|
|
5234
|
+
...record3.kind === "computer" ? { state: this.computers.status(record3.botId ?? botId)?.state ?? "stopped" } : {}
|
|
5235
|
+
})) };
|
|
5236
|
+
}
|
|
5237
|
+
/** environments.switch: the Bot moves itself, like cd, until it switches back. A sandbox is started first; if it
|
|
5238
|
+
* can't start, the Bot stays where it was. */
|
|
5239
|
+
async switch(botId, environmentId, workspacePath) {
|
|
5240
|
+
const target2 = this.usable(botId).find((record3) => record3.id === environmentId);
|
|
5241
|
+
if (!target2) throw new ChatError(404, "No work environment with this id that you may use; see environments.list");
|
|
5242
|
+
const already = this.store.environments.placement(botId).current.id === target2.id;
|
|
5243
|
+
if (target2.kind === "computer") await this.computers.ensure(target2.botId ?? botId);
|
|
5244
|
+
this.store.environments.setCurrent(botId, target2.id);
|
|
5245
|
+
return { ...this.place(botId, target2, workspacePath), about: ABOUT[target2.kind], ...already ? { note: "You were already here." } : {} };
|
|
5246
|
+
}
|
|
5247
|
+
/** The owner makes the Bot's sandbox (given it on first use) or their computer its default, and moves it there. */
|
|
5248
|
+
setHome(botId, kind, workspacePath) {
|
|
5249
|
+
const environments = this.store.environments, from = this.here(botId, workspacePath);
|
|
5250
|
+
if (kind === "host") environments.setHome(botId, environments.host().id);
|
|
5251
|
+
else {
|
|
5252
|
+
const computer = environments.computerOf(botId);
|
|
5253
|
+
if (computer) {
|
|
5254
|
+
environments.setHome(botId, computer.id);
|
|
5255
|
+
void this.computers.ensure(botId).catch(() => {
|
|
5256
|
+
});
|
|
5257
|
+
} else this.computers.create(botId);
|
|
5258
|
+
}
|
|
5259
|
+
const to = this.here(botId, workspacePath);
|
|
5260
|
+
return from.id === to.id ? null : { from, to };
|
|
5261
|
+
}
|
|
5262
|
+
/** The owner grants or takes back their computer (A50); taking it back while the Bot works there sends it home. */
|
|
5263
|
+
grantHost(botId, granted, by, workspacePath) {
|
|
5264
|
+
const environments = this.store.environments, host = environments.host();
|
|
5265
|
+
if (granted) return { changed: environments.grant(host.id, botId, by), moved: null };
|
|
5266
|
+
const changed = environments.revoke(host.id, botId), { home, current } = environments.placement(botId);
|
|
5267
|
+
if (current.id !== host.id || home.id === host.id) return { changed, moved: null };
|
|
5268
|
+
const from = this.here(botId, workspacePath);
|
|
5269
|
+
environments.setCurrent(botId, home.id);
|
|
5270
|
+
return { changed, moved: { from, to: this.here(botId, workspacePath) } };
|
|
5271
|
+
}
|
|
5272
|
+
async release(botId) {
|
|
5273
|
+
const open = this.open.get(botId);
|
|
5274
|
+
this.open.delete(botId);
|
|
5275
|
+
await open?.env.close();
|
|
5276
|
+
await this.computers.remove(botId);
|
|
5277
|
+
}
|
|
5278
|
+
async close() {
|
|
5279
|
+
const all = [...this.open.values()];
|
|
5280
|
+
this.open.clear();
|
|
5281
|
+
await Promise.all(all.map((open) => open.env.close()));
|
|
5282
|
+
await this.computers.close();
|
|
5283
|
+
}
|
|
5284
|
+
retire(open) {
|
|
5285
|
+
open.retired = true;
|
|
5286
|
+
if (!open.busy) void open.env.close();
|
|
5287
|
+
}
|
|
5288
|
+
/** Counts the operations running on an environment, so one the Bot has left closes only when they are done. */
|
|
5289
|
+
track(id, env) {
|
|
5290
|
+
const opened = { id, env, view: env, busy: 0, retired: false };
|
|
5291
|
+
const run = async (work) => {
|
|
5292
|
+
opened.busy++;
|
|
5293
|
+
try {
|
|
5294
|
+
return await work();
|
|
5295
|
+
} finally {
|
|
5296
|
+
if (--opened.busy === 0 && opened.retired) void env.close();
|
|
5297
|
+
}
|
|
5298
|
+
};
|
|
5299
|
+
opened.view = {
|
|
5300
|
+
id: env.id,
|
|
5301
|
+
kind: env.kind,
|
|
5302
|
+
cwd: env.cwd,
|
|
5303
|
+
exec: (command, options) => run(() => env.exec(command, options)),
|
|
5304
|
+
readFile: (path) => run(() => env.readFile(path)),
|
|
5305
|
+
head: (path, bytes) => run(() => env.head(path, bytes)),
|
|
5306
|
+
writeFile: (path, content2) => run(() => env.writeFile(path, content2)),
|
|
5307
|
+
mkdir: (path) => run(() => env.mkdir(path)),
|
|
5308
|
+
exists: (path) => run(() => env.exists(path)),
|
|
5309
|
+
fileSize: (path) => run(() => env.fileSize(path)),
|
|
5310
|
+
close: () => env.close()
|
|
5311
|
+
};
|
|
5312
|
+
return opened;
|
|
5313
|
+
}
|
|
5314
|
+
};
|
|
5315
|
+
|
|
5316
|
+
// src/server/service/bot/computers.ts
|
|
5317
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
5318
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
5319
|
+
var SWEEP_MS = 6e4;
|
|
5320
|
+
var Computers = class {
|
|
5321
|
+
constructor(deps) {
|
|
5322
|
+
this.deps = deps;
|
|
5323
|
+
this.dataDir = dirname4(deps.store.path);
|
|
5324
|
+
this.tag = createHash4("sha256").update(this.dataDir).digest("hex").slice(0, 12);
|
|
5325
|
+
}
|
|
5326
|
+
docker = null;
|
|
5327
|
+
probing = null;
|
|
5328
|
+
image = null;
|
|
5329
|
+
timer = null;
|
|
5330
|
+
states = /* @__PURE__ */ new Map();
|
|
5331
|
+
preparing = /* @__PURE__ */ new Map();
|
|
5332
|
+
connections = /* @__PURE__ */ new Map();
|
|
5333
|
+
/** Containers whose home was checked since Jarvy started (`homeAtWorkspace`). */
|
|
5334
|
+
homes = /* @__PURE__ */ new Map();
|
|
5335
|
+
dataDir;
|
|
5336
|
+
/** Tells this Jarvy's containers from those of other data directories on the same Docker. */
|
|
5337
|
+
tag;
|
|
5338
|
+
uid = process.getuid?.() ?? 1e3;
|
|
5339
|
+
gid = process.getgid?.() ?? 1e3;
|
|
5340
|
+
/** Looks for Docker and learns the state of every computer, once; every computer waits for it. */
|
|
5341
|
+
start() {
|
|
5342
|
+
this.probing ??= this.probe();
|
|
5343
|
+
return this.probing;
|
|
5344
|
+
}
|
|
5345
|
+
async probe() {
|
|
5346
|
+
this.docker = await dockerAvailable();
|
|
5347
|
+
if (!this.docker) return;
|
|
5348
|
+
try {
|
|
5349
|
+
await syncDaemon(join5(this.dataDir, "computers", "bin"));
|
|
5350
|
+
} catch (error) {
|
|
5351
|
+
this.docker = false;
|
|
5352
|
+
this.log(`computers disabled: ${error.message}`);
|
|
5353
|
+
return;
|
|
5354
|
+
}
|
|
5355
|
+
await this.sweep().catch((error) => this.log(`computer sweep failed: ${error.message}`));
|
|
5356
|
+
this.timer = setInterval(() => void this.sweep().catch((error) => this.log(`computer sweep failed: ${error.message}`)), SWEEP_MS);
|
|
5357
|
+
this.timer.unref();
|
|
5358
|
+
}
|
|
5359
|
+
/** Whether new computers can be made here; false until `start` found Docker. */
|
|
5360
|
+
get available() {
|
|
5361
|
+
return this.docker === true;
|
|
5362
|
+
}
|
|
5363
|
+
status(botId) {
|
|
5364
|
+
if (!this.deps.store.environments.computerOf(botId)) return null;
|
|
5365
|
+
if (!this.available) return { state: "unavailable" };
|
|
5366
|
+
return this.states.get(botId) ?? { state: "stopped" };
|
|
5367
|
+
}
|
|
5368
|
+
/** Gives a Bot its computer and makes it the Bot's default environment; the container is made in the background. */
|
|
5369
|
+
create(botId) {
|
|
5370
|
+
if (!this.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 409);
|
|
5371
|
+
const computer = this.deps.store.environments.addComputer(botId, `jarvy-${this.tag.slice(0, 6)}-${botId.slice(0, 8)}`);
|
|
5372
|
+
this.deps.store.environments.setHome(botId, computer.id);
|
|
5373
|
+
void this.ensure(botId).catch(() => {
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
/** What the Bot's tools use while it works on its computer: every call first makes sure the container runs. */
|
|
5377
|
+
environment(botId, record3) {
|
|
5378
|
+
const run = async (op) => {
|
|
5379
|
+
try {
|
|
5380
|
+
return await op(await this.connect(botId, record3));
|
|
5381
|
+
} catch (error) {
|
|
5382
|
+
if (error instanceof RuntimeFault || await containerState(record3.spec.container).catch(() => "missing") === "running") throw error;
|
|
5383
|
+
await this.disconnect(botId);
|
|
5384
|
+
this.set(botId, { state: "stopped" });
|
|
5385
|
+
return op(await this.connect(botId, record3));
|
|
5386
|
+
}
|
|
5387
|
+
};
|
|
5388
|
+
return {
|
|
5389
|
+
id: record3.id,
|
|
5390
|
+
kind: "computer",
|
|
5391
|
+
cwd: COMPUTER_CWD,
|
|
5392
|
+
exec: (command, options) => run((env) => env.exec(command, options)),
|
|
5393
|
+
readFile: (path) => run((env) => env.readFile(path)),
|
|
5394
|
+
head: (path, bytes) => run((env) => env.head(path, bytes)),
|
|
5395
|
+
writeFile: (path, content2) => run((env) => env.writeFile(path, content2)),
|
|
5396
|
+
mkdir: (path) => run((env) => env.mkdir(path)),
|
|
5397
|
+
exists: (path) => run((env) => env.exists(path)),
|
|
5398
|
+
fileSize: (path) => run((env) => env.fileSize(path)),
|
|
5399
|
+
close: () => this.disconnect(botId)
|
|
5400
|
+
};
|
|
5401
|
+
}
|
|
5402
|
+
async connect(botId, record3) {
|
|
5403
|
+
const container = await this.ensure(botId);
|
|
5404
|
+
let home = this.homes.get(container);
|
|
5405
|
+
if (!home) {
|
|
5406
|
+
home = homeAtWorkspace(container).catch((error) => this.log(`home of ${botId} not moved: ${error.message}`));
|
|
5407
|
+
this.homes.set(container, home);
|
|
5408
|
+
}
|
|
5409
|
+
await home;
|
|
5410
|
+
let env = this.connections.get(botId);
|
|
5411
|
+
if (!env) {
|
|
5412
|
+
env = containerEnvironment(record3.id, container, (text3) => this.log(`[${botId.slice(0, 8)}] ${text3}`));
|
|
5413
|
+
this.connections.set(botId, env);
|
|
5414
|
+
}
|
|
5415
|
+
return env;
|
|
5416
|
+
}
|
|
5417
|
+
/** Makes sure the container exists and runs, building the image first when needed; returns its name. */
|
|
5418
|
+
ensure(botId) {
|
|
5419
|
+
const pending = this.preparing.get(botId);
|
|
5420
|
+
if (pending) return pending;
|
|
5421
|
+
const computer = this.deps.store.environments.computerOf(botId);
|
|
5422
|
+
if (!computer) return Promise.reject(new RuntimeFault("computer_missing", "This Bot has no sandbox.", 404));
|
|
5423
|
+
const container = computer.spec.container;
|
|
5424
|
+
if (this.states.get(botId)?.state === "running") return Promise.resolve(container);
|
|
5425
|
+
if (this.docker !== false) this.set(botId, { state: "preparing" });
|
|
5426
|
+
const task = (async () => {
|
|
5427
|
+
try {
|
|
5428
|
+
await this.start();
|
|
5429
|
+
if (!this.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 503);
|
|
5430
|
+
const image = await this.imageReady();
|
|
5431
|
+
const state = await containerState(container);
|
|
5432
|
+
if (state === "missing") await runContainer({
|
|
5433
|
+
name: container,
|
|
5434
|
+
image,
|
|
5435
|
+
workspace: join5(this.dataDir, "bots", botId, "workspace"),
|
|
5436
|
+
daemonDir: join5(this.dataDir, "computers", "bin"),
|
|
5437
|
+
uid: this.uid,
|
|
5438
|
+
gid: this.gid,
|
|
5439
|
+
labels: { "jarvy.data": this.tag, "jarvy.bot": botId }
|
|
5440
|
+
});
|
|
5441
|
+
else if (state === "stopped") await startContainer(container);
|
|
5442
|
+
this.set(botId, { state: "running" });
|
|
5443
|
+
return container;
|
|
5444
|
+
} catch (error) {
|
|
5445
|
+
this.set(botId, { state: "failed", error: error instanceof RuntimeFault ? error.code : "computer_failed" });
|
|
5446
|
+
this.log(`computer of ${botId} failed: ${error.message}`);
|
|
5447
|
+
if (error instanceof RuntimeFault) throw error;
|
|
5448
|
+
throw new RuntimeFault("computer_failed", `Your sandbox could not start: ${error.message}`, 503);
|
|
5449
|
+
}
|
|
5450
|
+
})().finally(() => this.preparing.delete(botId));
|
|
5451
|
+
this.preparing.set(botId, task);
|
|
5452
|
+
return task;
|
|
5453
|
+
}
|
|
5454
|
+
async stop(botId) {
|
|
5455
|
+
const computer = this.need(botId);
|
|
5456
|
+
await this.disconnect(botId);
|
|
5457
|
+
await stopContainer(computer.spec.container);
|
|
5458
|
+
this.set(botId, { state: "stopped" });
|
|
5459
|
+
}
|
|
5460
|
+
/** Removes the container, so the next start makes a fresh one from the image: what was installed goes, the
|
|
5461
|
+
* workspace stays. */
|
|
5462
|
+
async reset(botId) {
|
|
5463
|
+
const computer = this.need(botId);
|
|
5464
|
+
await this.disconnect(botId);
|
|
5465
|
+
await removeContainer(computer.spec.container);
|
|
5466
|
+
this.homes.delete(computer.spec.container);
|
|
5467
|
+
this.set(botId, { state: "stopped" });
|
|
5468
|
+
}
|
|
5469
|
+
/** The Bot is being deleted: its container goes with it. */
|
|
5470
|
+
async remove(botId) {
|
|
5471
|
+
const computer = this.deps.store.environments.computerOf(botId);
|
|
5472
|
+
if (!computer) return;
|
|
5473
|
+
await this.disconnect(botId);
|
|
5474
|
+
if (this.available) await removeContainer(computer.spec.container);
|
|
5475
|
+
this.deps.store.environments.dropComputer(botId);
|
|
5476
|
+
this.states.delete(botId);
|
|
5477
|
+
}
|
|
5478
|
+
/** Closes the connections; containers keep running. */
|
|
5479
|
+
async close() {
|
|
5480
|
+
if (this.timer) clearInterval(this.timer);
|
|
5481
|
+
this.timer = null;
|
|
5482
|
+
await Promise.all([...this.connections.keys()].map((botId) => this.disconnect(botId)));
|
|
5483
|
+
}
|
|
5484
|
+
need(botId) {
|
|
5485
|
+
const computer = this.deps.store.environments.computerOf(botId);
|
|
5486
|
+
if (!computer) throw new RuntimeFault("computer_missing", "This Bot has no sandbox.", 404);
|
|
5487
|
+
if (!this.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 409);
|
|
5488
|
+
return computer;
|
|
5489
|
+
}
|
|
5490
|
+
imageReady() {
|
|
5491
|
+
this.image ??= (async () => {
|
|
5492
|
+
const tag = computerImage(this.uid, this.gid);
|
|
5493
|
+
if (!await imageExists(tag)) {
|
|
5494
|
+
this.log(`building ${tag}`);
|
|
5495
|
+
await buildImage(tag, this.uid, this.gid, join5(this.dataDir, "computers", "build.log"));
|
|
5496
|
+
void pruneImages(tag).then((removed) => {
|
|
5497
|
+
if (removed.length) this.log(`removed ${removed.join(", ")}`);
|
|
5498
|
+
}).catch(() => {
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
5501
|
+
return tag;
|
|
5502
|
+
})().catch((error) => {
|
|
5503
|
+
this.image = null;
|
|
5504
|
+
throw error;
|
|
5505
|
+
});
|
|
5506
|
+
return this.image;
|
|
5507
|
+
}
|
|
5508
|
+
async disconnect(botId) {
|
|
5509
|
+
const env = this.connections.get(botId);
|
|
5510
|
+
this.connections.delete(botId);
|
|
5511
|
+
await env?.close();
|
|
5512
|
+
}
|
|
5513
|
+
set(botId, status) {
|
|
5514
|
+
const before = this.states.get(botId);
|
|
5515
|
+
this.states.set(botId, status);
|
|
5516
|
+
if (before?.state !== status.state || before?.error !== status.error) this.deps.changed();
|
|
5517
|
+
}
|
|
5518
|
+
/** Learns what happened to containers outside Jarvy, and removes containers of this Jarvy that no Bot has any more
|
|
5519
|
+
* (a Bot deleted while Docker was out of reach). */
|
|
5520
|
+
async sweep() {
|
|
5521
|
+
const states = await containerStates(`jarvy.data=${this.tag}`);
|
|
5522
|
+
const computers = this.deps.store.environments.computers();
|
|
5523
|
+
const owned = new Set(computers.map((computer) => computer.spec.container));
|
|
5524
|
+
for (const name2 of states.keys()) if (!owned.has(name2)) {
|
|
5525
|
+
this.log(`removing ${name2}, which no Bot has`);
|
|
5526
|
+
await removeContainer(name2).catch((error) => this.log(`removing ${name2} failed: ${error.message}`));
|
|
5527
|
+
}
|
|
5528
|
+
for (const computer of computers) {
|
|
5529
|
+
const botId = computer.botId;
|
|
5530
|
+
if (this.preparing.has(botId)) continue;
|
|
5531
|
+
const running = states.get(computer.spec.container) === "running";
|
|
5532
|
+
if (!running) {
|
|
5533
|
+
if (this.states.get(botId)?.state !== "failed") this.set(botId, { state: "stopped" });
|
|
5534
|
+
await this.disconnect(botId);
|
|
5535
|
+
continue;
|
|
5536
|
+
}
|
|
5537
|
+
this.set(botId, { state: "running" });
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
log(text3) {
|
|
5541
|
+
(this.deps.log ?? console.log)(`[computers] ${text3}`);
|
|
5542
|
+
}
|
|
5543
|
+
};
|
|
5544
|
+
|
|
4628
5545
|
// src/server/infra/storage/history-repository.ts
|
|
4629
5546
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
4630
5547
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
4631
|
-
import { dirname as
|
|
5548
|
+
import { dirname as dirname5 } from "node:path";
|
|
4632
5549
|
var json = (value2) => value2 == null ? null : JSON.stringify(value2);
|
|
4633
5550
|
var CALL = "EXISTS (SELECT 1 FROM json_each(history.message, '$.content') b WHERE json_extract(b.value, '$.type')=? AND json_extract(b.value, '$.tool_call_id')=?)";
|
|
4634
5551
|
var BLOCKS = "FROM json_each(history.message,'$.content') b WHERE json_extract(b.value,'$.type')";
|
|
@@ -4640,7 +5557,7 @@ var SUMMARY = `SELECT id,timestamp,role,model,
|
|
|
4640
5557
|
var HistoryRepository = class {
|
|
4641
5558
|
db;
|
|
4642
5559
|
constructor(path) {
|
|
4643
|
-
mkdirSync2(
|
|
5560
|
+
mkdirSync2(dirname5(path), { recursive: true, mode: 448 });
|
|
4644
5561
|
this.db = new DatabaseSync2(path);
|
|
4645
5562
|
chmodSync2(path, 384);
|
|
4646
5563
|
const version = this.db.prepare("PRAGMA user_version").get().user_version;
|
|
@@ -4696,17 +5613,17 @@ var HistoryRepository = class {
|
|
|
4696
5613
|
const row = this.db.prepare("SELECT * FROM history WHERE id=?").get(id);
|
|
4697
5614
|
return row ? this.decode(row) : null;
|
|
4698
5615
|
}
|
|
4699
|
-
links(
|
|
5616
|
+
links(record3) {
|
|
4700
5617
|
const links = [];
|
|
4701
|
-
for (const block of
|
|
5618
|
+
for (const block of record3.message.content) {
|
|
4702
5619
|
if (block.type !== "tool_call" && block.type !== "tool_result") continue;
|
|
4703
5620
|
const callId = block.tool_call_id;
|
|
4704
5621
|
if (block.type === "tool_result") {
|
|
4705
|
-
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(
|
|
5622
|
+
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(record3.id, "tool_call", callId);
|
|
4706
5623
|
if (call) links.push({ ...call, tool_call_id: callId });
|
|
4707
5624
|
} else {
|
|
4708
|
-
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(
|
|
4709
|
-
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(
|
|
5625
|
+
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(record3.id, "tool_call", callId);
|
|
5626
|
+
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(record3.id, next?.id ?? Number.MAX_SAFE_INTEGER, "tool_result", callId);
|
|
4710
5627
|
links.push(...results.map((result) => ({ ...result, tool_call_id: callId })));
|
|
4711
5628
|
}
|
|
4712
5629
|
}
|
|
@@ -4750,14 +5667,14 @@ var Memory = class {
|
|
|
4750
5667
|
return this.history().usage(since);
|
|
4751
5668
|
}
|
|
4752
5669
|
read(id) {
|
|
4753
|
-
const
|
|
4754
|
-
return
|
|
5670
|
+
const record3 = this.history().read(id);
|
|
5671
|
+
return record3 ? { record: record3, links: this.history().links(record3) } : null;
|
|
4755
5672
|
}
|
|
4756
5673
|
readMany(ids, mode) {
|
|
4757
5674
|
const found = mode === "summary" ? this.history().summaries(ids).map((item) => item && summaryLine(item)) : this.history().details(ids);
|
|
4758
5675
|
const result = { mode, records: [], missingIds: [] };
|
|
4759
|
-
found.forEach((
|
|
4760
|
-
if (
|
|
5676
|
+
found.forEach((record3, index) => {
|
|
5677
|
+
if (record3) result.records.push(record3);
|
|
4761
5678
|
else result.missingIds.push(ids[index]);
|
|
4762
5679
|
});
|
|
4763
5680
|
return result;
|
|
@@ -4769,7 +5686,7 @@ var Memory = class {
|
|
|
4769
5686
|
};
|
|
4770
5687
|
|
|
4771
5688
|
// src/server/service/bot/bot.ts
|
|
4772
|
-
import { createHash as
|
|
5689
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
4773
5690
|
var Bot = class extends Member {
|
|
4774
5691
|
kind = "bot";
|
|
4775
5692
|
identity;
|
|
@@ -4860,7 +5777,7 @@ var Bot = class extends Member {
|
|
|
4860
5777
|
...message.question ? { question: message.question } : {},
|
|
4861
5778
|
...message.alsoToChannel ? { alsoToChannel: true } : {},
|
|
4862
5779
|
...message.replyTo ? { replyTo: message.replyTo } : {},
|
|
4863
|
-
clientMessageId: source2 ?
|
|
5780
|
+
clientMessageId: source2 ? createHash5("sha256").update(JSON.stringify([this.id, source2.historyId, source2.toolCallId])).digest("hex") : void 0
|
|
4864
5781
|
});
|
|
4865
5782
|
}
|
|
4866
5783
|
/** 由 Bot 协调配置与运行实例;配置在应用成功后提交。执行中切换从下一次模型请求生效(A28)。 */
|
|
@@ -4883,19 +5800,20 @@ var Bot = class extends Member {
|
|
|
4883
5800
|
identity: () => this.identity.snapshot(),
|
|
4884
5801
|
model: () => this.model.current(),
|
|
4885
5802
|
workspace: this.workspace.location(),
|
|
5803
|
+
home: () => this.workspace.home(),
|
|
4886
5804
|
tools: this.tools.definitions(),
|
|
4887
5805
|
onStatus: (status) => this.updateState(status),
|
|
4888
|
-
onHistory: (
|
|
5806
|
+
onHistory: (record3) => this.recordHistory(record3)
|
|
4889
5807
|
};
|
|
4890
5808
|
}
|
|
4891
5809
|
updateState(status) {
|
|
4892
5810
|
this.state.update(status);
|
|
4893
5811
|
}
|
|
4894
|
-
async recordHistory(
|
|
4895
|
-
const historyId = await this.memory.append(
|
|
4896
|
-
if (
|
|
5812
|
+
async recordHistory(record3) {
|
|
5813
|
+
const historyId = await this.memory.append(record3);
|
|
5814
|
+
if (record3.role !== "assistant") return;
|
|
4897
5815
|
this.callHistory.clear();
|
|
4898
|
-
for (const block of
|
|
5816
|
+
for (const block of record3.message.content) {
|
|
4899
5817
|
if (block.type === "tool_call") this.callHistory.set(block.tool_call_id, historyId);
|
|
4900
5818
|
}
|
|
4901
5819
|
}
|
|
@@ -4926,10 +5844,10 @@ var Identity = class {
|
|
|
4926
5844
|
|
|
4927
5845
|
// src/server/service/bot/model.ts
|
|
4928
5846
|
var Model = class {
|
|
4929
|
-
constructor(botId, store,
|
|
5847
|
+
constructor(botId, store, resolve5) {
|
|
4930
5848
|
this.botId = botId;
|
|
4931
5849
|
this.store = store;
|
|
4932
|
-
this.resolve =
|
|
5850
|
+
this.resolve = resolve5;
|
|
4933
5851
|
}
|
|
4934
5852
|
current() {
|
|
4935
5853
|
const bot = this.store.getBot(this.botId);
|
|
@@ -4950,43 +5868,86 @@ var Model = class {
|
|
|
4950
5868
|
}
|
|
4951
5869
|
};
|
|
4952
5870
|
|
|
5871
|
+
// src/server/service/bot/workspace.ts
|
|
5872
|
+
import { posix } from "node:path";
|
|
5873
|
+
|
|
4953
5874
|
// src/server/infra/files/workspace.ts
|
|
4954
|
-
import { mkdir as
|
|
4955
|
-
import { join as
|
|
5875
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "node:fs/promises";
|
|
5876
|
+
import { join as join6 } from "node:path";
|
|
4956
5877
|
var LocalWorkspace = class {
|
|
4957
5878
|
constructor(cwd) {
|
|
4958
5879
|
this.cwd = cwd;
|
|
4959
5880
|
}
|
|
4960
5881
|
get tempDir() {
|
|
4961
|
-
return
|
|
5882
|
+
return join6(this.cwd, "temp");
|
|
4962
5883
|
}
|
|
4963
5884
|
async prepare() {
|
|
4964
|
-
await
|
|
5885
|
+
await mkdir4(this.tempDir, { recursive: true, mode: 448 });
|
|
4965
5886
|
}
|
|
4966
5887
|
async writeTempFile(name2, content2, signal) {
|
|
4967
5888
|
await this.prepare();
|
|
4968
|
-
const path =
|
|
4969
|
-
await
|
|
5889
|
+
const path = join6(this.tempDir, name2);
|
|
5890
|
+
await writeFile3(path, content2, { encoding: "utf8", flag: "wx", mode: 384, signal });
|
|
4970
5891
|
return path;
|
|
4971
5892
|
}
|
|
4972
5893
|
};
|
|
4973
5894
|
|
|
4974
5895
|
// src/server/service/bot/workspace.ts
|
|
5896
|
+
var MAX_FILE = 10 * 1024 * 1024;
|
|
4975
5897
|
var Workspace = class {
|
|
4976
|
-
|
|
4977
|
-
|
|
5898
|
+
constructor(cwd, current, home) {
|
|
5899
|
+
this.current = current;
|
|
5900
|
+
this.home = home;
|
|
4978
5901
|
this.local = new LocalWorkspace(cwd);
|
|
4979
5902
|
}
|
|
5903
|
+
local;
|
|
4980
5904
|
prepare() {
|
|
4981
5905
|
return this.local.prepare();
|
|
4982
5906
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
5907
|
+
/** A file in the temporary directory of the environment the Bot works in, by the path it sees there. */
|
|
5908
|
+
async writeTempFile(name2, content2, signal) {
|
|
5909
|
+
const env = this.current();
|
|
5910
|
+
if (env.kind === "host") return this.local.writeTempFile(name2, content2, signal);
|
|
5911
|
+
const path = `${env.cwd}/temp/${name2}`;
|
|
5912
|
+
await env.writeFile(path, content2);
|
|
5913
|
+
return path;
|
|
5914
|
+
}
|
|
5915
|
+
/** Puts a chat file into the Bot's environment: at `path`, or in temp/files/ under its own name (with part of its
|
|
5916
|
+
* id when another file has that name). Returns where it went. */
|
|
5917
|
+
async put(name2, id, data, path) {
|
|
5918
|
+
const env = this.current();
|
|
5919
|
+
let target2 = path ? posix.resolve(env.cwd, path) : `${env.cwd}/temp/files/${plain(name2)}`;
|
|
5920
|
+
if (!path && await env.exists(target2)) {
|
|
5921
|
+
const dot = plain(name2).lastIndexOf("."), stem = dot > 0 ? plain(name2).slice(0, dot) : plain(name2), extension = dot > 0 ? plain(name2).slice(dot) : "";
|
|
5922
|
+
target2 = `${env.cwd}/temp/files/${stem}-${id.slice(0, 8)}${extension}`;
|
|
5923
|
+
}
|
|
5924
|
+
await env.mkdir(posix.dirname(target2));
|
|
5925
|
+
await env.writeFile(target2, data);
|
|
5926
|
+
return target2;
|
|
5927
|
+
}
|
|
5928
|
+
/** The files a Bot attaches to a message, read from its environment: paths there, relative to its directory. */
|
|
5929
|
+
async readOutgoing(items, signal) {
|
|
5930
|
+
if (items.length > 5) throw new ChatError(400, "Too many attachments");
|
|
5931
|
+
const env = this.current(), files = [];
|
|
5932
|
+
for (const item of items) {
|
|
5933
|
+
signal?.throwIfAborted();
|
|
5934
|
+
const path = posix.resolve(env.cwd, item.path), size = await env.fileSize(path);
|
|
5935
|
+
if (size === null) throw new ChatError(400, `No file at ${path} in your environment`);
|
|
5936
|
+
if (!size || size > MAX_FILE) throw new ChatError(400, "Invalid attachment size or type");
|
|
5937
|
+
const data = await env.readFile(path);
|
|
5938
|
+
if (data.length !== size) throw new ChatError(409, "Attachment changed while reading");
|
|
5939
|
+
files.push({ name: item.name, mimeType: "application/octet-stream", data });
|
|
5940
|
+
}
|
|
5941
|
+
return files;
|
|
4985
5942
|
}
|
|
4986
5943
|
location() {
|
|
4987
5944
|
return { type: "local", cwd: this.local.cwd, tempDir: this.local.tempDir };
|
|
4988
5945
|
}
|
|
4989
5946
|
};
|
|
5947
|
+
function plain(name2) {
|
|
5948
|
+
const cleaned = name2.replace(/[/\\\0]/g, "_").trim();
|
|
5949
|
+
return cleaned && cleaned !== "." && cleaned !== ".." ? cleaned : "file";
|
|
5950
|
+
}
|
|
4990
5951
|
|
|
4991
5952
|
// src/server/service/bot/state.ts
|
|
4992
5953
|
var BotState = class {
|
|
@@ -5054,20 +6015,20 @@ ${content(block.result.content, heading + "#")}`;
|
|
|
5054
6015
|
function formatHistory(result, format) {
|
|
5055
6016
|
if (format === "json") return JSON.stringify(result, null, 2);
|
|
5056
6017
|
const sections = [`# History (${result.mode})`];
|
|
5057
|
-
for (const
|
|
5058
|
-
sections.push(`## #${
|
|
5059
|
-
if (
|
|
6018
|
+
for (const record3 of result.records) {
|
|
6019
|
+
sections.push(`## #${record3.id} \xB7 ${record3.role}`, `Time: ${new Date(record3.timestamp).toISOString()}`);
|
|
6020
|
+
if (record3.model) sections.push(`Model:
|
|
5060
6021
|
|
|
5061
|
-
${jsonBlock(
|
|
5062
|
-
if ("preview" in
|
|
5063
|
-
sections.push(fence(
|
|
6022
|
+
${jsonBlock(record3.model)}`);
|
|
6023
|
+
if ("preview" in record3) {
|
|
6024
|
+
sections.push(fence(record3.preview));
|
|
5064
6025
|
continue;
|
|
5065
6026
|
}
|
|
5066
|
-
sections.push(content(
|
|
5067
|
-
if (
|
|
5068
|
-
if (
|
|
5069
|
-
if (
|
|
5070
|
-
if (
|
|
6027
|
+
sections.push(content(record3.message.content));
|
|
6028
|
+
if (record3.message.tools !== void 0) sections.push("### Tool definitions", jsonBlock(record3.message.tools));
|
|
6029
|
+
if (record3.message.tools_removed !== void 0) sections.push("### Tools removed", jsonBlock(record3.message.tools_removed));
|
|
6030
|
+
if (record3.message.stop_reason !== void 0) sections.push("### Stop reason", fence(record3.message.stop_reason));
|
|
6031
|
+
if (record3.message.error !== void 0) sections.push("### Error", fence(record3.message.error));
|
|
5071
6032
|
}
|
|
5072
6033
|
if (result.missingIds.length) sections.push(`## Missing IDs
|
|
5073
6034
|
|
|
@@ -5093,6 +6054,38 @@ async function readHistory(memory, workspace, input, signal) {
|
|
|
5093
6054
|
return workspace.writeTempFile(name2, rendered, signal);
|
|
5094
6055
|
}
|
|
5095
6056
|
|
|
6057
|
+
// src/server/service/bot/file-view.ts
|
|
6058
|
+
var TEXT_LIMIT = 64 * 1024;
|
|
6059
|
+
var PICTURE_LIMIT = 4 * 1024 * 1024;
|
|
6060
|
+
function viewFile(file, data) {
|
|
6061
|
+
const head = `${file.name} (${file.mimeType || "application/octet-stream"}, ${byteSize(file.size)})`;
|
|
6062
|
+
const picture = pictureType(data);
|
|
6063
|
+
if (picture) {
|
|
6064
|
+
if (data.length > PICTURE_LIMIT) throw new ChatError(400, "This picture is over 4 MB; use files.get and look at a smaller copy");
|
|
6065
|
+
return { kind: "media", text: head, images: [{ data: data.toString("base64"), mimeType: picture }] };
|
|
6066
|
+
}
|
|
6067
|
+
const text3 = data.length <= TEXT_LIMIT ? asText(data) : null;
|
|
6068
|
+
if (text3 !== null) return `${head}
|
|
6069
|
+
|
|
6070
|
+
${text3}`;
|
|
6071
|
+
throw new ChatError(400, "files.view shows pictures and text files up to 64 KB; use files.get to work on this file with your tools");
|
|
6072
|
+
}
|
|
6073
|
+
function pictureType(data) {
|
|
6074
|
+
if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return "image/png";
|
|
6075
|
+
if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
6076
|
+
if (data.length >= 6 && /^GIF8[79]a$/.test(data.subarray(0, 6).toString("latin1"))) return "image/gif";
|
|
6077
|
+
if (data.length >= 12 && data.subarray(0, 4).toString("latin1") === "RIFF" && data.subarray(8, 12).toString("latin1") === "WEBP") return "image/webp";
|
|
6078
|
+
return null;
|
|
6079
|
+
}
|
|
6080
|
+
function asText(data) {
|
|
6081
|
+
if (data.includes(0)) return null;
|
|
6082
|
+
try {
|
|
6083
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
6084
|
+
} catch {
|
|
6085
|
+
return null;
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
|
|
5096
6089
|
// src/server/service/bot/chat-views.ts
|
|
5097
6090
|
var iso = (time) => new Date(time).toISOString();
|
|
5098
6091
|
var name = (chats) => (memberId) => chats.member(memberId)?.name ?? memberId;
|
|
@@ -5318,7 +6311,8 @@ var ChatTools = class {
|
|
|
5318
6311
|
return { messages: await this.rows(page, signal), ...messages.length > limit ? { hasMore: true } : {} };
|
|
5319
6312
|
}
|
|
5320
6313
|
async rows(messages, signal) {
|
|
5321
|
-
|
|
6314
|
+
signal?.throwIfAborted();
|
|
6315
|
+
return messages.map((message) => messageRow(this.chats, this.bot, message, listing(message)));
|
|
5322
6316
|
}
|
|
5323
6317
|
search(input) {
|
|
5324
6318
|
const found = input.chatId === void 0 ? this.chats.searchAll(this.bot, input.query, input.limit ?? 30) : this.chats.search(this.bot, this.chat(input.chatId).id, input.query, input.limit ?? 30);
|
|
@@ -5327,13 +6321,7 @@ var ChatTools = class {
|
|
|
5327
6321
|
if (!chats.has(id)) chats.set(id, this.chats.get(this.bot, id));
|
|
5328
6322
|
return chats.get(id);
|
|
5329
6323
|
};
|
|
5330
|
-
return found.map((message) => messageRow(
|
|
5331
|
-
this.chats,
|
|
5332
|
-
this.bot,
|
|
5333
|
-
message,
|
|
5334
|
-
message.attachments.map((file) => ({ name: file.name })),
|
|
5335
|
-
input.chatId === void 0 ? chatOf(message.chatId) : null
|
|
5336
|
-
));
|
|
6324
|
+
return found.map((message) => messageRow(this.chats, this.bot, message, listing(message), input.chatId === void 0 ? chatOf(message.chatId) : null));
|
|
5337
6325
|
}
|
|
5338
6326
|
};
|
|
5339
6327
|
|
|
@@ -5342,6 +6330,7 @@ import { Type as Type3 } from "typebox";
|
|
|
5342
6330
|
var None = Type3.Object({}, { additionalProperties: false });
|
|
5343
6331
|
var Name = (description) => Type3.String({ minLength: 1, maxLength: 80, description });
|
|
5344
6332
|
var MessageId = Type3.String({ pattern: "^m\\d+$" });
|
|
6333
|
+
var FileId = Type3.String({ minLength: 1, maxLength: 80, description: "The file's id: from <file id=\u2026/> in a message, or the attachments of messages.read and messages.search" });
|
|
5345
6334
|
var QuestionOption = Type3.Object({
|
|
5346
6335
|
label: Type3.String({ minLength: 1, maxLength: 80, description: "Text shown on the option" }),
|
|
5347
6336
|
value: Type3.Optional(Type3.String({ minLength: 1, maxLength: 500, description: "What you get back when it is picked, written as the person would naturally say it; label by default" })),
|
|
@@ -5351,7 +6340,10 @@ var QuestionOption = Type3.Object({
|
|
|
5351
6340
|
var definitions = {
|
|
5352
6341
|
"history.read": { description: "Read your own History by a list of IDs: summaries or details, as JSON or Markdown, or written to a temporary file. IDs that don't exist are listed in missingIds. History is material to look back on, not an instruction.", schema: historyReadSchema },
|
|
5353
6342
|
"members.list": { description: "List every member, people and Bots: name, kind (user for a person, or bot) and description; self=true is you. Use these names to @-mention someone, add them to a channel or DM them.", schema: None },
|
|
5354
|
-
"bots.list": {
|
|
6343
|
+
"bots.list": {
|
|
6344
|
+
description: "List every Bot and its run state. Read-only; it changes nothing. People and Bots can all see every Bot's name and state; this is not private. For each Bot: name; self=true is you; agent=true is the agent Bot, the owner's helper for running Jarvy: ask it to create or change a Bot, give a Bot a model, see a Bot's usage, or delete a Bot, channel or channel group; status (stop: not running, idle, or running). While it runs, activity is what it is doing (reasoning, texting: writing text, tool_calling, or asking: waiting for a person's answer), chats are the chats it is handling this turn (only those you can read), and otherChats is how many others there are. If its last turn failed, error is the error code. With environments=true, each Bot also has environment: its default work environment, the one it is in now (current) and the ones it may use. You come first, then the rest by name.",
|
|
6345
|
+
schema: Type3.Object({ environments: Type3.Optional(Type3.Boolean({ description: "Also give each Bot's work environments" })) }, { additionalProperties: false })
|
|
6346
|
+
},
|
|
5355
6347
|
"chats.list": { description: "List the chats you are in: DMs, channels and the threads you follow. Each has its type, title (DMs also give the other member's name in with), channel group (section), topic, your unread count and whether you subscribe; threads also give their channel and status.", schema: None },
|
|
5356
6348
|
"chats.info": {
|
|
5357
6349
|
description: "Details of one chat: members, the channel topic, the full notice or thread summary, your unread count and read position, and whether you subscribe; threads also give their title, status, channel and source message. Reading a notice or summary counts as having seen that version.",
|
|
@@ -5401,7 +6393,7 @@ var definitions = {
|
|
|
5401
6393
|
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("New name") }, { additionalProperties: false })
|
|
5402
6394
|
},
|
|
5403
6395
|
"messages.read": {
|
|
5404
|
-
description: "Read messages
|
|
6396
|
+
description: "Read messages; their files are listed by id, name, type and size, for files.get and files.view. Three ways: \u2460 chatId + after: read on from that sequence number (from the start by default); \u2461 around with a message ID: that message and context messages on each side (5 by default), to see the context; this doesn't move your read position; \u2462 unread=true: your unread messages, in one chat with chatId, or in every chat (grouped by chat) without it. \u2460 and \u2462 move your read position to the last message read; hasMore=true means there is more, so call again.",
|
|
5405
6397
|
schema: Type3.Object({
|
|
5406
6398
|
chatId: Type3.Optional(Type3.String()),
|
|
5407
6399
|
after: Type3.Optional(Type3.Integer({ minimum: 0 })),
|
|
@@ -5412,9 +6404,22 @@ var definitions = {
|
|
|
5412
6404
|
}, { additionalProperties: false })
|
|
5413
6405
|
},
|
|
5414
6406
|
"messages.search": {
|
|
5415
|
-
description: "Search message text. With chatId, only that chat; without it, every chat you can see, and each result says which chat it is in.
|
|
6407
|
+
description: "Search message text. With chatId, only that chat; without it, every chat you can see, and each result says which chat it is in. Results list their files like messages.read.",
|
|
5416
6408
|
schema: Type3.Object({ chatId: Type3.Optional(Type3.String()), query: Type3.String({ minLength: 1, maxLength: 500 }), limit: Type3.Optional(Type3.Integer({ minimum: 1, maximum: 100 })) }, { additionalProperties: false })
|
|
5417
6409
|
},
|
|
6410
|
+
"files.get": {
|
|
6411
|
+
description: "Put a file from a message into your environment, to work on it with your tools; the result has its path there. It goes to temp/files/ under its own name unless you give a path.",
|
|
6412
|
+
schema: Type3.Object({ id: FileId, path: Type3.Optional(Type3.String({ minLength: 1, maxLength: 1e3, description: "Where to put it instead: absolute, or relative to your directory" })) }, { additionalProperties: false })
|
|
6413
|
+
},
|
|
6414
|
+
"environments.list": { description: "List the work environments you may use: your sandbox, your default and the ones your owner granted you. current is the id of the one you are in. Each has its id, name, the directory your file and command tools start in there, default=true for your default, and state for a sandbox (it starts when you switch to it).", schema: None },
|
|
6415
|
+
"environments.switch": {
|
|
6416
|
+
description: "Move your file and command tools (bash, read, write, edit) to another work environment from environments.list, like cd one level up: you stay there, across turns, until you switch back. The result has your directory there and what you can do there. Commands already running finish where they started.",
|
|
6417
|
+
schema: Type3.Object({ id: Type3.String({ minLength: 1, maxLength: 80, description: "The environment's id from environments.list" }) }, { additionalProperties: false })
|
|
6418
|
+
},
|
|
6419
|
+
"files.view": {
|
|
6420
|
+
description: "Look at a file from a message without putting it anywhere: a picture (PNG, JPEG, GIF or WebP, up to 4 MB) is shown to you, and a text file up to 64 KB comes back as text. Anything else needs files.get.",
|
|
6421
|
+
schema: Type3.Object({ id: FileId }, { additionalProperties: false })
|
|
6422
|
+
},
|
|
5418
6423
|
"messages.react": {
|
|
5419
6424
|
description: `Add or remove an emoji reaction on a message (people and Bots can both react). on=false removes it. Giving an emoji the message already has from you removes it instead (and the other way round); marked or unmarked and note in the result say which happened. Use this toggle to signal states such as "got it", "on it" and "done". Only the message's author is told: if they are running, it joins their current turn without interrupting; if they are idle, it starts a turn.`,
|
|
5420
6425
|
schema: Type3.Object({ messageId: MessageId, emoji: Type3.String({ minLength: 1, maxLength: 32, description: "A single emoji, such as \u{1F44D}" }), on: Type3.Optional(Type3.Boolean()) }, { additionalProperties: false })
|
|
@@ -5431,7 +6436,7 @@ var definitions = {
|
|
|
5431
6436
|
}, { additionalProperties: false })
|
|
5432
6437
|
},
|
|
5433
6438
|
"messages.send": {
|
|
5434
|
-
description: "Send a message, with files from
|
|
6439
|
+
description: "Send a message, with files from your environment if you like (path there, absolute or relative to your directory, and the name people see). Give chatId or to, not both: to is a member's name and sends them a DM (a person or a Bot; the first time creates the DM). Only messages sent successfully are visible in the chat. In channels and threads, only the members whose @name is in the text and the members subscribed there receive it; DMs need no @. The result lists who was @-mentioned (notified), who got it through a subscription (subscribers), and the names that aren't in the channel and weren't told. thread: the ID of a message in the channel, to reply in that message's thread (created if it has none; chatId is the channel). replyTo: the ID of a message in the same chat, to reply to it as a quote; this tells its author, like an @. When replying in a thread, alsoToChannel=true also posts a copy to the channel.",
|
|
5435
6440
|
schema: Type3.Object({
|
|
5436
6441
|
chatId: Type3.Optional(Type3.String()),
|
|
5437
6442
|
to: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
@@ -5445,7 +6450,7 @@ var definitions = {
|
|
|
5445
6450
|
};
|
|
5446
6451
|
|
|
5447
6452
|
// src/server/service/bot/roster.ts
|
|
5448
|
-
function botRows(chats, viewerId, entries) {
|
|
6453
|
+
function botRows(chats, viewerId, entries, environment) {
|
|
5449
6454
|
const rows2 = entries.map((entry) => {
|
|
5450
6455
|
const running = entry.status.status === "running";
|
|
5451
6456
|
const visible = [];
|
|
@@ -5463,7 +6468,8 @@ function botRows(chats, viewerId, entries) {
|
|
|
5463
6468
|
...running && entry.status.activity ? { activity: entry.status.activity } : {},
|
|
5464
6469
|
...visible.length ? { chats: visible } : {},
|
|
5465
6470
|
...hidden ? { otherChats: hidden } : {},
|
|
5466
|
-
...entry.errorCode ? { error: entry.errorCode } : {}
|
|
6471
|
+
...entry.errorCode ? { error: entry.errorCode } : {},
|
|
6472
|
+
...environment ? { environment: environment(entry.id) } : {}
|
|
5467
6473
|
} };
|
|
5468
6474
|
});
|
|
5469
6475
|
rows2.sort((a, b) => Number(b.self) - Number(a.self) || a.row.name.localeCompare(b.row.name));
|
|
@@ -5500,8 +6506,9 @@ var AgentTools = class {
|
|
|
5500
6506
|
case "models.list":
|
|
5501
6507
|
return this.ops.models();
|
|
5502
6508
|
case "bots.create": {
|
|
5503
|
-
const
|
|
5504
|
-
|
|
6509
|
+
const { environment, ...settings } = args;
|
|
6510
|
+
const bot = await this.ops.create({ ...settings, ...environment ? { environment: environment === "sandbox" ? "computer" : "host" } : {} });
|
|
6511
|
+
return { name: bot.name, model: bot.connectionId ? this.ops.modelLabel(bot) : null, thinking: bot.thinkingLevel, environment: this.ops.environment(bot.id) === "computer" ? "sandbox" : "host" };
|
|
5505
6512
|
}
|
|
5506
6513
|
case "bots.update": {
|
|
5507
6514
|
const { bot, ...settings } = args;
|
|
@@ -5571,6 +6578,9 @@ var Settings = {
|
|
|
5571
6578
|
model: Type4.Optional(Model2),
|
|
5572
6579
|
thinking: Type4.Optional(Thinking)
|
|
5573
6580
|
};
|
|
6581
|
+
var Environment = Type4.Union([Type4.Literal("sandbox"), Type4.Literal("host")], {
|
|
6582
|
+
description: "Where its file and command tools run by default: sandbox, a Linux container that Jarvy makes and only it uses (the default when Docker is available), or host, the owner's computer, where you work too"
|
|
6583
|
+
});
|
|
5574
6584
|
var Confirm = {
|
|
5575
6585
|
askIn: Type4.String({ description: "ID of a chat the owner is in, where the confirmation card goes; usually the chat you were asked in" }),
|
|
5576
6586
|
question: Type4.String({ minLength: 1, maxLength: 500, description: "The card's question, in the owner's language: what will be deleted and what goes with it" }),
|
|
@@ -5582,7 +6592,7 @@ var agentDefinitions = {
|
|
|
5582
6592
|
"models.list": { description: "List the models you can give a Bot: each connection with its models and their thinking levels. Only the owner adds connections, keys and logins, in Settings.", schema: None2 },
|
|
5583
6593
|
"bots.create": {
|
|
5584
6594
|
description: "Create a Bot. Give it a model so it can work; without one it waits until it gets one. It starts when a message first arrives for it. bots.update changes everything later.",
|
|
5585
|
-
schema: Type4.Object({ name: Name2, ...Settings }, { additionalProperties: false })
|
|
6595
|
+
schema: Type4.Object({ name: Name2, ...Settings, environment: Type4.Optional(Environment) }, { additionalProperties: false })
|
|
5586
6596
|
},
|
|
5587
6597
|
"bots.update": {
|
|
5588
6598
|
description: "Change a Bot's name, description, persona, model or thinking; give only what changes. Works for any Bot, yourself included. The Bot is told what changed with its next batch, in its DM with you.",
|
|
@@ -5676,7 +6686,16 @@ var Tools = class {
|
|
|
5676
6686
|
case "members.list":
|
|
5677
6687
|
return this.tools.membersList();
|
|
5678
6688
|
case "bots.list":
|
|
5679
|
-
return botRows(
|
|
6689
|
+
return botRows(
|
|
6690
|
+
this.scope.chats,
|
|
6691
|
+
this.scope.botId,
|
|
6692
|
+
this.scope.roster(),
|
|
6693
|
+
args.environments ? (id) => this.scope.places.summary(id) : void 0
|
|
6694
|
+
);
|
|
6695
|
+
case "environments.list":
|
|
6696
|
+
return this.scope.places.list();
|
|
6697
|
+
case "environments.switch":
|
|
6698
|
+
return this.scope.places.switch(args.id);
|
|
5680
6699
|
case "chats.create":
|
|
5681
6700
|
return this.tools.create(args);
|
|
5682
6701
|
case "chats.update":
|
|
@@ -5710,6 +6729,16 @@ var Tools = class {
|
|
|
5710
6729
|
}
|
|
5711
6730
|
case "messages.read":
|
|
5712
6731
|
return this.tools.read(args, signal);
|
|
6732
|
+
case "files.get": {
|
|
6733
|
+
const { id, path } = args;
|
|
6734
|
+
const { file, data } = this.scope.files.open(this.scope.botId, id);
|
|
6735
|
+
signal?.throwIfAborted();
|
|
6736
|
+
return { path: await this.scope.workspace.put(file.name, id, data, path), name: file.name, type: file.mimeType, size: byteSize(file.size) };
|
|
6737
|
+
}
|
|
6738
|
+
case "files.view": {
|
|
6739
|
+
const { file, data } = this.scope.files.open(this.scope.botId, args.id);
|
|
6740
|
+
return viewFile(file, data);
|
|
6741
|
+
}
|
|
5713
6742
|
case "messages.search":
|
|
5714
6743
|
return this.tools.search(args);
|
|
5715
6744
|
case "messages.send": {
|
|
@@ -5764,10 +6793,7 @@ var Tools = class {
|
|
|
5764
6793
|
};
|
|
5765
6794
|
|
|
5766
6795
|
// src/server/service/bot/inbox.ts
|
|
5767
|
-
import { join as
|
|
5768
|
-
|
|
5769
|
-
// src/server/service/chat/envelope.ts
|
|
5770
|
-
import { isAbsolute } from "node:path";
|
|
6796
|
+
import { join as join7, dirname as dirname6 } from "node:path";
|
|
5771
6797
|
|
|
5772
6798
|
// src/server/shared/agent.ts
|
|
5773
6799
|
var WELCOME_LANGUAGES = { en: "English", zh: "Chinese" };
|
|
@@ -5788,13 +6814,14 @@ function remember(map, id, definition) {
|
|
|
5788
6814
|
if (old !== void 0 && old !== definition) throw new Error("Conflicting source metadata");
|
|
5789
6815
|
map.set(id, definition);
|
|
5790
6816
|
}
|
|
6817
|
+
var environmentLine = (place) => place ? `<environment${attribute("name", place.name)}${attribute("directory", place.directory)}/>` : "";
|
|
5791
6818
|
var iso2 = (time) => new Date(time).toISOString().replace(".000Z", "Z");
|
|
5792
6819
|
var welcome = (owner, language) => `${owner} has just set up Jarvy and picked your model. You are their agent Bot. In this DM, welcome them in ${WELCOME_LANGUAGES[language]}, the language of their interface: say in a few sentences what Jarvy is and how you can help, such as building a team of Bots for their work and running Jarvy for them. Then ask what they want to get done: a software project, writing, research or something else.`;
|
|
5793
|
-
function encodeNoticeBatch(botId, notices) {
|
|
6820
|
+
function encodeNoticeBatch(botId, notices, environment) {
|
|
5794
6821
|
if (!notices.length) throw new Error("Empty notice batch");
|
|
5795
6822
|
const chats = /* @__PURE__ */ new Map(), members = /* @__PURE__ */ new Map();
|
|
5796
6823
|
const list = noticeTail(notices, chats, members);
|
|
5797
|
-
return `<notice_batch${attribute("bot", botId)}>${[...chats.values()].join("")}${[...members.values()].join("")}${list}</notice_batch>`;
|
|
6824
|
+
return `<notice_batch${attribute("bot", botId)}>${[...chats.values()].join("")}${[...members.values()].join("")}${list}${environmentLine(environment)}</notice_batch>`;
|
|
5798
6825
|
}
|
|
5799
6826
|
function noticeTail(notices, chats, members) {
|
|
5800
6827
|
for (const notice of notices) {
|
|
@@ -5805,9 +6832,11 @@ function noticeTail(notices, chats, members) {
|
|
|
5805
6832
|
for (const notice of notices) if (notice.kind === "note" && (latest.get(notice.chat.id)?.version ?? 0) < notice.version) latest.set(notice.chat.id, notice);
|
|
5806
6833
|
const updates = [...latest.values()].map((note) => `<${note.chat.type === "thread" ? "summary" : "notice"}${attribute("chat", note.chat.id)}${attribute("version", String(note.version))}${attribute("by", note.actor.id)}${attribute("at", iso2(note.createdAt))}/>`);
|
|
5807
6834
|
const marks = notices.flatMap((notice) => notice.kind !== "reaction" ? [] : [`<reaction${attribute("message", notice.message.id)}${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("emoji", notice.emoji)}${attribute("at", iso2(notice.createdAt))}>${escape(notice.message.text)}</reaction>`]);
|
|
5808
|
-
const
|
|
6835
|
+
const changed = notices.filter((notice) => notice.kind === "change");
|
|
6836
|
+
const changes = changed.map((notice) => `<change${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("what", notice.what)}${attribute("at", iso2(notice.createdAt))}${notice.value === null ? "/>" : `>${escape(notice.value)}</change>`}`);
|
|
6837
|
+
const byAgent = changed.some((notice) => notice.actor.kind === "bot"), byOwner = changed.some((notice) => notice.actor.kind === "user");
|
|
5809
6838
|
const onboard = notices.flatMap((notice) => notice.kind !== "onboard" ? [] : [`<onboard${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("at", iso2(notice.createdAt))}><note>${escape(welcome(notice.actor.name, notice.language))}</note></onboard>`]).slice(-1);
|
|
5810
|
-
return (updates.length ? `<updates>${updates.join("")}</updates>` : "") + (marks.length ? `<notices>${marks.join("")}<note>These are reactions to your messages. Act on them if you need to; no reply is required.</note></notices>` : "") + (changes.length ? `<changes>${changes.join("")}<note>The agent Bot changed your settings, and the changes already apply. A persona change is already in your instructions. If something is unclear, ask it in that DM.</note
|
|
6839
|
+
return (updates.length ? `<updates>${updates.join("")}</updates>` : "") + (marks.length ? `<notices>${marks.join("")}<note>These are reactions to your messages. Act on them if you need to; no reply is required.</note></notices>` : "") + (changes.length ? `<changes>${changes.join("")}` + (byAgent ? "<note>The agent Bot changed your settings, and the changes already apply. A persona change is already in your instructions. If something is unclear, ask it in that DM.</note>" : "") + (byOwner ? "<note>Your owner changed where you work, and it already applies: environment is where your file and command tools run now, and access is what environments.switch can take you to. If you were in the middle of something, carry on where you are now.</note>" : "") + "</changes>" : "") + onboard.join("");
|
|
5811
6840
|
}
|
|
5812
6841
|
function encodeChatBatch(botId, messages, digest, notices) {
|
|
5813
6842
|
if (!messages.length) throw new Error("Empty chat batch");
|
|
@@ -5818,30 +6847,30 @@ function encodeChatBatch(botId, messages, digest, notices) {
|
|
|
5818
6847
|
const { chat } = message;
|
|
5819
6848
|
remember(chats, chat.id, `<chat${attribute("id", chat.id)}${attribute("type", chat.type)}${chat.title ? attribute("title", chat.title) : ""}${chat.section ? attribute("section", chat.section) : ""}${chat.parent ? attribute("parent", chat.parent) : ""}${chat.root ? attribute("root", chat.root) : ""}${chat.status ? attribute("status", chat.status) : ""}/>`);
|
|
5820
6849
|
remember(members, message.sender.id, `<member${attribute("id", message.sender.id)}${attribute("name", message.sender.name)}${attribute("kind", message.sender.kind)}/>`);
|
|
5821
|
-
const
|
|
5822
|
-
const attributes = attribute("id", message.id) + attribute("chat", message.chat.id) + attribute("from", message.sender.id) + attribute("at",
|
|
6850
|
+
const at2 = new Date(message.createdAt).toISOString().replace(".000Z", "Z");
|
|
6851
|
+
const attributes = attribute("id", message.id) + attribute("chat", message.chat.id) + attribute("from", message.sender.id) + attribute("at", at2);
|
|
5823
6852
|
const reply = message.replyTo;
|
|
5824
6853
|
if (reply) remember(members, reply.sender.id, `<member${attribute("id", reply.sender.id)}${attribute("name", reply.sender.name)}${attribute("kind", reply.sender.kind)}/>`);
|
|
5825
6854
|
const quote = reply ? `<reply_to${attribute("message", reply.id)}${attribute("from", reply.sender.id)}>${escape(reply.text)}</reply_to>` : "";
|
|
5826
|
-
const files = message.attachments.map((file) => {
|
|
5827
|
-
if (!isAbsolute(file.path)) throw new Error("Attachment path must be absolute");
|
|
5828
|
-
return `<file${attribute("path", file.path)}${attribute("name", file.name)}/>`;
|
|
5829
|
-
}).join("");
|
|
6855
|
+
const files = message.attachments.map((file) => `<file${attribute("id", file.id)}${attribute("name", file.name)}${attribute("type", file.type)}${attribute("size", file.size)}/>`).join("");
|
|
5830
6856
|
return `<message${attributes}>${quote}${message.content ? `<text>${escape(message.content)}</text>` : ""}${files}</message>`;
|
|
5831
6857
|
});
|
|
5832
6858
|
const ref = (key, item) => attribute(key, item.chatId) + (chats.has(item.chatId) ? "" : attribute("title", item.title) + attribute("type", item.type));
|
|
5833
6859
|
let tail = notices?.length ? noticeTail(notices, chats, members) : "";
|
|
5834
6860
|
if (digest?.unread.length) tail += `<unread${attribute("total", String(digest.unread.reduce((sum, item) => sum + item.count, 0)))}>${digest.unread.map((item) => `<chat${ref("id", item)}${attribute("count", String(item.count))}/>`).join("")}</unread>`;
|
|
6861
|
+
tail += environmentLine(digest?.environment);
|
|
5835
6862
|
return `<chat_batch${attribute("bot", botId)}>${[...chats.values()].join("")}${[...members.values()].join("")}${encoded.join("")}${tail}</chat_batch>`;
|
|
5836
6863
|
}
|
|
5837
6864
|
|
|
5838
6865
|
// src/server/service/bot/inbox.ts
|
|
5839
6866
|
var BotInbox = class {
|
|
5840
|
-
|
|
6867
|
+
/** `where` is the work environment the Bot is in now, named at the end of each batch (A50). */
|
|
6868
|
+
constructor(botId, repository, chats, files, where2) {
|
|
5841
6869
|
this.botId = botId;
|
|
5842
6870
|
this.repository = repository;
|
|
5843
6871
|
this.chats = chats;
|
|
5844
6872
|
this.files = files;
|
|
6873
|
+
this.where = where2;
|
|
5845
6874
|
}
|
|
5846
6875
|
/** Messages and other deliveries wait in the inbox (A41); either kind is work for the next run. A digest delivery
|
|
5847
6876
|
* (a change the agent Bot made, A48) is not work of its own: it rides along with the next batch. */
|
|
@@ -5856,7 +6885,7 @@ var BotInbox = class {
|
|
|
5856
6885
|
}
|
|
5857
6886
|
reference(workspace) {
|
|
5858
6887
|
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.");
|
|
5859
|
-
return this.repository.ensureSession(this.botId,
|
|
6888
|
+
return this.repository.ensureSession(this.botId, join7(dirname6(workspace), "agent", "pi", "sessions", `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`));
|
|
5860
6889
|
}
|
|
5861
6890
|
/** XML for the claimed messages plus the tail (other deliveries not told yet, unread); `notices` are told once
|
|
5862
6891
|
* the batch is submitted. */
|
|
@@ -5867,7 +6896,7 @@ var BotInbox = class {
|
|
|
5867
6896
|
}
|
|
5868
6897
|
const inputs = await this.files.envelope(this.botId, batch.inputs.map((input) => input.messageId));
|
|
5869
6898
|
for (const input of batch.inputs) if (!this.chats.get(this.botId, input.chatId)) throw new RuntimeFault("chat_access_changed", "Chat access has changed.");
|
|
5870
|
-
const digest = this.chats.channels.digest(this.botId);
|
|
6899
|
+
const digest = { ...this.chats.channels.digest(this.botId), ...this.where ? { environment: this.where() } : {} };
|
|
5871
6900
|
const pending = this.envelope(this.chats.notices(this.botId));
|
|
5872
6901
|
return { xml: encodeChatBatch(this.botId, inputs, digest, pending.length ? pending : void 0), notices: pending.map((notice) => notice.id) };
|
|
5873
6902
|
}
|
|
@@ -5894,7 +6923,7 @@ var BotInbox = class {
|
|
|
5894
6923
|
notices() {
|
|
5895
6924
|
const pending = this.chats.notices(this.botId);
|
|
5896
6925
|
if (!pending.length) return null;
|
|
5897
|
-
return { xml: encodeNoticeBatch(this.botId, this.envelope(pending)), ids: pending.map((notice) => notice.id) };
|
|
6926
|
+
return { xml: encodeNoticeBatch(this.botId, this.envelope(pending), this.where?.()), ids: pending.map((notice) => notice.id) };
|
|
5898
6927
|
}
|
|
5899
6928
|
/** These notices reached the model. */
|
|
5900
6929
|
tell(ids) {
|
|
@@ -5927,8 +6956,8 @@ var AgentRuntime = class {
|
|
|
5927
6956
|
return this.runId;
|
|
5928
6957
|
}
|
|
5929
6958
|
update(patch) {
|
|
5930
|
-
const
|
|
5931
|
-
this.status = { ...this.status, ...patch, context:
|
|
6959
|
+
const context2 = this.session?.context;
|
|
6960
|
+
this.status = { ...this.status, ...patch, context: context2 ? { tokens: context2.tokens, limit: context2.contextWindow } : null };
|
|
5932
6961
|
this.setup?.onStatus(this.status);
|
|
5933
6962
|
}
|
|
5934
6963
|
activity(phase) {
|
|
@@ -5939,9 +6968,9 @@ var AgentRuntime = class {
|
|
|
5939
6968
|
if (this.prompting) this.update({ status: "running", activity: on ? "asking" : "tool_calling" });
|
|
5940
6969
|
}
|
|
5941
6970
|
fail(error) {
|
|
5942
|
-
const
|
|
5943
|
-
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code:
|
|
5944
|
-
console.error("[bot] execution failed", { botId: this.inbox.botId, runId: this.runId, code:
|
|
6971
|
+
const fault2 = publicFault(error);
|
|
6972
|
+
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault2.code, message: fault2.message, ...fault2.params ? { params: fault2.params } : {} } });
|
|
6973
|
+
console.error("[bot] execution failed", { botId: this.inbox.botId, runId: this.runId, code: fault2.code, ...error instanceof RuntimeFault ? {} : { error } });
|
|
5945
6974
|
}
|
|
5946
6975
|
async start(setup) {
|
|
5947
6976
|
await this.operation(async () => {
|
|
@@ -6065,7 +7094,7 @@ var AgentRuntime = class {
|
|
|
6065
7094
|
return task;
|
|
6066
7095
|
}
|
|
6067
7096
|
// Model/thinking changes queue behind start/stop and each other but not behind the running turn:
|
|
6068
|
-
// Pi
|
|
7097
|
+
// Pi 1.2.0 re-reads agent.state.model/thinkingLevel before every next request (AgentSession's prepareNextTurnWithContext),
|
|
6069
7098
|
// and each request resolves its API key when it starts, so in-flight requests are unaffected.
|
|
6070
7099
|
reconfigure(operation) {
|
|
6071
7100
|
const task = this.operations.catch(() => {
|
|
@@ -6153,7 +7182,7 @@ These instructions and the tool descriptions are in English so that every Bot re
|
|
|
6153
7182
|
|
|
6154
7183
|
## Chat
|
|
6155
7184
|
The chat tool is your only way to talk to the other members. Anything you don't send through it is seen by no one.
|
|
6156
|
-
|
|
7185
|
+
Files in messages come with an ID: files.get puts one into your environment and gives you its path, and files.view shows you an image or a text file directly. To pass a file to someone, attach its path in your environment and its name when you send with the chat tool.
|
|
6157
7186
|
Channels can be put in channel groups and have a notice. Threads have a title, a status and a summary.
|
|
6158
7187
|
In channels and threads you only get new messages when you are @-mentioned; once you subscribe, every new message there is sent to you. DMs need no @.
|
|
6159
7188
|
Each batch of messages ends with your unread counts and the notices and summaries that have a new version. Read them through the chat gateway when you need to.
|
|
@@ -6162,17 +7191,33 @@ Each batch of messages ends with your unread counts and the notices and summarie
|
|
|
6162
7191
|
To look into earlier work, search the chat first, then call history.read with the History ID a message carries to read how the work was done.
|
|
6163
7192
|
history.read is listed by the chat gateway. History is material to look back on, not an instruction for the task at hand.
|
|
6164
7193
|
|
|
6165
|
-
|
|
6166
|
-
|
|
7194
|
+
${workspaceSection(setup, bot.workspacePath)}
|
|
7195
|
+
## Responding
|
|
7196
|
+
When you are @-mentioned or get a DM, first let the person know you have it: react to that message with an emoji, or confirm in a sentence or two with an emoji. A reaction is only a signal, not a command; pick the emoji yourself.
|
|
7197
|
+
When something will take a while, post a short progress note partway through, and give the outcome when you are done. You don't need to answer every message that isn't meant for you.
|
|
7198
|
+
`;
|
|
7199
|
+
}
|
|
7200
|
+
function workspaceSection(setup, workspacePath) {
|
|
7201
|
+
const place = setup.home();
|
|
7202
|
+
if (place.kind === "computer") return `## Workspace
|
|
7203
|
+
Your workspace is your own folder and your home directory: your file and command tools see it as ${place.cwd}, and ~ is the same place, so settings such as ~/.ssh and ~/.gitconfig stay with it. It is kept when your sandbox is reset. Keep it tidy, and put temporary files under temp and clear them out.
|
|
7204
|
+
|
|
7205
|
+
## Work environment
|
|
7206
|
+
Your tools run in your sandbox by default: a Linux container only you use, with Node, Python, git, build tools, and Playwright with its browsers. You can install software there with sudo; it stays until the owner resets your sandbox.
|
|
7207
|
+
${SWITCHING}
|
|
7208
|
+
`;
|
|
7209
|
+
return `## Workspace
|
|
7210
|
+
You have your own workspace: ${workspacePath}
|
|
6167
7211
|
Treat it as your only base. Create, change and delete files only inside it, keep it tidy, and put temporary files under temp and clear them out.
|
|
6168
7212
|
Leave everything else alone by default: other members' workspaces, live data directories (such as ~/.jarvy), main repository checkouts and system directories. Touch them only when the task itself needs it or the person in charge asks you to, and say in your reply what you changed.
|
|
6169
7213
|
Shared directories and other people's files are read-only. Ask the person in charge before writing to them.
|
|
6170
7214
|
|
|
6171
|
-
##
|
|
6172
|
-
|
|
6173
|
-
|
|
7215
|
+
## Work environment
|
|
7216
|
+
Your tools run on your owner's computer by default, the machine running Jarvy.
|
|
7217
|
+
${SWITCHING}
|
|
6174
7218
|
`;
|
|
6175
7219
|
}
|
|
7220
|
+
var SWITCHING = "Each batch of messages ends with the environment you are in now. environments.list shows the ones you may use, and environments.switch moves your tools to another, like cd, until you switch back.";
|
|
6176
7221
|
|
|
6177
7222
|
// src/server/shared/live.ts
|
|
6178
7223
|
var LIVE_STEPS = 300;
|
|
@@ -6186,9 +7231,9 @@ function clip(text3, length) {
|
|
|
6186
7231
|
return text3.length > length ? `${text3.slice(0, length - 1)}\u2026` : text3;
|
|
6187
7232
|
}
|
|
6188
7233
|
var field = (value2, length = LIVE_INPUT) => typeof value2 === "string" ? clip(value2, length) : void 0;
|
|
6189
|
-
var
|
|
7234
|
+
var record2 = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
|
|
6190
7235
|
function liveInput(tool, args, chat) {
|
|
6191
|
-
const values =
|
|
7236
|
+
const values = record2(args);
|
|
6192
7237
|
switch (tool) {
|
|
6193
7238
|
case "bash":
|
|
6194
7239
|
return { command: field(values.command) };
|
|
@@ -6212,15 +7257,15 @@ function liveInput(tool, args, chat) {
|
|
|
6212
7257
|
}
|
|
6213
7258
|
function gateway(values, chat) {
|
|
6214
7259
|
const action = values.action === "call" && typeof values.tool === "string" ? values.tool : typeof values.action === "string" ? values.action : "";
|
|
6215
|
-
const inner =
|
|
6216
|
-
if (action === "messages.send") return { action, chat:
|
|
7260
|
+
const inner = record2(values.args), where2 = target(inner, chat);
|
|
7261
|
+
if (action === "messages.send") return { action, chat: where2, text: field(inner.message, LIVE_MESSAGE) };
|
|
6217
7262
|
if (action === "messages.ask") return {
|
|
6218
7263
|
action,
|
|
6219
|
-
chat:
|
|
7264
|
+
chat: where2,
|
|
6220
7265
|
text: field(inner.question, LIVE_MESSAGE),
|
|
6221
|
-
options: Array.isArray(inner.options) ? inner.options.map((option2) => field(
|
|
7266
|
+
options: Array.isArray(inner.options) ? inner.options.map((option2) => field(record2(option2).label) ?? "").filter(Boolean) : void 0
|
|
6222
7267
|
};
|
|
6223
|
-
return
|
|
7268
|
+
return where2 ? { action, chat: where2 } : { action };
|
|
6224
7269
|
}
|
|
6225
7270
|
function target(inner, chat) {
|
|
6226
7271
|
if (typeof inner.to === "string") return { id: null, type: "dm", title: clip(inner.to, 80) };
|
|
@@ -6440,7 +7485,7 @@ var AgentOps = class {
|
|
|
6440
7485
|
agentId = AGENT_BOT_ID;
|
|
6441
7486
|
/** A Bot by name, ignoring case and a leading @. */
|
|
6442
7487
|
bot(name2) {
|
|
6443
|
-
const bot = this.store.listBots().find((
|
|
7488
|
+
const bot = this.store.listBots().find((record3) => same2(name2, record3.name));
|
|
6444
7489
|
if (!bot) throw new ChatError(404, `No Bot named ${name2} (see bots.list)`);
|
|
6445
7490
|
return bot;
|
|
6446
7491
|
}
|
|
@@ -6467,6 +7512,10 @@ var AgentOps = class {
|
|
|
6467
7512
|
if (!connectionId) throw new ChatError(400, "This Bot has no model yet; give model too");
|
|
6468
7513
|
return { connectionId, modelId, ...input.thinking === void 0 ? {} : { thinkingLevel: input.thinking } };
|
|
6469
7514
|
}
|
|
7515
|
+
/** Where a Bot works now: computer or host (A50). */
|
|
7516
|
+
environment(botId) {
|
|
7517
|
+
return this.bots.describe(botId).environment.home;
|
|
7518
|
+
}
|
|
6470
7519
|
/** "model (connection)", as the agent and the changed Bot read it. */
|
|
6471
7520
|
modelLabel(bot) {
|
|
6472
7521
|
const connection = bot.connectionId ? this.store.getModelConnection(bot.connectionId) : null;
|
|
@@ -6475,7 +7524,7 @@ var AgentOps = class {
|
|
|
6475
7524
|
}
|
|
6476
7525
|
async create(input) {
|
|
6477
7526
|
const selection = this.selection(null, input);
|
|
6478
|
-
const created = this.bots.create(input.name);
|
|
7527
|
+
const created = this.bots.create(input.name, input.environment);
|
|
6479
7528
|
if (input.description !== void 0 || input.persona !== void 0)
|
|
6480
7529
|
this.bots.get(created.id).identity.update({ name: created.name, description: input.description ?? "", persona: input.persona ?? "" });
|
|
6481
7530
|
if (selection) await this.bots.selectModel(created.id, selection);
|
|
@@ -6544,9 +7593,13 @@ var BotService = class {
|
|
|
6544
7593
|
constructor(deps) {
|
|
6545
7594
|
this.deps = deps;
|
|
6546
7595
|
this.agent = new AgentOps(this, deps.store, deps.chats);
|
|
7596
|
+
this.computers = new Computers({ store: deps.store, changed: () => this.listChanged() });
|
|
7597
|
+
this.environments = new BotEnvironments(deps.store, this.computers);
|
|
6547
7598
|
}
|
|
6548
7599
|
bots = /* @__PURE__ */ new Map();
|
|
6549
7600
|
lives = /* @__PURE__ */ new Map();
|
|
7601
|
+
computers;
|
|
7602
|
+
environments;
|
|
6550
7603
|
closed = false;
|
|
6551
7604
|
started = false;
|
|
6552
7605
|
/** What only the agent Bot does (A48). */
|
|
@@ -6556,14 +7609,18 @@ var BotService = class {
|
|
|
6556
7609
|
}
|
|
6557
7610
|
get(id) {
|
|
6558
7611
|
if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
|
|
6559
|
-
const
|
|
6560
|
-
if (!
|
|
7612
|
+
const record3 = this.deps.store.getBot(id);
|
|
7613
|
+
if (!record3) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6561
7614
|
let bot = this.bots.get(id);
|
|
6562
7615
|
if (bot) return bot;
|
|
6563
7616
|
const { store, chats, files, credentials } = this.deps, live = this.live(id);
|
|
6564
|
-
const
|
|
7617
|
+
const here = () => {
|
|
7618
|
+
const place = this.environments.here(id, record3.workspacePath);
|
|
7619
|
+
return { name: place.name, directory: place.directory };
|
|
7620
|
+
};
|
|
7621
|
+
const runtime = new AgentRuntime(new BotInbox(id, store.runtime, chats, files, here), (setup, model, reference, activity) => (this.deps.factory ?? PiSession.create)({
|
|
6565
7622
|
store,
|
|
6566
|
-
bot:
|
|
7623
|
+
bot: record3,
|
|
6567
7624
|
selection: model.selection,
|
|
6568
7625
|
reference,
|
|
6569
7626
|
credentials,
|
|
@@ -6571,10 +7628,14 @@ var BotService = class {
|
|
|
6571
7628
|
systemPrompt: () => botPrompt(setup),
|
|
6572
7629
|
onHistory: setup.onHistory,
|
|
6573
7630
|
activity,
|
|
6574
|
-
live: (event) => live.event(event)
|
|
7631
|
+
live: (event) => live.event(event),
|
|
7632
|
+
environment: () => this.environments.current(id, record3.workspacePath)
|
|
6575
7633
|
}));
|
|
6576
|
-
const workspace = new Workspace(
|
|
6577
|
-
|
|
7634
|
+
const workspace = new Workspace(record3.workspacePath, () => this.environments.current(id, record3.workspacePath), () => {
|
|
7635
|
+
const place = this.environments.home(id, record3.workspacePath);
|
|
7636
|
+
return { kind: place.kind === "sandbox" ? "computer" : "host", cwd: place.directory };
|
|
7637
|
+
});
|
|
7638
|
+
const memory = new Memory(join8(dirname7(record3.workspacePath), "history.db"));
|
|
6578
7639
|
bot = new Bot(id, {
|
|
6579
7640
|
identity: new Identity(id, store),
|
|
6580
7641
|
model: new Model(id, store, resolveThinking),
|
|
@@ -6588,12 +7649,17 @@ var BotService = class {
|
|
|
6588
7649
|
workspace,
|
|
6589
7650
|
asking: (on) => runtime.waitingForUser(on),
|
|
6590
7651
|
roster: () => this.roster(),
|
|
7652
|
+
places: {
|
|
7653
|
+
list: () => this.environments.list(id, record3.workspacePath),
|
|
7654
|
+
switch: (environmentId) => this.environments.switch(id, environmentId, record3.workspacePath),
|
|
7655
|
+
summary: (botId) => this.environments.summary(botId)
|
|
7656
|
+
},
|
|
6591
7657
|
...id === AGENT_BOT_ID ? { agent: this.agent } : {}
|
|
6592
7658
|
}),
|
|
6593
7659
|
agentRuntime: runtime,
|
|
6594
7660
|
state: new BotState(() => this.deps.changed(id))
|
|
6595
7661
|
}, { deliver: async (message) => {
|
|
6596
|
-
const outgoing = await
|
|
7662
|
+
const outgoing = await workspace.readOutgoing(message.attachments ?? [], message.signal);
|
|
6597
7663
|
message.signal?.throwIfAborted();
|
|
6598
7664
|
return chats.deliver(id, message.chatId, {
|
|
6599
7665
|
content: message.content,
|
|
@@ -6609,9 +7675,14 @@ var BotService = class {
|
|
|
6609
7675
|
this.bots.set(id, bot);
|
|
6610
7676
|
return bot;
|
|
6611
7677
|
}
|
|
6612
|
-
details(
|
|
6613
|
-
const model =
|
|
6614
|
-
return {
|
|
7678
|
+
details(record3) {
|
|
7679
|
+
const model = record3.connectionId ? this.deps.store.getModelConnection(record3.connectionId)?.models.find((model2) => model2.id === record3.modelId) : void 0;
|
|
7680
|
+
return {
|
|
7681
|
+
...record3,
|
|
7682
|
+
effectiveThinkingLevel: model ? resolveThinking(model, record3.thinkingLevel) : null,
|
|
7683
|
+
agent: record3.id === AGENT_BOT_ID,
|
|
7684
|
+
environment: this.environments.placement(record3.id)
|
|
7685
|
+
};
|
|
6615
7686
|
}
|
|
6616
7687
|
owns(id) {
|
|
6617
7688
|
return this.deps.store.getBot(id) !== null;
|
|
@@ -6634,15 +7705,58 @@ var BotService = class {
|
|
|
6634
7705
|
return { id: chat.id, type: chat.type, title: other ? this.deps.chats.member(other)?.name ?? chat.title : chat.title };
|
|
6635
7706
|
}
|
|
6636
7707
|
list() {
|
|
6637
|
-
return this.deps.store.listBots().map((
|
|
6638
|
-
}
|
|
6639
|
-
|
|
6640
|
-
|
|
7708
|
+
return this.deps.store.listBots().map((record3) => this.details(record3));
|
|
7709
|
+
}
|
|
7710
|
+
/** A new Bot works in its sandbox when this machine has Docker, unless asked for the owner's computer (A50). */
|
|
7711
|
+
create(name2, environment) {
|
|
7712
|
+
const kind = environment ?? (this.computers.available ? "computer" : "host");
|
|
7713
|
+
if (kind === "computer" && !this.computers.available) throw new RuntimeFault("docker_unavailable", "Docker is not available on this machine.", 409);
|
|
7714
|
+
const record3 = this.deps.store.createBot(name2);
|
|
7715
|
+
this.environments.setHome(record3.id, kind, record3.workspacePath);
|
|
7716
|
+
return this.details(record3);
|
|
7717
|
+
}
|
|
7718
|
+
/** The owner moves a Bot to its sandbox or to their computer, as its default; the next tool call runs there. */
|
|
7719
|
+
setHome(id, kind) {
|
|
7720
|
+
const moved = this.environments.setHome(id, kind, this.record(id).workspacePath);
|
|
7721
|
+
if (moved) this.tellOwner(id, [{ what: "environment", value: where(moved) }], true);
|
|
7722
|
+
return this.describe(id);
|
|
7723
|
+
}
|
|
7724
|
+
/** The owner lets a Bot use their computer, or takes it back; taking it back while the Bot is there sends it home. */
|
|
7725
|
+
grantHost(id, granted) {
|
|
7726
|
+
const { changed, moved } = this.environments.grantHost(id, granted, this.deps.store.userMemberId(), this.record(id).workspacePath);
|
|
7727
|
+
const changes = changed ? [{ what: "access", value: `your owner's computer: ${granted ? "you may switch to it" : "taken back"}` }] : [];
|
|
7728
|
+
if (moved) changes.push({ what: "environment", value: where(moved) });
|
|
7729
|
+
if (changes.length) this.tellOwner(id, changes, !!moved);
|
|
7730
|
+
return this.describe(id);
|
|
7731
|
+
}
|
|
7732
|
+
/** Tells a Bot what its owner changed about where it works (A41, A50). A move interrupts a running turn, as an
|
|
7733
|
+
* @-mention does, so the Bot doesn't go on in the old place; a Bot not running, or a change of access alone, is
|
|
7734
|
+
* told with its next batch (fish m1012–m1015). */
|
|
7735
|
+
tellOwner(id, changes, move) {
|
|
7736
|
+
if (!this.record(id).connectionId) return;
|
|
7737
|
+
const owner = this.deps.store.userMemberId(), chatId = this.deps.chats.openDirect(owner, id).id;
|
|
7738
|
+
const urgency = move && this.bots.get(id)?.status().status === "running" ? urgencyOf("change", "owner") : "digest";
|
|
7739
|
+
for (const change of changes) this.deps.store.agent.notify({ botId: id, kind: "change", reason: "owner", urgency, chatId, actorId: owner, detail: JSON.stringify(change) });
|
|
7740
|
+
if (urgency === "interrupt") this.deliver(id, urgency);
|
|
7741
|
+
}
|
|
7742
|
+
record(id) {
|
|
7743
|
+
const record3 = this.deps.store.getBot(id);
|
|
7744
|
+
if (!record3) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
7745
|
+
return record3;
|
|
7746
|
+
}
|
|
7747
|
+
/** Starting (or resetting, which starts afresh) returns at once: the Bot's state follows as it changes. */
|
|
7748
|
+
async computer(id, action) {
|
|
7749
|
+
this.describe(id);
|
|
7750
|
+
if (action === "stop") await this.computers.stop(id);
|
|
7751
|
+
else {
|
|
7752
|
+
if (action === "reset") await this.computers.reset(id);
|
|
7753
|
+
void this.computers.ensure(id).catch(() => {
|
|
7754
|
+
});
|
|
7755
|
+
}
|
|
7756
|
+
return this.describe(id);
|
|
6641
7757
|
}
|
|
6642
7758
|
describe(id) {
|
|
6643
|
-
|
|
6644
|
-
if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6645
|
-
return this.details(record2);
|
|
7759
|
+
return this.details(this.record(id));
|
|
6646
7760
|
}
|
|
6647
7761
|
runs(id) {
|
|
6648
7762
|
this.describe(id);
|
|
@@ -6669,9 +7783,9 @@ var BotService = class {
|
|
|
6669
7783
|
if (this.closed || !this.deps.store.getBot(id)) return;
|
|
6670
7784
|
const bot = this.get(id);
|
|
6671
7785
|
void bot.deliver(urgency).catch((error) => {
|
|
6672
|
-
const
|
|
6673
|
-
bot.state.update({ ...bot.status(), error: { code:
|
|
6674
|
-
console.error("[bot] delivery failed", { botId: id, code:
|
|
7786
|
+
const fault2 = publicFault(error);
|
|
7787
|
+
bot.state.update({ ...bot.status(), error: { code: fault2.code, message: fault2.message, ...fault2.params ? { params: fault2.params } : {} } });
|
|
7788
|
+
console.error("[bot] delivery failed", { botId: id, code: fault2.code });
|
|
6675
7789
|
});
|
|
6676
7790
|
}
|
|
6677
7791
|
/** Picks up what is still waiting for a Bot (after a failure, a new model, or a restart), if anything. */
|
|
@@ -6685,21 +7799,22 @@ var BotService = class {
|
|
|
6685
7799
|
start() {
|
|
6686
7800
|
if (this.started) return;
|
|
6687
7801
|
this.started = true;
|
|
7802
|
+
void this.computers.start();
|
|
6688
7803
|
for (const { botId, urgency } of this.deps.store.runtime.waitingBots(this.deps.store.userMemberId())) this.deliver(botId, urgency);
|
|
6689
7804
|
}
|
|
6690
7805
|
/** Every Bot's run state, for `bots.list` (A43). A Bot not loaded in this process is stopped. */
|
|
6691
7806
|
roster() {
|
|
6692
7807
|
const { store } = this.deps;
|
|
6693
|
-
return store.listBots().map((
|
|
6694
|
-
const bot = this.bots.get(
|
|
7808
|
+
return store.listBots().map((record3) => {
|
|
7809
|
+
const bot = this.bots.get(record3.id), runId = bot?.agentRuntime.activeRunId ?? null;
|
|
6695
7810
|
const status = bot?.status() ?? { status: "stop", activity: null, context: null, error: null };
|
|
6696
7811
|
return {
|
|
6697
|
-
id:
|
|
6698
|
-
name:
|
|
6699
|
-
agent:
|
|
7812
|
+
id: record3.id,
|
|
7813
|
+
name: record3.name,
|
|
7814
|
+
agent: record3.id === AGENT_BOT_ID,
|
|
6700
7815
|
status,
|
|
6701
7816
|
runChatIds: status.status === "running" && runId ? store.runtime.inputs(runId).map((input) => input.chatId) : [],
|
|
6702
|
-
errorCode: store.runtime.latestErrorCode(
|
|
7817
|
+
errorCode: store.runtime.latestErrorCode(record3.id)
|
|
6703
7818
|
};
|
|
6704
7819
|
});
|
|
6705
7820
|
}
|
|
@@ -6737,16 +7852,19 @@ var BotService = class {
|
|
|
6737
7852
|
this.bots.delete(id);
|
|
6738
7853
|
this.lives.get(id)?.close();
|
|
6739
7854
|
this.lives.delete(id);
|
|
7855
|
+
await this.environments.release(id);
|
|
6740
7856
|
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6741
|
-
if (!keepFiles) await
|
|
7857
|
+
if (!keepFiles) await rm3(dirname7(workspacePath), { recursive: true, force: true });
|
|
6742
7858
|
}
|
|
6743
7859
|
async close() {
|
|
6744
7860
|
if (this.closed) return;
|
|
6745
7861
|
this.closed = true;
|
|
6746
7862
|
await Promise.all([...this.bots.values()].map((bot) => bot.stop()));
|
|
6747
7863
|
for (const live of this.lives.values()) live.close();
|
|
7864
|
+
await this.environments.close();
|
|
6748
7865
|
}
|
|
6749
7866
|
};
|
|
7867
|
+
var where = (moved) => `${moved.to.name}, in ${moved.to.directory}`;
|
|
6750
7868
|
|
|
6751
7869
|
// src/server/application.ts
|
|
6752
7870
|
var Application = class _Application {
|
|
@@ -6768,7 +7886,7 @@ var Application = class _Application {
|
|
|
6768
7886
|
(id) => this.events.publish({ kind: "chat", id }),
|
|
6769
7887
|
() => this.events.publish({ kind: "inbox", memberId: this.user.id })
|
|
6770
7888
|
);
|
|
6771
|
-
this.files = new ChatFiles(
|
|
7889
|
+
this.files = new ChatFiles(this.chats);
|
|
6772
7890
|
store.runtime.recover();
|
|
6773
7891
|
this.bots = new BotService({
|
|
6774
7892
|
store,
|
|
@@ -6780,8 +7898,8 @@ var Application = class _Application {
|
|
|
6780
7898
|
factory
|
|
6781
7899
|
});
|
|
6782
7900
|
this.auth = new CredentialLogins(store, this.credentials, (id) => this.bots.connectionChanged(id));
|
|
6783
|
-
this.account = new AccountService(store,
|
|
6784
|
-
this.models = new ModelService(store, this.credentials, this.auth, (id) => this.bots.connectionChanged(id));
|
|
7901
|
+
this.account = new AccountService(store, createHash6("sha256").update(dataDir).digest("hex").slice(0, 10));
|
|
7902
|
+
this.models = new ModelService(store, this.credentials, this.auth, (id) => this.bots.connectionChanged(id), new ModelCatalog(dataDir));
|
|
6785
7903
|
}
|
|
6786
7904
|
events = new Changes();
|
|
6787
7905
|
credentials = new CredentialCoordinator();
|
|
@@ -6794,8 +7912,8 @@ var Application = class _Application {
|
|
|
6794
7912
|
models;
|
|
6795
7913
|
closing;
|
|
6796
7914
|
static async open(dataDir, options = {}) {
|
|
6797
|
-
await
|
|
6798
|
-
const root = await
|
|
7915
|
+
await mkdir5(resolve3(dataDir), { recursive: true, mode: 448 });
|
|
7916
|
+
const root = await realpath(resolve3(dataDir));
|
|
6799
7917
|
let app;
|
|
6800
7918
|
let release;
|
|
6801
7919
|
try {
|
|
@@ -6803,7 +7921,7 @@ var Application = class _Application {
|
|
|
6803
7921
|
retries: 0,
|
|
6804
7922
|
stale: 3e4,
|
|
6805
7923
|
update: 1e4,
|
|
6806
|
-
lockfilePath:
|
|
7924
|
+
lockfilePath: join9(root, ".runtime.lock"),
|
|
6807
7925
|
onCompromised: () => {
|
|
6808
7926
|
console.error("[runtime] data lock compromised");
|
|
6809
7927
|
options.onCompromised?.();
|
|
@@ -6826,6 +7944,7 @@ var Application = class _Application {
|
|
|
6826
7944
|
}
|
|
6827
7945
|
start() {
|
|
6828
7946
|
this.bots.start();
|
|
7947
|
+
this.models.refreshCatalog();
|
|
6829
7948
|
}
|
|
6830
7949
|
close() {
|
|
6831
7950
|
if (!this.closing) this.closing = (async () => {
|
|
@@ -6841,14 +7960,14 @@ var Application = class _Application {
|
|
|
6841
7960
|
};
|
|
6842
7961
|
|
|
6843
7962
|
// src/server/daemon.ts
|
|
6844
|
-
import { spawn } from "node:child_process";
|
|
7963
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
6845
7964
|
import { closeSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
6846
|
-
import { join as
|
|
6847
|
-
var pidPath = (dir) =>
|
|
6848
|
-
var logPath = (dir) =>
|
|
7965
|
+
import { join as join10, resolve as resolve4 } from "node:path";
|
|
7966
|
+
var pidPath = (dir) => join10(resolve4(dir), "jarvy.pid");
|
|
7967
|
+
var logPath = (dir) => join10(resolve4(dir), "jarvy.log");
|
|
6849
7968
|
function writePidFile(dir, url) {
|
|
6850
|
-
const
|
|
6851
|
-
writeFileSync(pidPath(dir), JSON.stringify(
|
|
7969
|
+
const record3 = { pid: process.pid, startToken: startToken(process.pid), url, startedAt: Date.now() };
|
|
7970
|
+
writeFileSync(pidPath(dir), JSON.stringify(record3), { mode: 384 });
|
|
6852
7971
|
}
|
|
6853
7972
|
function removePidFile(dir) {
|
|
6854
7973
|
if (readRecord(dir)?.pid === process.pid) rmSync(pidPath(dir), { force: true });
|
|
@@ -6876,19 +7995,19 @@ function alive(pid) {
|
|
|
6876
7995
|
}
|
|
6877
7996
|
}
|
|
6878
7997
|
function runningServer(dir) {
|
|
6879
|
-
const
|
|
6880
|
-
if (!
|
|
6881
|
-
const token = alive(
|
|
6882
|
-
if (token === null || token &&
|
|
7998
|
+
const record3 = readRecord(dir);
|
|
7999
|
+
if (!record3) return null;
|
|
8000
|
+
const token = alive(record3.pid) ? startToken(record3.pid) : null;
|
|
8001
|
+
if (token === null || token && record3.startToken && token !== record3.startToken) {
|
|
6883
8002
|
rmSync(pidPath(dir), { force: true });
|
|
6884
8003
|
return null;
|
|
6885
8004
|
}
|
|
6886
|
-
return
|
|
8005
|
+
return record3;
|
|
6887
8006
|
}
|
|
6888
8007
|
function startBackground(script, runArgs, dir) {
|
|
6889
|
-
mkdirSync3(
|
|
8008
|
+
mkdirSync3(resolve4(dir), { recursive: true, mode: 448 });
|
|
6890
8009
|
const log = openSync(logPath(dir), "a", 384);
|
|
6891
|
-
const child =
|
|
8010
|
+
const child = spawn2(process.execPath, [...process.execArgv, script, "run", ...runArgs], { detached: true, stdio: ["ignore", log, log, "ipc"] });
|
|
6892
8011
|
closeSync(log);
|
|
6893
8012
|
return new Promise((resolvePromise, reject) => {
|
|
6894
8013
|
child.once("error", reject);
|
|
@@ -6916,11 +8035,11 @@ function signalReady(url) {
|
|
|
6916
8035
|
process.disconnect();
|
|
6917
8036
|
}
|
|
6918
8037
|
async function stopServer(dir, waitMs = 3e4) {
|
|
6919
|
-
const
|
|
6920
|
-
if (!
|
|
6921
|
-
process.kill(
|
|
8038
|
+
const record3 = runningServer(dir);
|
|
8039
|
+
if (!record3) return "not_running";
|
|
8040
|
+
process.kill(record3.pid, "SIGTERM");
|
|
6922
8041
|
for (const deadline = Date.now() + waitMs; Date.now() < deadline; ) {
|
|
6923
|
-
if (!alive(
|
|
8042
|
+
if (!alive(record3.pid)) {
|
|
6924
8043
|
rmSync(pidPath(dir), { force: true });
|
|
6925
8044
|
return "stopped";
|
|
6926
8045
|
}
|
|
@@ -6939,7 +8058,7 @@ function option(args, flag, fallback) {
|
|
|
6939
8058
|
function secretPrompt(label) {
|
|
6940
8059
|
if (!process.stdin.isTTY || !process.stdin.setRawMode) throw new Error("Account setup requires an interactive terminal");
|
|
6941
8060
|
process.stdout.write(label);
|
|
6942
|
-
return new Promise((
|
|
8061
|
+
return new Promise((resolve5, reject) => {
|
|
6943
8062
|
let value2 = "";
|
|
6944
8063
|
const done = (result, error) => {
|
|
6945
8064
|
process.stdin.off("data", onData);
|
|
@@ -6947,7 +8066,7 @@ function secretPrompt(label) {
|
|
|
6947
8066
|
process.stdin.pause();
|
|
6948
8067
|
process.stdout.write("\n");
|
|
6949
8068
|
if (error) reject(error);
|
|
6950
|
-
else
|
|
8069
|
+
else resolve5(result ?? "");
|
|
6951
8070
|
};
|
|
6952
8071
|
const onData = (buffer) => {
|
|
6953
8072
|
for (const char of buffer.toString("utf8")) {
|
|
@@ -7064,18 +8183,18 @@ async function main() {
|
|
|
7064
8183
|
try {
|
|
7065
8184
|
if (!store.isInitialized()) await initialize(store);
|
|
7066
8185
|
const { host, port, url } = listenOptions(args);
|
|
7067
|
-
const webRoot =
|
|
7068
|
-
if (!existsSync2(
|
|
8186
|
+
const webRoot = join11(dirname8(fileURLToPath(import.meta.url)), "../web");
|
|
8187
|
+
if (!existsSync2(join11(webRoot, "index.html"))) throw new Error("Web build missing. Run npm run build first");
|
|
7069
8188
|
const server = createJarvyServer(app, webRoot);
|
|
7070
8189
|
closeHttp = () => {
|
|
7071
8190
|
server.close();
|
|
7072
8191
|
server.closeAllConnections();
|
|
7073
8192
|
};
|
|
7074
|
-
await new Promise((
|
|
8193
|
+
await new Promise((resolve5, reject) => {
|
|
7075
8194
|
server.once("error", reject);
|
|
7076
8195
|
server.listen(port, host, () => {
|
|
7077
8196
|
server.off("error", reject);
|
|
7078
|
-
|
|
8197
|
+
resolve5();
|
|
7079
8198
|
});
|
|
7080
8199
|
});
|
|
7081
8200
|
app.start();
|