viberoom 0.5.7 → 0.5.9
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/README.md +19 -1
- package/dist/context.js +6 -0
- package/dist/hub.js +4 -2
- package/dist/main.js +3 -2
- package/dist/mcp-skills-server.js +119 -0
- package/dist/persona.js +98 -32
- package/dist/room-design.js +205 -0
- package/dist/room.js +253 -118
- package/dist/server.js +54 -1
- package/dist/shortcuts.js +10 -10
- package/dist/skills.js +28 -2
- package/dist/templates.js +17 -0
- package/dist/update.js +3 -3
- package/package.json +1 -1
- package/scripts/install.mjs +6 -1
- package/scripts/update.mjs +6 -1
- package/templates/maken-and-elaine/template.json +30 -0
- package/ui/app.css +13 -1
- package/ui/app.js +164 -92
- package/ui/avatars.js +125 -26
- package/ui/icons.js +2 -0
- package/ui/theme.css +12 -2
- package/templates/wren-and-quinn/template.json +0 -30
package/dist/room.js
CHANGED
|
@@ -3,7 +3,7 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.js";
|
|
6
|
-
import { NOTES_ONLY_PROMPT, NOTES_REQUEST, crossedThreshold, extractNotes, isBareContextFullError, isContextFullError, overThreshold, visibleChunk } from "./context.js";
|
|
6
|
+
import { NOTES_ONLY_PROMPT, NOTES_REQUEST, crossedThreshold, emptyUsageReport, extractNotes, isBareContextFullError, isContextFullError, looksCompacted, overThreshold, visibleChunk } from "./context.js";
|
|
7
7
|
import { affectedByEdit, editNotice, partitionHistory, rewriteNotice } from "./edit.js";
|
|
8
8
|
import { saveImages } from "./files.js";
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
@@ -12,11 +12,12 @@ import { RemoteError } from "./jsonrpc.js";
|
|
|
12
12
|
import { getRecipe, listRecipes } from "./recipes.js";
|
|
13
13
|
import { composeSkillBlock, skillPull, SKILL_TOOL_NAME } from "./persona.js";
|
|
14
14
|
import { BUILTIN_AUTHOR, parseSkillInvocation, renderSkillBody, SKILL_NAME_PATTERN, } from "./skills.js";
|
|
15
|
-
import {
|
|
15
|
+
import { templateId } from "./templates.js";
|
|
16
|
+
import { applyVibemateChanges, diffSettings, lintRoomDesign, ruleLines } from "./room-design.js";
|
|
17
|
+
import { BRIEF_AFFECTING_SETTINGS, AGENT_SETTINGS, coerceSetting, describeSettings, DEFAULT_ROOM_SETTINGS, ROOM_SETTINGS_SPEC, REQUEST_BRIEF_MARKER, NAME_PATTERN, SILENT_MARKER, buildBrief, buildHeader, composeCorrectionPrompt, composePrompt, countSentences, ensureDir, } from "./persona.js";
|
|
16
18
|
import { Transcript } from "./log.js";
|
|
17
19
|
const SKILL_TOOL_READY_MS = 5000;
|
|
18
20
|
const COLORS = ["#6d5dfc", "#16a34a", "#d97706", "#dc2626", "#0891b2", "#be185d", "#4d7c0f", "#7c3aed"];
|
|
19
|
-
const NAME_PATTERN = /^[\p{L}\p{N}][\p{L}\p{N}_-]{0,23}$/u;
|
|
20
21
|
const MENTION_PATTERN = /@([\p{L}\p{N}][\p{L}\p{N}_-]*)/gu;
|
|
21
22
|
const RULE_REF_TOKEN = /@\{p:([^}]+)\}/g;
|
|
22
23
|
const ADAPTER_ERROR_PATTERN = /^(?:Warning: Falling back from WebSockets|unexpected status \d{3}|Error when talking to|API Error|You have exhausted your (?:daily )?quota|Rate limit|429 |5\d\d )/i;
|
|
@@ -45,6 +46,8 @@ export class Room extends EventEmitter {
|
|
|
45
46
|
runtimes = new Map();
|
|
46
47
|
drafts = new Map();
|
|
47
48
|
permissions = new Map();
|
|
49
|
+
proposals = new Map();
|
|
50
|
+
proposalPlans = new Map();
|
|
48
51
|
optionCache;
|
|
49
52
|
log;
|
|
50
53
|
colorIndex = 0;
|
|
@@ -231,6 +234,7 @@ export class Room extends EventEmitter {
|
|
|
231
234
|
participants: [...this.participants.values()],
|
|
232
235
|
messages: [...this.messages, ...this.drafts.values()],
|
|
233
236
|
permissions: [...this.permissions.values()].map(({ resolve: _r, ...p }) => p),
|
|
237
|
+
proposals: [...this.proposals.values()],
|
|
234
238
|
recipes: listRecipes().map(({ build: _b, ...r }) => r),
|
|
235
239
|
lastMessageAt: last?.ts ?? this.createdAt,
|
|
236
240
|
};
|
|
@@ -612,124 +616,20 @@ export class Room extends EventEmitter {
|
|
|
612
616
|
updateSettings(patch) {
|
|
613
617
|
const next = { ...this.settings };
|
|
614
618
|
const changed = [];
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
619
|
+
let unknownRefs = [];
|
|
620
|
+
for (const key of Object.keys(ROOM_SETTINGS_SPEC)) {
|
|
621
|
+
if (patch[key] === undefined || ROOM_SETTINGS_SPEC[key].kind === "own-path")
|
|
622
|
+
continue;
|
|
623
|
+
let value = coerceSetting(key, patch[key]);
|
|
624
|
+
if (key === "customRules") {
|
|
625
|
+
const resolved = this.resolveRuleReferences(value);
|
|
626
|
+
unknownRefs = resolved.unknown;
|
|
627
|
+
value = resolved.stored;
|
|
624
628
|
}
|
|
625
|
-
|
|
626
|
-
setNumber("hopLimit", 0, 10_000);
|
|
627
|
-
setNumber("fullBriefEveryTurns", 1, 10_000);
|
|
628
|
-
setNumber("fullBriefEveryTokens", 1000, 10_000_000);
|
|
629
|
-
setNumber("replayAfterRestart", 0, 200);
|
|
630
|
-
setNumber("backlogCap", 1, 1000);
|
|
631
|
-
if (patch.replyDelay !== undefined) {
|
|
632
|
-
const value = Number(patch.replyDelay);
|
|
633
|
-
if (!Number.isFinite(value) || value < 0 || value > 120)
|
|
634
|
-
throw new Error("replyDelay must be between 0 and 120 seconds");
|
|
635
|
-
next.replyDelay = value;
|
|
636
|
-
}
|
|
637
|
-
const setText = (key, max) => {
|
|
638
|
-
if (patch[key] === undefined)
|
|
639
|
-
return;
|
|
640
|
-
const value = String(patch[key]).slice(0, max);
|
|
641
|
-
if (value !== next[key]) {
|
|
629
|
+
if (JSON.stringify(value) !== JSON.stringify(next[key])) {
|
|
642
630
|
next[key] = value;
|
|
643
631
|
changed.push(key);
|
|
644
632
|
}
|
|
645
|
-
};
|
|
646
|
-
setText("topic", 2000);
|
|
647
|
-
setText("emoji", 8);
|
|
648
|
-
setText("humanDescription", 200);
|
|
649
|
-
let unknownRefs = [];
|
|
650
|
-
if (patch.customRules !== undefined) {
|
|
651
|
-
const resolved = this.resolveRuleReferences(String(patch.customRules).slice(0, 4000));
|
|
652
|
-
unknownRefs = resolved.unknown;
|
|
653
|
-
if (resolved.stored !== next.customRules) {
|
|
654
|
-
next.customRules = resolved.stored;
|
|
655
|
-
changed.push("customRules");
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (patch.humanDescriptionMode !== undefined) {
|
|
659
|
-
const mode = String(patch.humanDescriptionMode);
|
|
660
|
-
if (mode !== "inherit" && mode !== "override" && mode !== "append" && mode !== "none")
|
|
661
|
-
throw new Error("humanDescriptionMode must be inherit, override, append or none");
|
|
662
|
-
if (mode !== next.humanDescriptionMode) {
|
|
663
|
-
next.humanDescriptionMode = mode;
|
|
664
|
-
changed.push("humanDescriptionMode");
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
if (patch.refereeAction !== undefined) {
|
|
668
|
-
const action = String(patch.refereeAction);
|
|
669
|
-
if (action !== "next-header" && action !== "retry-hidden")
|
|
670
|
-
throw new Error("refereeAction must be next-header or retry-hidden");
|
|
671
|
-
if (action !== next.refereeAction) {
|
|
672
|
-
next.refereeAction = action;
|
|
673
|
-
changed.push("refereeAction");
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (patch.turnTaking !== undefined) {
|
|
677
|
-
const mode = String(patch.turnTaking);
|
|
678
|
-
if (mode !== "parallel" && mode !== "one-at-a-time")
|
|
679
|
-
throw new Error("turnTaking must be parallel or one-at-a-time");
|
|
680
|
-
if (mode !== next.turnTaking) {
|
|
681
|
-
next.turnTaking = mode;
|
|
682
|
-
changed.push("turnTaking");
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (patch.agentsWakeEachOther !== undefined) {
|
|
686
|
-
const on = patch.agentsWakeEachOther === true || patch.agentsWakeEachOther === "true";
|
|
687
|
-
if (on !== next.agentsWakeEachOther) {
|
|
688
|
-
next.agentsWakeEachOther = on;
|
|
689
|
-
changed.push("agentsWakeEachOther");
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
if (patch.waitWhileHumanTypes !== undefined) {
|
|
693
|
-
const on = patch.waitWhileHumanTypes === true || patch.waitWhileHumanTypes === "true";
|
|
694
|
-
if (on !== next.waitWhileHumanTypes) {
|
|
695
|
-
next.waitWhileHumanTypes = on;
|
|
696
|
-
changed.push("waitWhileHumanTypes");
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
if (patch.language !== undefined) {
|
|
700
|
-
const raw = String(patch.language).trim();
|
|
701
|
-
const language = !raw || raw === "follow-human" ? { mode: "follow-human" } : { mode: "fixed", language: raw };
|
|
702
|
-
if (JSON.stringify(language) !== JSON.stringify(next.language)) {
|
|
703
|
-
next.language = language;
|
|
704
|
-
changed.push("language");
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (patch.tools !== undefined) {
|
|
708
|
-
const tools = String(patch.tools);
|
|
709
|
-
if (tools !== "on-request" && tools !== "never")
|
|
710
|
-
throw new Error("tools must be on-request or never");
|
|
711
|
-
if (tools !== next.tools) {
|
|
712
|
-
next.tools = tools;
|
|
713
|
-
changed.push("tools");
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (patch.maxSentences !== undefined) {
|
|
717
|
-
const value = patch.maxSentences === null || patch.maxSentences === "" ? null : Number(patch.maxSentences);
|
|
718
|
-
if (value !== null && (!Number.isInteger(value) || value < 1 || value > 100))
|
|
719
|
-
throw new Error("maxSentences must be 1-100 or empty");
|
|
720
|
-
if (value !== next.maxSentences) {
|
|
721
|
-
next.maxSentences = value;
|
|
722
|
-
changed.push("maxSentences");
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
for (const key of ["headerRules", "showVendorInRoster"]) {
|
|
726
|
-
if (patch[key] !== undefined) {
|
|
727
|
-
const value = patch[key] === true || patch[key] === "true";
|
|
728
|
-
if (value !== next[key]) {
|
|
729
|
-
next[key] = value;
|
|
730
|
-
changed.push(key);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
633
|
}
|
|
734
634
|
this.settings = next;
|
|
735
635
|
this.push(this.roomEvent());
|
|
@@ -1294,6 +1194,237 @@ export class Room extends EventEmitter {
|
|
|
1294
1194
|
return undefined;
|
|
1295
1195
|
return { items, channel, canCreate: channel === "tool" };
|
|
1296
1196
|
}
|
|
1197
|
+
skillsForDesign(participantId) {
|
|
1198
|
+
if (!this.skills)
|
|
1199
|
+
return undefined;
|
|
1200
|
+
const runtime = this.runtimes.get(participantId);
|
|
1201
|
+
const channel = runtime?.skillChannel === "tool" ? "tool" : "marker";
|
|
1202
|
+
return {
|
|
1203
|
+
library: this.skills.library.list().filter((s) => !s.problems.length && !s.draft && s.agentInvocable).map((s) => ({ name: s.name, description: s.description })),
|
|
1204
|
+
channel,
|
|
1205
|
+
canCreate: channel === "tool",
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
agentInRoom(participantId) {
|
|
1209
|
+
const participant = this.participants.get(participantId);
|
|
1210
|
+
if (!participant || !this.runtimes.has(participantId))
|
|
1211
|
+
throw new Error("this agent is not in the room any more");
|
|
1212
|
+
return participant;
|
|
1213
|
+
}
|
|
1214
|
+
describeRoomForAgent(participantId) {
|
|
1215
|
+
const participant = this.agentInRoom(participantId);
|
|
1216
|
+
const shape = this.templateOf();
|
|
1217
|
+
const skills = this.skills ? this.skills.library.list().filter((s) => !s.problems.length && !s.draft).map((s) => ({ name: s.name, description: s.description })) : [];
|
|
1218
|
+
const templates = this.skills ? this.skills.templates.list().map((t) => ({ id: t.id, name: t.name, builtin: !!t.builtin })) : [];
|
|
1219
|
+
return {
|
|
1220
|
+
room: { name: this.settings.name, topic: this.settings.topic, emoji: this.settings.emoji, dir: this.dir },
|
|
1221
|
+
human: this.settings.humanName,
|
|
1222
|
+
you: participant.name,
|
|
1223
|
+
settings: describeSettings({ ...this.settings, customRules: this.renderRuleReferences(this.settings.customRules) }),
|
|
1224
|
+
rules: ruleLines(this.renderRuleReferences(this.settings.customRules)),
|
|
1225
|
+
vibemates: shape.vibemates.map((v) => {
|
|
1226
|
+
const role = v.role ?? "";
|
|
1227
|
+
const own = v.name === participant.name;
|
|
1228
|
+
return {
|
|
1229
|
+
name: v.name,
|
|
1230
|
+
tagline: v.tagline ?? "",
|
|
1231
|
+
...(own ? { role } : { rolePrivate: true, roleLength: role.length }),
|
|
1232
|
+
avatar: v.avatar ?? "",
|
|
1233
|
+
skills: v.skills ?? [],
|
|
1234
|
+
agentType: v.agentType,
|
|
1235
|
+
};
|
|
1236
|
+
}),
|
|
1237
|
+
skills,
|
|
1238
|
+
templates,
|
|
1239
|
+
yourBrief: buildBrief(this.settings, this.personaOf(participant), this.roster(), undefined, this.skillsForPrompt(participant, this.runtimes.get(participant.id))),
|
|
1240
|
+
howTo: "Settings are proposed by key with the values above; rules are one per line in customRules; a vibemate is { name, tagline, role, avatar, skills }. Another vibemate's role is private: you learn only that it has one and how long it is, and you may still propose a new one, which the human reads in full on the card. Check a design with lint_room_design, then create_template (a file for the human to pick) or propose_room_changes (a card the human applies).",
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
lintDesignForAgent(participantId, kind, design) {
|
|
1244
|
+
this.agentInRoom(participantId);
|
|
1245
|
+
const result = lintRoomDesign(design, { ...this.designContext(kind), skills: this.skillsForDesign(participantId) });
|
|
1246
|
+
return { ok: !result.errors.length, errors: result.errors.map((e) => e.message), warnings: result.warnings.map((w) => w.message), preview: result.preview };
|
|
1247
|
+
}
|
|
1248
|
+
designContext(kind) {
|
|
1249
|
+
return {
|
|
1250
|
+
kind,
|
|
1251
|
+
humanName: this.settings.humanName,
|
|
1252
|
+
roomName: this.settings.name,
|
|
1253
|
+
base: kind === "room" ? { ...this.settings, customRules: this.renderRuleReferences(this.settings.customRules) } : undefined,
|
|
1254
|
+
knownSkills: this.skills ? this.skills.library.list().map((s) => s.name) : undefined,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
createTemplateForAgent(participantId, design, replace) {
|
|
1258
|
+
const participant = this.agentInRoom(participantId);
|
|
1259
|
+
if (!this.skills)
|
|
1260
|
+
throw new Error("templates are not available in this hub");
|
|
1261
|
+
const result = lintRoomDesign(design, this.designContext("template"));
|
|
1262
|
+
if (result.errors.length)
|
|
1263
|
+
throw new Error(`not saved: ${result.errors.map((e) => e.message).join("; ")}`);
|
|
1264
|
+
const settings = result.settings;
|
|
1265
|
+
const rest = {};
|
|
1266
|
+
for (const key of Object.keys(design.settings ?? {}))
|
|
1267
|
+
if (AGENT_SETTINGS.includes(key))
|
|
1268
|
+
rest[key] = settings[key];
|
|
1269
|
+
const draft = {
|
|
1270
|
+
name: String(design.name).trim(),
|
|
1271
|
+
description: String(design.description ?? "").trim(),
|
|
1272
|
+
emoji: settings.emoji || undefined,
|
|
1273
|
+
settings: rest,
|
|
1274
|
+
vibemates: (design.vibemates ?? []).map((v) => {
|
|
1275
|
+
const out = { name: v.name.trim() };
|
|
1276
|
+
if (v.tagline?.trim())
|
|
1277
|
+
out.tagline = v.tagline.trim();
|
|
1278
|
+
if (v.role?.trim())
|
|
1279
|
+
out.role = v.role.trim();
|
|
1280
|
+
if (v.avatar?.trim())
|
|
1281
|
+
out.avatar = v.avatar.trim();
|
|
1282
|
+
if (v.skills?.length)
|
|
1283
|
+
out.skills = v.skills.map((s) => s.trim()).filter(Boolean);
|
|
1284
|
+
if (typeof v.replyDelay === "number")
|
|
1285
|
+
out.replyDelay = v.replyDelay;
|
|
1286
|
+
return out;
|
|
1287
|
+
}),
|
|
1288
|
+
};
|
|
1289
|
+
const library = this.skills.templates;
|
|
1290
|
+
const wanted = templateId(draft.name);
|
|
1291
|
+
const existing = library.list().find((t) => t.id === wanted);
|
|
1292
|
+
let saved;
|
|
1293
|
+
if (existing && replace) {
|
|
1294
|
+
if (existing.builtin)
|
|
1295
|
+
throw new Error(`"${existing.name}" is a template viberoom ships and cannot be replaced; pick another name`);
|
|
1296
|
+
saved = library.overwrite(wanted, draft);
|
|
1297
|
+
}
|
|
1298
|
+
else
|
|
1299
|
+
saved = library.save(draft);
|
|
1300
|
+
this.skills.templatesChanged();
|
|
1301
|
+
const warnings = result.warnings.map((w) => w.message);
|
|
1302
|
+
this.postSystem(`${participant.name} ${existing && replace ? "updated" : "created"} the room template "${saved.name}" (${saved.vibemates.map((v) => v.name).join(", ") || "no vibemates"}); it is in the picker under New room.`);
|
|
1303
|
+
this.log.info(`templates: ${participant.name} ${existing && replace ? "updated" : "created"} "${saved.name}" (${saved.id})`);
|
|
1304
|
+
return {
|
|
1305
|
+
ok: true,
|
|
1306
|
+
message: `Template "${saved.name}" saved as ${saved.id}${existing && !replace ? ` (the name was taken, so the id got a number; pass replace: true to update your own template instead)` : ""}. The human creates a room from it under New room; nothing in this room changed.${warnings.length ? ` Warnings: ${warnings.join("; ")}` : ""}`,
|
|
1307
|
+
id: saved.id,
|
|
1308
|
+
path: join(library.dir, saved.id, "template.json"),
|
|
1309
|
+
warnings,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
proposeRoomChanges(participantId, why, changes) {
|
|
1313
|
+
const participant = this.agentInRoom(participantId);
|
|
1314
|
+
const shape = this.templateOf();
|
|
1315
|
+
const current = shape.vibemates;
|
|
1316
|
+
const vibes = applyVibemateChanges(current, changes.vibemates);
|
|
1317
|
+
if (vibes.errors.length)
|
|
1318
|
+
throw new Error(`not proposed: ${vibes.errors.join("; ")}`);
|
|
1319
|
+
const touched = vibes.ops.flatMap((op) => [op.name, ...(op.fields ?? []).filter((f) => f.field === "name").map((f) => f.to)]);
|
|
1320
|
+
const result = lintRoomDesign({ settings: changes.settings, vibemates: vibes.next }, { ...this.designContext("room"), changedVibemates: touched });
|
|
1321
|
+
if (result.errors.length)
|
|
1322
|
+
throw new Error(`not proposed: ${result.errors.map((e) => e.message).join("; ")}`);
|
|
1323
|
+
const base = this.designContext("room").base;
|
|
1324
|
+
const settingChanges = diffSettings(base, result.settings);
|
|
1325
|
+
if (!settingChanges.length && !vibes.ops.length)
|
|
1326
|
+
throw new Error("not proposed: the change set leaves the room as it is");
|
|
1327
|
+
const touchesOwn = vibes.ops.some((op) => op.name.toLowerCase() === participant.name.toLowerCase()) || settingChanges.some((c) => c.key === "customRules");
|
|
1328
|
+
const proposal = {
|
|
1329
|
+
key: randomUUID(),
|
|
1330
|
+
participantId,
|
|
1331
|
+
participantName: participant.name,
|
|
1332
|
+
ts: Date.now(),
|
|
1333
|
+
why: String(why ?? "").trim().slice(0, 600),
|
|
1334
|
+
settings: settingChanges,
|
|
1335
|
+
vibemates: vibes.ops,
|
|
1336
|
+
warnings: result.warnings.map((w) => w.message),
|
|
1337
|
+
touchesOwn,
|
|
1338
|
+
status: "pending",
|
|
1339
|
+
};
|
|
1340
|
+
const ids = {};
|
|
1341
|
+
for (const op of vibes.ops) {
|
|
1342
|
+
const target = op.op === "add" ? undefined : this.findByName(op.name);
|
|
1343
|
+
if (target)
|
|
1344
|
+
ids[op.name] = target.id;
|
|
1345
|
+
}
|
|
1346
|
+
this.proposalPlans.set(proposal.key, { settings: settingChanges, vibemates: vibes.next, ops: vibes.ops, ids });
|
|
1347
|
+
this.proposals.set(proposal.key, proposal);
|
|
1348
|
+
this.push({ type: "proposal", proposal });
|
|
1349
|
+
const what = [...settingChanges.map((c) => c.key), ...vibes.ops.map((o) => `${o.op} ${o.name}`)].join(", ");
|
|
1350
|
+
this.postSystem(`${participant.name} proposes changes to the room (${what}); apply or reject them on the card.`, "human");
|
|
1351
|
+
this.log.info(`proposal ${proposal.key} from ${participant.name}: ${what}`);
|
|
1352
|
+
return {
|
|
1353
|
+
ok: true,
|
|
1354
|
+
message: `Proposal sent to ${this.settings.humanName} as a card in the room (${what}). Nothing changes until they apply it; you will see a room line with the outcome.${proposal.warnings.length ? ` Warnings shown on the card: ${proposal.warnings.join("; ")}` : ""}`,
|
|
1355
|
+
key: proposal.key,
|
|
1356
|
+
warnings: proposal.warnings,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
async resolveProposal(key, accept) {
|
|
1360
|
+
const proposal = this.proposals.get(key);
|
|
1361
|
+
const plan = this.proposalPlans.get(key);
|
|
1362
|
+
if (!proposal || !plan)
|
|
1363
|
+
throw new Error("no such pending proposal");
|
|
1364
|
+
if (proposal.status !== "pending")
|
|
1365
|
+
return proposal;
|
|
1366
|
+
const what = [...proposal.settings.map((c) => c.key), ...proposal.vibemates.map((o) => `${o.op} ${o.name}`)].join(", ");
|
|
1367
|
+
if (!accept) {
|
|
1368
|
+
proposal.status = "rejected";
|
|
1369
|
+
this.proposalPlans.delete(key);
|
|
1370
|
+
this.push({ type: "proposal.resolved", key, status: "rejected" });
|
|
1371
|
+
this.postSystem(`${this.settings.humanName} rejected ${proposal.participantName}'s proposal (${what}).`);
|
|
1372
|
+
return proposal;
|
|
1373
|
+
}
|
|
1374
|
+
const skipped = [];
|
|
1375
|
+
for (const op of plan.ops) {
|
|
1376
|
+
const known = plan.ids[op.name];
|
|
1377
|
+
const existing = known ? this.participants.get(known) : this.findByName(op.name);
|
|
1378
|
+
if (op.op !== "add" && (!existing || existing.kind !== "agent" || existing.status === "left")) {
|
|
1379
|
+
skipped.push(`${op.op} ${op.name} (no longer in the room)`);
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (op.op === "remove") {
|
|
1383
|
+
if (existing && existing.kind === "agent")
|
|
1384
|
+
await this.removeParticipant(existing.id);
|
|
1385
|
+
}
|
|
1386
|
+
else if (op.op === "update") {
|
|
1387
|
+
if (!existing || existing.kind !== "agent")
|
|
1388
|
+
continue;
|
|
1389
|
+
const target = plan.vibemates.find((v) => v.name === op.name) ?? plan.vibemates.find((v) => op.fields?.some((f) => f.field === "name" && f.to === v.name));
|
|
1390
|
+
const patch = {};
|
|
1391
|
+
for (const f of op.fields ?? []) {
|
|
1392
|
+
if (f.field === "name")
|
|
1393
|
+
patch.name = f.to;
|
|
1394
|
+
else if (f.field === "tagline")
|
|
1395
|
+
patch.tagline = target?.tagline ?? f.to;
|
|
1396
|
+
else if (f.field === "role")
|
|
1397
|
+
patch.role = target?.role ?? f.to;
|
|
1398
|
+
else if (f.field === "avatar")
|
|
1399
|
+
patch.avatar = target?.avatar ?? f.to;
|
|
1400
|
+
else if (f.field === "skills")
|
|
1401
|
+
patch.skills = target?.skills ?? [];
|
|
1402
|
+
else if (f.field === "replyDelay")
|
|
1403
|
+
patch.replyDelay = target?.replyDelay ?? null;
|
|
1404
|
+
}
|
|
1405
|
+
this.updatePersona(existing.id, patch);
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
const v = plan.vibemates.find((x) => x.name === op.name);
|
|
1409
|
+
if (!v || this.findByName(op.name))
|
|
1410
|
+
skipped.push(`add ${op.name} (the name is taken now)`);
|
|
1411
|
+
else
|
|
1412
|
+
this.addUnstaffed({ name: v.name, tagline: v.tagline, role: v.role, avatar: v.avatar, skills: v.skills });
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (plan.settings.length) {
|
|
1416
|
+
const patch = {};
|
|
1417
|
+
for (const c of plan.settings)
|
|
1418
|
+
patch[c.key] = c.key === "language" ? c.to : c.to;
|
|
1419
|
+
this.updateSettings(patch);
|
|
1420
|
+
}
|
|
1421
|
+
proposal.status = "applied";
|
|
1422
|
+
proposal.skipped = skipped;
|
|
1423
|
+
this.proposalPlans.delete(key);
|
|
1424
|
+
this.push({ type: "proposal.resolved", key, status: "applied", skipped });
|
|
1425
|
+
this.postSystem(`${this.settings.humanName} applied ${proposal.participantName}'s proposal (${what}).${skipped.length ? ` Not applied: ${skipped.join("; ")}.` : ""}`);
|
|
1426
|
+
return proposal;
|
|
1427
|
+
}
|
|
1297
1428
|
createSkillForAgent(participantId, input) {
|
|
1298
1429
|
const participant = this.participants.get(participantId);
|
|
1299
1430
|
if (!participant || !this.runtimes.has(participantId))
|
|
@@ -2127,11 +2258,15 @@ export class Room extends EventEmitter {
|
|
|
2127
2258
|
}
|
|
2128
2259
|
case "usage_update": {
|
|
2129
2260
|
const u = update;
|
|
2261
|
+
if (emptyUsageReport(runtime.lastUsed, u.used)) {
|
|
2262
|
+
runtime.log.info(`usage report of 0 after ${runtime.lastUsed} tokens ignored (failed request?)`);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2130
2265
|
participant.contextUsed = u.used;
|
|
2131
2266
|
participant.contextSize = u.size;
|
|
2132
2267
|
if (u.cost)
|
|
2133
2268
|
participant.cost = { amount: u.cost.amount, currency: u.cost.currency };
|
|
2134
|
-
if (runtime.lastUsed
|
|
2269
|
+
if (looksCompacted(runtime.lastUsed, u.used) && !runtime.briefPending) {
|
|
2135
2270
|
runtime.briefPending = `context shrank from ${runtime.lastUsed} to ${u.used} tokens (compaction?)`;
|
|
2136
2271
|
runtime.log.info(`usage dropped ${runtime.lastUsed} -> ${u.used}; brief scheduled`);
|
|
2137
2272
|
participant.contextEvent = { kind: "compacted", at: Date.now(), used: u.used, size: u.size };
|
package/dist/server.js
CHANGED
|
@@ -193,7 +193,7 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
|
193
193
|
if (!latest)
|
|
194
194
|
throw new Error("no newer version is known; check for updates first");
|
|
195
195
|
log.info(`installing viberoom ${latest} (npm install -g)`);
|
|
196
|
-
const result = await installUpdate(latest);
|
|
196
|
+
const result = await installUpdate(latest, hub.dataDir);
|
|
197
197
|
if (!result.ok)
|
|
198
198
|
throw new Error(`npm install failed: ${result.output.slice(-600) || "no output"}`);
|
|
199
199
|
log.info(`viberoom ${latest} installed; starting the new build, which replaces this hub`);
|
|
@@ -219,6 +219,15 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
|
219
219
|
sendJson(res, 200, { skill });
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
|
+
if (req.method === "GET" && path === "/api/mcp/room") {
|
|
223
|
+
const target = hub.resolveMcpToken(url.searchParams.get("token") ?? "");
|
|
224
|
+
if (!target) {
|
|
225
|
+
sendJson(res, 403, { error: "unknown skills token (the session it belonged to is gone)" });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
sendJson(res, 200, target.room.describeRoomForAgent(target.participantId));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
222
231
|
if (req.method === "GET" && path === "/api/mcp/skill") {
|
|
223
232
|
const target = hub.resolveMcpToken(url.searchParams.get("token") ?? "");
|
|
224
233
|
if (!target) {
|
|
@@ -325,6 +334,50 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
|
325
334
|
sendJson(res, 200, { ok: true, skill: hub.approveSkill(decodeURIComponent(skillApprove[1])) });
|
|
326
335
|
return;
|
|
327
336
|
}
|
|
337
|
+
if (path === "/api/mcp/design/lint" || path === "/api/mcp/templates") {
|
|
338
|
+
const target = hub.resolveMcpToken(String(body.token ?? ""));
|
|
339
|
+
if (!target) {
|
|
340
|
+
sendJson(res, 403, { error: "unknown skills token (the session it belonged to is gone)" });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const design = {
|
|
344
|
+
name: optionalString(body.name) ?? undefined,
|
|
345
|
+
description: optionalString(body.description) ?? undefined,
|
|
346
|
+
emoji: optionalString(body.emoji) ?? undefined,
|
|
347
|
+
settings: body.settings && typeof body.settings === "object" && !Array.isArray(body.settings) ? body.settings : undefined,
|
|
348
|
+
vibemates: Array.isArray(body.vibemates) ? body.vibemates.map((v) => ({ ...v, name: String(v?.name ?? "") })) : undefined,
|
|
349
|
+
};
|
|
350
|
+
if (path === "/api/mcp/design/lint") {
|
|
351
|
+
sendJson(res, 200, target.room.lintDesignForAgent(target.participantId, body.kind === "room" ? "room" : "template", design));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
sendJson(res, 200, target.room.createTemplateForAgent(target.participantId, design, body.replace === true || body.replace === "true"));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (path === "/api/mcp/propose") {
|
|
358
|
+
const target = hub.resolveMcpToken(String(body.token ?? ""));
|
|
359
|
+
if (!target) {
|
|
360
|
+
sendJson(res, 403, { error: "unknown skills token (the session it belonged to is gone)" });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const vib = body.vibemates && typeof body.vibemates === "object" ? body.vibemates : {};
|
|
364
|
+
const list = (v) => (Array.isArray(v) ? v.map((x) => ({ ...x, name: String(x?.name ?? "") })) : undefined);
|
|
365
|
+
sendJson(res, 200, target.room.proposeRoomChanges(target.participantId, String(body.why ?? ""), {
|
|
366
|
+
settings: body.settings && typeof body.settings === "object" && !Array.isArray(body.settings) ? body.settings : undefined,
|
|
367
|
+
vibemates: {
|
|
368
|
+
add: list(vib.add),
|
|
369
|
+
update: list(vib.update),
|
|
370
|
+
remove: Array.isArray(vib.remove) ? vib.remove.map((x) => String(x)) : undefined,
|
|
371
|
+
},
|
|
372
|
+
}));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const proposal = path.match(/^\/api\/rooms\/([^/]+)\/proposals\/([^/]+)$/);
|
|
376
|
+
if (proposal) {
|
|
377
|
+
const room = hub.getRoom(decodeURIComponent(proposal[1]));
|
|
378
|
+
sendJson(res, 200, await room.resolveProposal(decodeURIComponent(proposal[2]), body.accept === true || body.accept === "true"));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
328
381
|
if (path === "/api/mcp/skills" || path === "/api/mcp/attach") {
|
|
329
382
|
const target = hub.resolveMcpToken(String(body.token ?? ""));
|
|
330
383
|
if (!target) {
|
package/dist/shortcuts.js
CHANGED
|
@@ -4,8 +4,8 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, writeFileSync } from "n
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, join } from "node:path";
|
|
6
6
|
import { findChromium } from "./launcher.js";
|
|
7
|
-
export function vbsLauncher(node, main) {
|
|
8
|
-
return ["' viberoom: start the hub without a console window and open the app window.", 'Set sh = CreateObject("WScript.Shell")', `sh.Run """${node}"" ""${main}"" start", 0, False`, ""].join("\r\n");
|
|
7
|
+
export function vbsLauncher(node, main, workingDir) {
|
|
8
|
+
return ["' viberoom: start the hub without a console window and open the app window.", 'Set sh = CreateObject("WScript.Shell")', `sh.CurrentDirectory = "${workingDir}"`, `sh.Run """${node}"" ""${main}"" start", 0, False`, ""].join("\r\n");
|
|
9
9
|
}
|
|
10
10
|
const AUMID_BASE = { "chrome.exe": "Chrome", "msedge.exe": "MSEdge", "brave.exe": "Brave", "chromium.exe": "Chromium" };
|
|
11
11
|
export function appUserModelId(browserPath, url = "http://127.0.0.1:4810/", profileDirName = "browser") {
|
|
@@ -42,13 +42,13 @@ public static class LnkAumid {
|
|
|
42
42
|
}
|
|
43
43
|
}`;
|
|
44
44
|
const psq = (s) => s.replace(/'/g, "''");
|
|
45
|
-
export function shortcutScript(lnk, wscript, vbs,
|
|
45
|
+
export function shortcutScript(lnk, wscript, vbs, workingDir, ico, aumid = null) {
|
|
46
46
|
const lines = [
|
|
47
47
|
"$ErrorActionPreference = 'Stop'",
|
|
48
48
|
`$s = (New-Object -ComObject WScript.Shell).CreateShortcut('${psq(lnk)}')`,
|
|
49
49
|
`$s.TargetPath = '${psq(wscript)}'`,
|
|
50
50
|
`$s.Arguments = '"${psq(vbs)}"'`,
|
|
51
|
-
`$s.WorkingDirectory = '${psq(
|
|
51
|
+
`$s.WorkingDirectory = '${psq(workingDir)}'`,
|
|
52
52
|
`$s.Description = 'viberoom: rooms for you and your coding agents'`,
|
|
53
53
|
ico ? `$s.IconLocation = '${psq(ico)},0'` : "",
|
|
54
54
|
"$s.Save()",
|
|
@@ -82,8 +82,8 @@ export function windowsShortcutPaths(home, env, desktop) {
|
|
|
82
82
|
targets.push(join(home, "Desktop", "viberoom.lnk"));
|
|
83
83
|
return targets;
|
|
84
84
|
}
|
|
85
|
-
export function desktopEntry(node, main, icon) {
|
|
86
|
-
return ["[Desktop Entry]", "Type=Application", "Name=viberoom", "Comment=Rooms for you and your coding agents", `Exec="${node}" "${main}" start`, `Icon=${icon}`, "Terminal=false", "StartupWMClass=viberoom", "Categories=Development;Chat;", ""].join("\n");
|
|
85
|
+
export function desktopEntry(node, main, icon, workingDir) {
|
|
86
|
+
return ["[Desktop Entry]", "Type=Application", "Name=viberoom", "Comment=Rooms for you and your coding agents", `Exec="${node}" "${main}" start`, `Path=${workingDir}`, `Icon=${icon}`, "Terminal=false", "StartupWMClass=viberoom", "Categories=Development;Chat;", ""].join("\n");
|
|
87
87
|
}
|
|
88
88
|
export function macPlist(version) {
|
|
89
89
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -113,7 +113,7 @@ export function installShortcuts(o) {
|
|
|
113
113
|
const icnsSrc = join(o.root, "assets", "icon.icns");
|
|
114
114
|
if (platform === "win32") {
|
|
115
115
|
const vbs = join(launcherDir, "viberoom.vbs");
|
|
116
|
-
writeFileSync(vbs, vbsLauncher(o.node, main));
|
|
116
|
+
writeFileSync(vbs, vbsLauncher(o.node, main, o.dataDir));
|
|
117
117
|
result.files.push(vbs);
|
|
118
118
|
let ico = null;
|
|
119
119
|
if (existsSync(icoSrc)) {
|
|
@@ -126,7 +126,7 @@ export function installShortcuts(o) {
|
|
|
126
126
|
const aumid = appUserModelId(browser, "http://127.0.0.1:4810/", basename(join(o.dataDir, "browser")));
|
|
127
127
|
for (const lnk of windowsShortcutPaths(home, env, o.desktop)) {
|
|
128
128
|
mkdirSync(join(lnk, ".."), { recursive: true });
|
|
129
|
-
const r = spawnSync("powershell", ["-NoProfile", "-Command", shortcutScript(lnk, wscript, vbs, o.
|
|
129
|
+
const r = spawnSync("powershell", ["-NoProfile", "-Command", shortcutScript(lnk, wscript, vbs, o.dataDir, ico, aumid)], { encoding: "utf8" });
|
|
130
130
|
if (r.status === 0)
|
|
131
131
|
result.files.push(lnk);
|
|
132
132
|
else
|
|
@@ -142,7 +142,7 @@ export function installShortcuts(o) {
|
|
|
142
142
|
mkdirSync(join(app, "Contents", "Resources"), { recursive: true });
|
|
143
143
|
writeFileSync(join(app, "Contents", "Info.plist"), macPlist(o.version));
|
|
144
144
|
const exe = join(app, "Contents", "MacOS", "viberoom");
|
|
145
|
-
writeFileSync(exe, `#!/bin/sh\nexec "${o.node}" "${main}" start\n`);
|
|
145
|
+
writeFileSync(exe, `#!/bin/sh\ncd "${o.dataDir}" 2>/dev/null\nexec "${o.node}" "${main}" start\n`);
|
|
146
146
|
chmodSync(exe, 0o755);
|
|
147
147
|
if (existsSync(icnsSrc))
|
|
148
148
|
copyFileSync(icnsSrc, join(app, "Contents", "Resources", "icon.icns"));
|
|
@@ -157,7 +157,7 @@ export function installShortcuts(o) {
|
|
|
157
157
|
copyFileSync(pngSrc, icon);
|
|
158
158
|
result.files.push(icon);
|
|
159
159
|
}
|
|
160
|
-
const entry = desktopEntry(o.node, main, icon);
|
|
160
|
+
const entry = desktopEntry(o.node, main, icon, o.dataDir);
|
|
161
161
|
const appsDir = join(home, ".local", "share", "applications");
|
|
162
162
|
mkdirSync(appsDir, { recursive: true });
|
|
163
163
|
const menuEntry = join(appsDir, "viberoom.desktop");
|
package/dist/skills.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { isReservedSkillName, RESERVED_SKILL_NAMES } from "./commands.js";
|
|
5
5
|
export const SKILL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
6
6
|
export const SKILL_FILE = "SKILL.md";
|
|
7
|
+
import { ROOM_DESIGNER_NAME } from "./persona.js";
|
|
7
8
|
export const BUILTIN_AUTHOR = "viberoom";
|
|
8
9
|
export const HUMAN_AUTHOR = "human";
|
|
9
10
|
const DESCRIPTION_MAX = 300;
|
|
@@ -85,6 +86,29 @@ export const SKILL_WRITER = {
|
|
|
85
86
|
reviewed: true,
|
|
86
87
|
draft: false,
|
|
87
88
|
};
|
|
89
|
+
export const ROOM_DESIGNER = {
|
|
90
|
+
name: ROOM_DESIGNER_NAME,
|
|
91
|
+
description: "How to design a good viberoom room: rules, vibemates and settings, as a template or as a change to this room. Load it before lint_room_design, create_template or propose_room_changes.",
|
|
92
|
+
argumentHint: "",
|
|
93
|
+
body: [
|
|
94
|
+
"A room is a protocol between one human and a few vibemates. The hub already tells every vibemate the mechanics (who it is, @Name addressing, the [silent] reply, tools, language, Markdown); your design adds only what the mechanics do not say. Facts about the settings (keys, bounds, defaults, current values) come from the describe_room tool; do not guess them.",
|
|
95
|
+
"",
|
|
96
|
+
"Rules are the protocol; roles are the people. Write how the vibemates work together once, in the room rules, where all of them read it. A role says who this one is and which way it leans, then ends with \"Everything else is in the room rules\". Two roles that each restate the protocol drift apart.",
|
|
97
|
+
"",
|
|
98
|
+
"Every rule answers a question this room will actually meet. Find the questions before you write the rules: walk through a working day of this particular room and stop wherever two answers are possible. Who acts, who waits, who decides, what \"done\" looks like, what happens when they disagree. The set differs per room: a pair sharing one codebase has to settle ownership and reporting; a room where one drafts and another critiques has to settle when the critique comes and what it is measured against; a room that only answers questions may need almost nothing. A rule that answers no foreseeable question is weight the vibemate carries on every turn.",
|
|
99
|
+
"",
|
|
100
|
+
"Explain, do not enumerate: a rule with its reason generalises (\"Reply only when addressed: every message to agents costs a turn\"); a list of cases fails at the first case not on it. Silence is a design tool: the most useful rule in a multi-mate room is the one that keeps a vibemate at [silent] when a message does not concern it. Pair it with the settings: agentsWakeEachOther off and a low hopLimit for rooms that report to the human; on and higher (about three times the number of vibemates) for rooms that work things out among themselves. Each vibemate works only on its own task and never touches the other's; when a report changes something the other relies on, it is addressed to the other with one line saying what is wanted.",
|
|
101
|
+
"",
|
|
102
|
+
"Names: short, distinct first letters, ideally a hint of the leaning that survives translation. The tagline is the one line the others see in the roster: what this one leans to and what it does when asked. Keep the whole thing short: eight to twelve rules, one per line, is a full protocol; a role is a few sentences; if a rule needs a paragraph it is a skill, not a rule. Do not pin an agent or model in a template: the human picks from what the machine has.",
|
|
103
|
+
"",
|
|
104
|
+
"Before you write anything: describe_room for the facts, then lint_room_design with your draft and read its warnings and the brief preview (that is exactly what the vibemates will read). Play the risky cases against the rules: an unaddressed task, both starting at once, one finding a bug in the other's work unasked, a question in the middle of a task. Then create_template (a file the human picks from; no effect on any room) or propose_room_changes (a card the human applies or rejects; nothing changes without the click). Say in the room what you made and why, in a few lines.",
|
|
105
|
+
].join("\n"),
|
|
106
|
+
userInvocable: true,
|
|
107
|
+
agentInvocable: true,
|
|
108
|
+
author: BUILTIN_AUTHOR,
|
|
109
|
+
reviewed: true,
|
|
110
|
+
draft: false,
|
|
111
|
+
};
|
|
88
112
|
export function parseFrontmatter(text) {
|
|
89
113
|
const normalized = text.replace(/^/, "").replace(/\r\n/g, "\n");
|
|
90
114
|
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
@@ -151,8 +175,10 @@ export function renderSkillBody(body, args) {
|
|
|
151
175
|
.replace(/\r\n/g, "\n")
|
|
152
176
|
.trim();
|
|
153
177
|
}
|
|
178
|
+
export const BUILTIN_SKILLS = [SKILL_WRITER, ROOM_DESIGNER];
|
|
154
179
|
export function isBuiltinSkill(name) {
|
|
155
|
-
|
|
180
|
+
const lower = name.trim().toLowerCase();
|
|
181
|
+
return BUILTIN_SKILLS.some((b) => b.name === lower);
|
|
156
182
|
}
|
|
157
183
|
export class SkillLibrary {
|
|
158
184
|
dir;
|
|
@@ -253,7 +279,7 @@ export class SkillLibrary {
|
|
|
253
279
|
});
|
|
254
280
|
}
|
|
255
281
|
seedBuiltins() {
|
|
256
|
-
for (const builtin of
|
|
282
|
+
for (const builtin of BUILTIN_SKILLS) {
|
|
257
283
|
const folder = this.folderFor(builtin.name);
|
|
258
284
|
const current = folder ? this.load(folder) : undefined;
|
|
259
285
|
if (current && current.description === builtin.description && current.body === builtin.body && (current.argumentHint ?? "") === (builtin.argumentHint ?? ""))
|