openmausbot 0.1.90 → 0.1.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +128 -15
- package/dist-server/container-mcp.js +277 -45
- package/dist-server/drivers/agents-proxy.js +16 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +46703 -39955
- package/dist-server/local-computer-proxy.js +128 -7
- package/dist-server/mcp-gate.js +11 -5
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4099 -1217
- package/dist-server/pair-cli.js +4099 -1217
- package/dist-server/server/boat.js +67 -22
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +136 -26
- package/dist-server/server/cloud-home.js +64 -5
- package/dist-server/server/cloud-lending.js +167 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/config.js +215 -17
- package/dist-server/server/container-computer.js +3 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/drivers/acp/core.js +167 -21
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/agents-catalog.js +22 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +313 -34
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +41 -3
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +15 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1753 -263
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/provider-key-check.js +4 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +25 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routes/desktop-viewer.js +194 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +79 -124
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +20 -11
- package/dist-server/server/vps-computer.js +79 -12
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +25 -3
- package/dist-server/server/workspace-backup.js +28 -8
- package/dist-server/server/workspace.js +65 -15
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +278 -45
- package/dist-server/workspace-backup.worker.js +184 -38
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
|
@@ -26,6 +26,12 @@ const EXCLUDED = new Set([
|
|
|
26
26
|
".openmausbot-server-child", "environment-id", "sessions.json", "tunnel-account.json",
|
|
27
27
|
"team-computers.json",
|
|
28
28
|
"openmausbot-server.lease", "box-create-requests.lock", "messages.db-wal", "messages.db-shm",
|
|
29
|
+
// This machine's decision-model log (server/decider/log.ts): local
|
|
30
|
+
// measurement of what the classifier picked, not workspace data.
|
|
31
|
+
"decider-log",
|
|
32
|
+
// A Cloud home's record of who its owner was (server/cloud-owner.ts): this
|
|
33
|
+
// machine's own, kept in place by a restore, which settles what it brought.
|
|
34
|
+
"cloud-owner.json",
|
|
29
35
|
]);
|
|
30
36
|
const EXCLUSION_NOTES = [
|
|
31
37
|
"Device pairing, server identity, live leases and runtime files (existing destination identities are preserved).",
|
|
@@ -49,6 +55,12 @@ function preserved(name) {
|
|
|
49
55
|
const folded = name.toLowerCase();
|
|
50
56
|
return excluded(folded) && folded !== "messages.db-wal" && folded !== "messages.db-shm";
|
|
51
57
|
}
|
|
58
|
+
/** Left where it is by a restore. The session registry's open marker is this
|
|
59
|
+
* machine's own (never exported or installed): a crash before the restore
|
|
60
|
+
* still costs account sign-ins, as it would have without one. */
|
|
61
|
+
function keptInPlace(name) {
|
|
62
|
+
return preserved(name) || name.toLowerCase() === "sessions.json.open";
|
|
63
|
+
}
|
|
52
64
|
function folder(path) {
|
|
53
65
|
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
54
66
|
const stat = lstatSync(path);
|
|
@@ -544,22 +556,24 @@ async function decryptArchive(inputPath, plaintext, password) {
|
|
|
544
556
|
}
|
|
545
557
|
async function inspectTar(path) {
|
|
546
558
|
const fd = openSync(path, "r");
|
|
547
|
-
const signature = Buffer.alloc(
|
|
559
|
+
const signature = Buffer.alloc(4);
|
|
548
560
|
try {
|
|
549
|
-
readSync(fd, signature, 0,
|
|
561
|
+
readSync(fd, signature, 0, 4, 0);
|
|
550
562
|
}
|
|
551
563
|
finally {
|
|
552
564
|
closeSync(fd);
|
|
553
565
|
}
|
|
554
|
-
// This format is uncompressed tar. Reject auto-detected gzip
|
|
555
|
-
// parser can inflate an authenticated but malicious
|
|
556
|
-
|
|
566
|
+
// This format is uncompressed tar. Reject auto-detected gzip and zstd
|
|
567
|
+
// before tar's parser can inflate an authenticated but malicious
|
|
568
|
+
// decompression bomb; tar is also told never to decompress (below).
|
|
569
|
+
if ((signature[0] === 0x1f && signature[1] === 0x8b) || signature.equals(Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))) {
|
|
557
570
|
throw new Error("Compressed payloads are not supported in this workspace backup version.");
|
|
571
|
+
}
|
|
558
572
|
const entries = new Map();
|
|
559
573
|
const names = new Set();
|
|
560
574
|
let bytes = 0;
|
|
561
575
|
let problem = "";
|
|
562
|
-
await tar.t({ file: path, strict: true, onReadEntry(entry) {
|
|
576
|
+
await tar.t({ file: path, strict: true, brotli: false, zstd: false, onReadEntry(entry) {
|
|
563
577
|
const name = entry.type === "Directory" ? entry.path.replace(/\/$/, "") : entry.path;
|
|
564
578
|
const expectedRoot = name === "manifest.json" || name === "data" || name.startsWith("data/");
|
|
565
579
|
// ReadEntry normalizes backslashes on Windows; inspect the raw header too
|
|
@@ -651,7 +665,7 @@ export async function stageWorkspaceBackup(dataDir, archivePath, options) {
|
|
|
651
665
|
const expected = await inspectTar(plaintext);
|
|
652
666
|
const staged = join(job.directory, "staged");
|
|
653
667
|
folder(staged);
|
|
654
|
-
await tar.x({ file: plaintext, cwd: staged, strict: true, preservePaths: false, umask: 0o077, noChmod: true });
|
|
668
|
+
await tar.x({ file: plaintext, cwd: staged, strict: true, preservePaths: false, umask: 0o077, noChmod: true, brotli: false, zstd: false });
|
|
655
669
|
const manifest = validateStaged(staged, expected);
|
|
656
670
|
const versions = [manifest.summary.appVersion, options.currentAppVersion ?? ""].map((version) => /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version)?.slice(1).map(Number));
|
|
657
671
|
if (versions[0] && versions[1]) {
|
|
@@ -1037,7 +1051,7 @@ export function applyPendingWorkspaceRestore(dataDir) {
|
|
|
1037
1051
|
folder(join(safetyCopyPath, "data"));
|
|
1038
1052
|
const journal = {
|
|
1039
1053
|
id: pending.id, phase: "applying",
|
|
1040
|
-
existing: readdirSync(dataDir).filter((name) => !
|
|
1054
|
+
existing: readdirSync(dataDir).filter((name) => !keptInPlace(name)).sort(),
|
|
1041
1055
|
incoming: readdirSync(prepared).sort(),
|
|
1042
1056
|
};
|
|
1043
1057
|
// Top-level source and destination names are checked before recording any
|
|
@@ -1087,3 +1101,9 @@ export function removeWorkspaceBackupJob(dataDir, id) {
|
|
|
1087
1101
|
throw new Error("Unsafe workspace backup cleanup target.");
|
|
1088
1102
|
rmSync(path, { recursive: true, force: true });
|
|
1089
1103
|
}
|
|
1104
|
+
/** Whether a snapshot leaves out this workspace-relative path, by the same
|
|
1105
|
+
* rules as the export walk (cloud-move.ts sizes a move with it). Symbolic
|
|
1106
|
+
* links are the caller's to skip. */
|
|
1107
|
+
export function omittedFromWorkspaceBackup(path) {
|
|
1108
|
+
return excluded(path.split("/")[0]) || excludedWorkspaceAuthPath(path) || ephemeralWorkspaceTokenPath(path) || redownloadedOrgLibraryPath(path);
|
|
1109
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// memory/ holds topic files the bot reads on demand with its ordinary
|
|
9
9
|
// file tools. Plain markdown on purpose — the user can open, edit, or
|
|
10
10
|
// delete anything the bot believes.
|
|
11
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { writeFileAtomic } from "./atomic.js";
|
|
14
14
|
import { isMemoryDate, untilMark, withoutExpired } from "./memory-entries.js";
|
|
@@ -58,6 +58,53 @@ export function ensureWorkspace(botId) {
|
|
|
58
58
|
export function workspaceDir(botId) {
|
|
59
59
|
return join(WORKSPACES_DIR, botId);
|
|
60
60
|
}
|
|
61
|
+
/** On an OMB Cloud home a bot's memory is read only from regular files,
|
|
62
|
+
* never through a link: server/lending-memory.ts judges a link by where it
|
|
63
|
+
* points, not by what is there, so a turn must never read through one.
|
|
64
|
+
* Elsewhere memory reads exactly as it always did. */
|
|
65
|
+
let regularMemoryFilesOnly = false;
|
|
66
|
+
export function readMemoryOnlyFromRegularFiles() {
|
|
67
|
+
regularMemoryFilesOnly = true;
|
|
68
|
+
}
|
|
69
|
+
/** A memory file's text. Throws like readFileSync when it cannot be read,
|
|
70
|
+
* and, on a Cloud home, when it is not a regular file. */
|
|
71
|
+
export function readMemoryText(path) {
|
|
72
|
+
if (!regularMemoryFilesOnly)
|
|
73
|
+
return readFileSync(path, "utf8");
|
|
74
|
+
// O_NOFOLLOW refuses a link at open where there is one; Windows has none
|
|
75
|
+
// (and opens what a link points at), so the entry is looked at first too.
|
|
76
|
+
if (!lstatSync(path).isFile())
|
|
77
|
+
throw Object.assign(new Error(`not a regular file: ${path}`), { code: "EINVAL" });
|
|
78
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
79
|
+
try {
|
|
80
|
+
if (!fstatSync(fd).isFile())
|
|
81
|
+
throw Object.assign(new Error(`not a regular file: ${path}`), { code: "EINVAL" });
|
|
82
|
+
return readFileSync(fd, "utf8");
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
closeSync(fd);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** A memory folder's entries; on a Cloud home, none when the folder itself
|
|
89
|
+
* is a link. */
|
|
90
|
+
function memoryFolderEntries(dir) {
|
|
91
|
+
if (regularMemoryFilesOnly && !lstatSync(dir).isDirectory())
|
|
92
|
+
return [];
|
|
93
|
+
return readdirSync(dir);
|
|
94
|
+
}
|
|
95
|
+
function regularFile(path) {
|
|
96
|
+
try {
|
|
97
|
+
return lstatSync(path).isFile();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The stat a listing trusts: on a Cloud home, the entry's own (a link is
|
|
104
|
+
* not a file); elsewhere what it points at, as before. */
|
|
105
|
+
function memoryEntryStat(path) {
|
|
106
|
+
return regularMemoryFilesOnly ? lstatSync(path) : statSync(path);
|
|
107
|
+
}
|
|
61
108
|
/** File locations, not file contents or wider tool permissions. Threads keep
|
|
62
109
|
* independent working directories; the same bot can find its earlier output
|
|
63
110
|
* without assuming that a file absent from the current directory was lost. */
|
|
@@ -89,7 +136,7 @@ export function memoryOverBudget(text) {
|
|
|
89
136
|
export function loadMemory(botId, opts = {}) {
|
|
90
137
|
let raw;
|
|
91
138
|
try {
|
|
92
|
-
raw =
|
|
139
|
+
raw = readMemoryText(join(workspaceDir(botId), "MEMORY.md"));
|
|
93
140
|
}
|
|
94
141
|
catch {
|
|
95
142
|
return null;
|
|
@@ -128,7 +175,7 @@ export const MEMORY_FILE_MAX_BYTES = 256 * 1024;
|
|
|
128
175
|
export function readMemoryFile(botId) {
|
|
129
176
|
let raw;
|
|
130
177
|
try {
|
|
131
|
-
raw =
|
|
178
|
+
raw = readMemoryText(join(workspaceDir(botId), "MEMORY.md"));
|
|
132
179
|
}
|
|
133
180
|
catch {
|
|
134
181
|
return { text: "", truncated: false };
|
|
@@ -159,7 +206,7 @@ function indexWrittenMemoryFile(botId, relativePath) {
|
|
|
159
206
|
try {
|
|
160
207
|
const path = join(workspaceDir(botId), relativePath);
|
|
161
208
|
const stat = statSync(path);
|
|
162
|
-
indexMemoryFile(botId, relativePath, searchableMemoryText(relativePath,
|
|
209
|
+
indexMemoryFile(botId, relativePath, searchableMemoryText(relativePath, readMemoryText(path)), { mtimeMs: stat.mtimeMs, bytes: stat.size });
|
|
163
210
|
}
|
|
164
211
|
catch {
|
|
165
212
|
// the next search's sync pass picks it up
|
|
@@ -176,7 +223,7 @@ function memoryFilesOnDisk(botId) {
|
|
|
176
223
|
];
|
|
177
224
|
return names.flatMap((relativePath) => {
|
|
178
225
|
try {
|
|
179
|
-
const stat =
|
|
226
|
+
const stat = memoryEntryStat(join(dir, relativePath));
|
|
180
227
|
return stat.isFile() ? [{ path: relativePath, mtimeMs: stat.mtimeMs, bytes: stat.size }] : [];
|
|
181
228
|
}
|
|
182
229
|
catch {
|
|
@@ -206,7 +253,7 @@ export function syncMemoryIndex(botId) {
|
|
|
206
253
|
if (!dateChanged && known && known.bytes === file.bytes && known.mtimeMs === Math.trunc(file.mtimeMs))
|
|
207
254
|
continue;
|
|
208
255
|
try {
|
|
209
|
-
const text =
|
|
256
|
+
const text = readMemoryText(join(workspaceDir(botId), file.path));
|
|
210
257
|
indexMemoryFile(botId, file.path, searchableMemoryText(file.path, text), file);
|
|
211
258
|
}
|
|
212
259
|
catch {
|
|
@@ -318,7 +365,7 @@ export function updateMemory(botId, update, opts = {}) {
|
|
|
318
365
|
const dir = ensureWorkspace(botId);
|
|
319
366
|
// Do not use readMemoryFile's editor-friendly missing/read-error fallback:
|
|
320
367
|
// a failed read must never turn into a successful overwrite of old notes.
|
|
321
|
-
const raw =
|
|
368
|
+
const raw = readMemoryText(join(dir, "MEMORY.md"));
|
|
322
369
|
const current = raw === MEMORY_SEED ? "" : raw;
|
|
323
370
|
const today = memoryDate(opts.now);
|
|
324
371
|
// Every appended entry ends its own line; a file the person left without
|
|
@@ -419,7 +466,7 @@ export function appendMemoryLog(botId, text, opts = {}) {
|
|
|
419
466
|
const path = join(dir, file);
|
|
420
467
|
let current = "";
|
|
421
468
|
try {
|
|
422
|
-
current =
|
|
469
|
+
current = readMemoryText(path);
|
|
423
470
|
}
|
|
424
471
|
catch {
|
|
425
472
|
// first line of the day
|
|
@@ -442,7 +489,8 @@ export function writeMemoryLog(botId, name, text) {
|
|
|
442
489
|
/** The bot's daily log files, oldest first, by day name. */
|
|
443
490
|
export function listMemoryLogs(botId) {
|
|
444
491
|
try {
|
|
445
|
-
|
|
492
|
+
const dir = join(workspaceDir(botId), "memory", MEMORY_LOG_DIR);
|
|
493
|
+
return memoryFolderEntries(dir).filter((name) => LOG_FILE_NAME.test(name) && (!regularMemoryFilesOnly || regularFile(join(dir, name)))).sort();
|
|
446
494
|
}
|
|
447
495
|
catch {
|
|
448
496
|
return [];
|
|
@@ -453,7 +501,7 @@ export function readMemoryLog(botId, name) {
|
|
|
453
501
|
if (!LOG_FILE_NAME.test(name))
|
|
454
502
|
return null;
|
|
455
503
|
try {
|
|
456
|
-
return
|
|
504
|
+
return readMemoryText(join(workspaceDir(botId), "memory", MEMORY_LOG_DIR, name));
|
|
457
505
|
}
|
|
458
506
|
catch {
|
|
459
507
|
return null;
|
|
@@ -472,7 +520,7 @@ export function isMemoryTopicName(name) {
|
|
|
472
520
|
export function listMemoryTopics(botId) {
|
|
473
521
|
let entries;
|
|
474
522
|
try {
|
|
475
|
-
entries =
|
|
523
|
+
entries = memoryFolderEntries(join(workspaceDir(botId), "memory"));
|
|
476
524
|
}
|
|
477
525
|
catch {
|
|
478
526
|
return [];
|
|
@@ -481,7 +529,7 @@ export function listMemoryTopics(botId) {
|
|
|
481
529
|
.filter(isMemoryTopicName)
|
|
482
530
|
.flatMap((name) => {
|
|
483
531
|
try {
|
|
484
|
-
const stat =
|
|
532
|
+
const stat = memoryEntryStat(join(workspaceDir(botId), "memory", name));
|
|
485
533
|
return stat.isFile() ? [{ name, bytes: stat.size }] : [];
|
|
486
534
|
}
|
|
487
535
|
catch {
|
|
@@ -507,7 +555,7 @@ export function readMemoryTopic(botId, name) {
|
|
|
507
555
|
if (!isMemoryTopicName(name))
|
|
508
556
|
return null;
|
|
509
557
|
try {
|
|
510
|
-
return
|
|
558
|
+
return readMemoryText(join(workspaceDir(botId), "memory", name));
|
|
511
559
|
}
|
|
512
560
|
catch {
|
|
513
561
|
return null;
|
|
@@ -541,12 +589,14 @@ export function memoryTopicIndex(botId) {
|
|
|
541
589
|
const dir = join(workspaceDir(botId), "memory");
|
|
542
590
|
return renderTopicIndex(listMemoryTopics(botId).map((topic) => ({ name: topic.name, header: parseTopicHeader(readTopicHead(join(dir, topic.name))) })));
|
|
543
591
|
}
|
|
544
|
-
/** The memory block appended to a bot's system prompt.
|
|
545
|
-
* bots with a workspace, so the bot knows the mechanism exists even before
|
|
592
|
+
/** The memory block appended to a bot's system prompt when enabled. Present for
|
|
593
|
+
* enabled bots with a workspace, so the bot knows the mechanism exists even before
|
|
546
594
|
* it has written anything. Content from other bots or imported files must
|
|
547
595
|
* never be recorded as fact — memory is a prompt-injection persistence
|
|
548
596
|
* vector the moment a bot copies untrusted text into it. */
|
|
549
597
|
export function memorySystemPrompt(botId, opts = {}) {
|
|
598
|
+
if (opts.enabled === false)
|
|
599
|
+
return "";
|
|
550
600
|
const memory = loadMemory(botId);
|
|
551
601
|
const memoryFile = join(workspaceDir(botId), "MEMORY.md");
|
|
552
602
|
const topicDir = join(workspaceDir(botId), "memory");
|
|
@@ -161,7 +161,24 @@ const requirementsSchema = z.object({
|
|
|
161
161
|
capabilities: z.array(requiredText(80)).max(20),
|
|
162
162
|
platforms: z.array(requiredText(80)).max(10).optional(),
|
|
163
163
|
});
|
|
164
|
-
const
|
|
164
|
+
const ROOM_RESPONDER_KINDS = new Set(["agent", "everyone", "mentions", "auto"]);
|
|
165
|
+
/** A responder kind from a newer release degrades to the room's first member
|
|
166
|
+
* as lead, which is what a room without a responder does, instead of
|
|
167
|
+
* refusing the whole package. Malformed values still fail below. */
|
|
168
|
+
function knownRoomResponder(value) {
|
|
169
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
170
|
+
return value;
|
|
171
|
+
const room = value;
|
|
172
|
+
const responder = room.defaultResponder;
|
|
173
|
+
if (!responder || typeof responder !== "object" || Array.isArray(responder))
|
|
174
|
+
return value;
|
|
175
|
+
const kind = responder.kind;
|
|
176
|
+
if (typeof kind !== "string" || ROOM_RESPONDER_KINDS.has(kind))
|
|
177
|
+
return value;
|
|
178
|
+
const first = Array.isArray(room.members) && typeof room.members[0] === "string" ? room.members[0] : undefined;
|
|
179
|
+
return { ...room, defaultResponder: first ? { kind: "agent", agent: first } : { kind: "mentions" } };
|
|
180
|
+
}
|
|
181
|
+
const roomSchema = z.preprocess(knownRoomResponder, z.object({
|
|
165
182
|
key,
|
|
166
183
|
name: requiredText(100),
|
|
167
184
|
members: z.array(key).min(1).max(200),
|
|
@@ -170,8 +187,11 @@ const roomSchema = z.object({
|
|
|
170
187
|
z.object({ kind: z.literal("agent"), agent: key }),
|
|
171
188
|
z.object({ kind: z.literal("everyone") }),
|
|
172
189
|
z.object({ kind: z.literal("mentions") }),
|
|
190
|
+
// Read, not written: exports spell an Auto room as its lead (see
|
|
191
|
+
// server/package-export.ts) so every older app can still open them.
|
|
192
|
+
z.object({ kind: z.literal("auto"), agent: key.optional() }),
|
|
173
193
|
]),
|
|
174
|
-
});
|
|
194
|
+
}));
|
|
175
195
|
const exampleSchema = z.object({
|
|
176
196
|
title: requiredText(120),
|
|
177
197
|
input: requiredText(4_000),
|
|
@@ -580,7 +600,8 @@ function checkV1References(pkg) {
|
|
|
580
600
|
if (!agents.has(member))
|
|
581
601
|
throw invalid(`Room ${room.key} references unknown agent: ${member}`);
|
|
582
602
|
}
|
|
583
|
-
if (room.defaultResponder.kind === "agent" &&
|
|
603
|
+
if ((room.defaultResponder.kind === "agent" || room.defaultResponder.kind === "auto") && room.defaultResponder.agent !== undefined &&
|
|
604
|
+
!members.has(room.defaultResponder.agent)) {
|
|
584
605
|
throw invalid(`Room ${room.key} has an unknown default responder`);
|
|
585
606
|
}
|
|
586
607
|
}
|
|
@@ -637,7 +658,8 @@ function checkV2References(pkg) {
|
|
|
637
658
|
if (!agents.has(member))
|
|
638
659
|
throw invalid(`Room ${room.key} references unknown agent: ${member}`);
|
|
639
660
|
}
|
|
640
|
-
if (room.defaultResponder.kind === "agent" &&
|
|
661
|
+
if ((room.defaultResponder.kind === "agent" || room.defaultResponder.kind === "auto") && room.defaultResponder.agent !== undefined &&
|
|
662
|
+
!members.has(room.defaultResponder.agent)) {
|
|
641
663
|
throw invalid(`Room ${room.key} has a default responder who is not a member: ${room.defaultResponder.agent}`);
|
|
642
664
|
}
|
|
643
665
|
roomMembers.set(room.key, members);
|
|
@@ -821,8 +843,12 @@ export function downgradeToV1(document) {
|
|
|
821
843
|
return [{ ...kept, enabledAfterInstall: false }];
|
|
822
844
|
});
|
|
823
845
|
const { team, publisher: _publisher, notes: _notes, presets: _presets, connections: _connections, skills: _skills, agents: _agents, routines: _routines, ...rest } = pkg;
|
|
846
|
+
// Older apps know no Auto room: spell one as its lead, as exports do.
|
|
847
|
+
const rooms = rest.rooms?.map((room) => room.defaultResponder.kind !== "auto" ? room
|
|
848
|
+
: { ...room, defaultResponder: { kind: "agent", agent: room.defaultResponder.agent ?? room.members[0] } });
|
|
824
849
|
const v1 = {
|
|
825
850
|
...rest,
|
|
851
|
+
...(rooms ? { rooms } : {}),
|
|
826
852
|
agents,
|
|
827
853
|
...(team.leader ? { chiefOfStaff: team.leader } : {}),
|
|
828
854
|
...(routines.length ? { routines } : {}),
|
|
@@ -63,6 +63,20 @@ const memory = z.object({
|
|
|
63
63
|
* against. Shareable exports never carry grants at all, and an import
|
|
64
64
|
* always lands bots grant-less whatever the file says. */
|
|
65
65
|
const connectorTools = z.record(z.string().regex(CONNECTOR_SLUG_PATTERN), z.object({ tools: z.union([z.literal("*"), z.array(z.string().regex(CONNECTOR_TOOL_NAME_PATTERN)).min(1).max(500)]) }));
|
|
66
|
+
/** A responder kind from a newer release restores as the room's first
|
|
67
|
+
* member as lead (what a room without one does) instead of refusing the
|
|
68
|
+
* whole backup. Malformed values still fail the schema. */
|
|
69
|
+
function knownRoomResponder(value) {
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
71
|
+
return value;
|
|
72
|
+
const group = value;
|
|
73
|
+
const kind = group.defaultResponder && typeof group.defaultResponder === "object"
|
|
74
|
+
? group.defaultResponder.kind : undefined;
|
|
75
|
+
if (typeof kind !== "string" || ["member", "everyone", "mentions", "auto"].includes(kind))
|
|
76
|
+
return value;
|
|
77
|
+
const first = Array.isArray(group.memberIds) && typeof group.memberIds[0] === "string" ? group.memberIds[0] : undefined;
|
|
78
|
+
return { ...group, defaultResponder: first ? { kind: "member", botId: first } : { kind: "mentions" } };
|
|
79
|
+
}
|
|
66
80
|
const backupSchema = z.object({
|
|
67
81
|
format: z.literal("openmaus.backup"),
|
|
68
82
|
version: z.literal(1),
|
|
@@ -86,7 +100,7 @@ const backupSchema = z.object({
|
|
|
86
100
|
connectorTools: connectorTools.optional(),
|
|
87
101
|
memory: memory.optional(),
|
|
88
102
|
})).min(1).max(200),
|
|
89
|
-
groups: z.array(z.object({
|
|
103
|
+
groups: z.array(z.preprocess(knownRoomResponder, z.object({
|
|
90
104
|
...owner,
|
|
91
105
|
memberIds: z.array(key).max(200),
|
|
92
106
|
dm: z.boolean(),
|
|
@@ -95,8 +109,9 @@ const backupSchema = z.object({
|
|
|
95
109
|
z.object({ kind: z.literal("member"), botId: key }),
|
|
96
110
|
z.object({ kind: z.literal("everyone") }),
|
|
97
111
|
z.object({ kind: z.literal("mentions") }),
|
|
112
|
+
z.object({ kind: z.literal("auto"), fallbackBotId: key.optional() }),
|
|
98
113
|
]),
|
|
99
|
-
})).max(2_000),
|
|
114
|
+
}))).max(2_000),
|
|
100
115
|
routines: z.array(z.object({
|
|
101
116
|
name, prompt: z.string().trim().min(1).max(200_000),
|
|
102
117
|
target: z.enum(["bot", "room-goal"]), botId: key, groupId: key.optional(),
|
|
@@ -136,7 +151,9 @@ export function parseTeamBackup(input) {
|
|
|
136
151
|
if (group.memberIds.some((id) => !bots.has(id)))
|
|
137
152
|
throw new Error("Invalid backup: unknown room member");
|
|
138
153
|
unique(group.memberIds, "room member");
|
|
139
|
-
|
|
154
|
+
const lead = group.defaultResponder.kind === "member" ? group.defaultResponder.botId
|
|
155
|
+
: group.defaultResponder.kind === "auto" ? group.defaultResponder.fallbackBotId : undefined;
|
|
156
|
+
if (lead !== undefined && !group.memberIds.includes(lead)) {
|
|
140
157
|
throw new Error("Invalid backup: room responder is not a member");
|
|
141
158
|
}
|
|
142
159
|
if (group.dm && (group.memberIds.length !== 2 || group.tasks.length !== 1))
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Selection matching is adapted from T3 Code's assistantTextSelection.ts
|
|
2
|
+
// (Copyright 2026 T3 Tools Inc., MIT License; see third_party/t3-code/LICENSE).
|
|
3
|
+
export const CITATION_MAX_QUOTE_LENGTH = 12_000;
|
|
4
|
+
export const CITATION_MAX_COMMENT_LENGTH = 4_000;
|
|
5
|
+
const CITATION_CONTEXT_LENGTH = 64;
|
|
6
|
+
const CITATION_MARKER = "<!--omb-citation-v1:";
|
|
7
|
+
const CITATION_MARKER_RE = /<!--omb-citation-v1:([A-Za-z0-9_-]{1,100000})-->/g;
|
|
8
|
+
function boundedString(value, max, allowEmpty = false) {
|
|
9
|
+
return typeof value === "string" && value.length <= max && (allowEmpty || value.length > 0);
|
|
10
|
+
}
|
|
11
|
+
export function isCitationAttachment(value) {
|
|
12
|
+
if (!value || typeof value !== "object")
|
|
13
|
+
return false;
|
|
14
|
+
const citation = value;
|
|
15
|
+
const source = citation.source;
|
|
16
|
+
return citation.kind === "citation" &&
|
|
17
|
+
citation.version === 1 &&
|
|
18
|
+
boundedString(citation.id, 200) &&
|
|
19
|
+
boundedString(citation.quote, CITATION_MAX_QUOTE_LENGTH) &&
|
|
20
|
+
(citation.comment === undefined || boundedString(citation.comment, CITATION_MAX_COMMENT_LENGTH, true)) &&
|
|
21
|
+
typeof citation.size === "number" && Number.isFinite(citation.size) && citation.size >= 0 &&
|
|
22
|
+
Boolean(source) &&
|
|
23
|
+
(source.ownerType === "bot" || source.ownerType === "group") &&
|
|
24
|
+
boundedString(source.ownerId, 200) &&
|
|
25
|
+
boundedString(source.threadId, 200) &&
|
|
26
|
+
boundedString(source.messageId, 200) &&
|
|
27
|
+
Number.isSafeInteger(source.start) && Number(source.start) >= 0 &&
|
|
28
|
+
Number.isSafeInteger(source.end) && Number(source.end) >= Number(source.start) &&
|
|
29
|
+
boundedString(source.prefix, CITATION_CONTEXT_LENGTH, true) &&
|
|
30
|
+
boundedString(source.suffix, CITATION_CONTEXT_LENGTH, true);
|
|
31
|
+
}
|
|
32
|
+
function newId() {
|
|
33
|
+
return globalThis.crypto?.randomUUID?.() ?? `c${Math.random().toString(36).slice(2)}`;
|
|
34
|
+
}
|
|
35
|
+
function byteLength(text) {
|
|
36
|
+
return new TextEncoder().encode(text).length;
|
|
37
|
+
}
|
|
38
|
+
export function citationAttachment(source, selector, comment = "") {
|
|
39
|
+
const trimmedComment = comment.trim();
|
|
40
|
+
if (!selector.text.trim() || selector.text.length > CITATION_MAX_QUOTE_LENGTH) {
|
|
41
|
+
throw new Error("Citation quote is empty or too long");
|
|
42
|
+
}
|
|
43
|
+
if (trimmedComment.length > CITATION_MAX_COMMENT_LENGTH)
|
|
44
|
+
throw new Error("Citation comment is too long");
|
|
45
|
+
return {
|
|
46
|
+
kind: "citation",
|
|
47
|
+
version: 1,
|
|
48
|
+
id: newId(),
|
|
49
|
+
quote: selector.text,
|
|
50
|
+
...(trimmedComment ? { comment: trimmedComment } : {}),
|
|
51
|
+
source: { ...source, start: selector.start, end: selector.end, prefix: selector.prefix, suffix: selector.suffix },
|
|
52
|
+
size: byteLength(selector.text) + byteLength(trimmedComment),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function withCitationComment(citation, comment) {
|
|
56
|
+
const trimmed = comment.trim();
|
|
57
|
+
if (trimmed.length > CITATION_MAX_COMMENT_LENGTH)
|
|
58
|
+
throw new Error("Citation comment is too long");
|
|
59
|
+
const { comment: _old, ...rest } = citation;
|
|
60
|
+
return { ...rest, ...(trimmed ? { comment: trimmed } : {}), size: byteLength(citation.quote) + byteLength(trimmed) };
|
|
61
|
+
}
|
|
62
|
+
function base64UrlEncode(value) {
|
|
63
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
64
|
+
let binary = "";
|
|
65
|
+
for (const byte of bytes)
|
|
66
|
+
binary += String.fromCharCode(byte);
|
|
67
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
68
|
+
}
|
|
69
|
+
function base64UrlDecode(value) {
|
|
70
|
+
const padded = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
71
|
+
const binary = atob(padded);
|
|
72
|
+
return JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0))));
|
|
73
|
+
}
|
|
74
|
+
/** Readable in clients without citation UI and unambiguous to model providers. */
|
|
75
|
+
export function citationFallback(citation) {
|
|
76
|
+
const quote = citation.quote.split("\n").map((line) => `> ${line}`).join("\n");
|
|
77
|
+
return `> Quoted message:\n${quote}${citation.comment ? `\n\nComment:\n${citation.comment}` : ""}`;
|
|
78
|
+
}
|
|
79
|
+
export function serializeCitation(citation) {
|
|
80
|
+
return `${CITATION_MARKER}${base64UrlEncode(citation)}-->\n${citationFallback(citation)}`;
|
|
81
|
+
}
|
|
82
|
+
export function splitTranscriptCitations(text) {
|
|
83
|
+
const citations = [];
|
|
84
|
+
let display = "";
|
|
85
|
+
let cursor = 0;
|
|
86
|
+
for (const match of text.matchAll(CITATION_MARKER_RE)) {
|
|
87
|
+
if (match.index < cursor)
|
|
88
|
+
continue;
|
|
89
|
+
let citation;
|
|
90
|
+
try {
|
|
91
|
+
const decoded = base64UrlDecode(match[1]);
|
|
92
|
+
if (!isCitationAttachment(decoded))
|
|
93
|
+
continue;
|
|
94
|
+
citation = decoded;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const block = `${match[0]}\n${citationFallback(citation)}`;
|
|
100
|
+
if (!text.startsWith(block, match.index))
|
|
101
|
+
continue;
|
|
102
|
+
display += text.slice(cursor, match.index);
|
|
103
|
+
cursor = match.index + block.length;
|
|
104
|
+
citations.push(citation);
|
|
105
|
+
}
|
|
106
|
+
return { display: `${display}${text.slice(cursor)}`.trim(), citations };
|
|
107
|
+
}
|
|
108
|
+
export function citationPreviewText(text) {
|
|
109
|
+
const cited = splitTranscriptCitations(text);
|
|
110
|
+
if (!cited.citations.length)
|
|
111
|
+
return text;
|
|
112
|
+
return [
|
|
113
|
+
cited.display,
|
|
114
|
+
...cited.citations.map((citation) => citation.comment
|
|
115
|
+
? `${citation.quote} — ${citation.comment}`
|
|
116
|
+
: citation.quote),
|
|
117
|
+
].filter(Boolean).join(" ");
|
|
118
|
+
}
|
|
119
|
+
function normalizeWhitespace(text) {
|
|
120
|
+
return text.replace(/\s+/g, " ");
|
|
121
|
+
}
|
|
122
|
+
function splitsSurrogatePair(text, offset) {
|
|
123
|
+
const before = text.charCodeAt(offset - 1);
|
|
124
|
+
const after = text.charCodeAt(offset);
|
|
125
|
+
return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
|
|
126
|
+
}
|
|
127
|
+
export function createCitationTextSelector(text, rawStart, rawEnd) {
|
|
128
|
+
const quote = text.slice(rawStart, rawEnd);
|
|
129
|
+
if (!quote.trim())
|
|
130
|
+
return null;
|
|
131
|
+
const normalized = normalizeWhitespace(text);
|
|
132
|
+
let start = normalizeWhitespace(text.slice(0, rawStart)).length;
|
|
133
|
+
if (rawStart > 0 && /\s/.test(text[rawStart - 1]) && /\s/.test(text[rawStart]))
|
|
134
|
+
start -= 1;
|
|
135
|
+
const end = normalizeWhitespace(text.slice(0, rawEnd)).length;
|
|
136
|
+
let prefixStart = Math.max(0, start - CITATION_CONTEXT_LENGTH);
|
|
137
|
+
let suffixEnd = Math.min(normalized.length, end + CITATION_CONTEXT_LENGTH);
|
|
138
|
+
if (splitsSurrogatePair(normalized, prefixStart))
|
|
139
|
+
prefixStart += 1;
|
|
140
|
+
if (splitsSurrogatePair(normalized, suffixEnd))
|
|
141
|
+
suffixEnd -= 1;
|
|
142
|
+
return {
|
|
143
|
+
text: quote,
|
|
144
|
+
start,
|
|
145
|
+
end,
|
|
146
|
+
prefix: normalized.slice(prefixStart, start),
|
|
147
|
+
suffix: normalized.slice(end, suffixEnd),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function findCitationText(text, selector) {
|
|
151
|
+
const normalized = normalizeWhitespace(text);
|
|
152
|
+
const quote = normalizeWhitespace(selector.text);
|
|
153
|
+
if (!quote.trim())
|
|
154
|
+
return null;
|
|
155
|
+
const prefix = normalizeWhitespace(selector.prefix);
|
|
156
|
+
const suffix = normalizeWhitespace(selector.suffix);
|
|
157
|
+
const matchesContext = (start, end) => normalized.slice(Math.max(0, start - prefix.length), start) === prefix &&
|
|
158
|
+
normalized.slice(end, end + suffix.length) === suffix;
|
|
159
|
+
let match = Number.isSafeInteger(selector.start) && Number.isSafeInteger(selector.end) &&
|
|
160
|
+
selector.start >= 0 && selector.end - selector.start === quote.length &&
|
|
161
|
+
normalized.slice(selector.start, selector.end) === quote && matchesContext(selector.start, selector.end)
|
|
162
|
+
? { start: selector.start, end: selector.end }
|
|
163
|
+
: null;
|
|
164
|
+
let onlyQuote = null;
|
|
165
|
+
let quoteCount = 0;
|
|
166
|
+
for (let start = normalized.indexOf(quote); start !== -1; start = normalized.indexOf(quote, start + 1)) {
|
|
167
|
+
const end = start + quote.length;
|
|
168
|
+
quoteCount += 1;
|
|
169
|
+
onlyQuote = { start, end };
|
|
170
|
+
if (!matchesContext(start, end))
|
|
171
|
+
continue;
|
|
172
|
+
if (match && match.start !== start)
|
|
173
|
+
return null;
|
|
174
|
+
match = { start, end };
|
|
175
|
+
}
|
|
176
|
+
return match ?? (quoteCount === 1 ? onlyQuote : null);
|
|
177
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { isCitationAttachment, serializeCitation } from "./citations.js";
|
|
1
2
|
export function isAttachment(value) {
|
|
2
3
|
if (!value || typeof value !== "object")
|
|
3
4
|
return false;
|
|
4
5
|
const attachment = value;
|
|
6
|
+
if (attachment.kind === "citation")
|
|
7
|
+
return isCitationAttachment(value);
|
|
5
8
|
if (typeof attachment.id !== "string" || !validSize(attachment.size))
|
|
6
9
|
return false;
|
|
7
10
|
if (attachment.kind === "paste") {
|
|
@@ -362,7 +365,10 @@ export function formatSize(bytes) {
|
|
|
362
365
|
export function composeMessage(text, attachments) {
|
|
363
366
|
const parts = [text.trim()];
|
|
364
367
|
attachments.forEach((a, i) => {
|
|
365
|
-
if (a.kind === "
|
|
368
|
+
if (a.kind === "citation") {
|
|
369
|
+
parts.push(serializeCitation(a));
|
|
370
|
+
}
|
|
371
|
+
else if (a.kind === "paste") {
|
|
366
372
|
parts.push(`<pasted-text index="${i + 1}">\n${a.text}\n</pasted-text>`);
|
|
367
373
|
}
|
|
368
374
|
else if (a.kind === "image") {
|