- Coordinator tools in its own chat (super admin only): list_extensions,
set_extension_access, delete_extension, and update_skill /
update_mcp_server / update_custom_tool for what can't change where
anything is sent (on/off, names, arguments, left-out tools)
- Adding a skill, MCP server or tool, or changing a skill's words, a
server's URL or headers or a tool's kind or config, goes through
propose_skill / propose_mcp_server / propose_custom_tool: stored in
extension_proposals with the bot's background check, shown as a
[proposal:ID] chip that opens a card with Confirm and Reject
- The card's facts are worked out by the app from what Confirm saves: the
host, every {{env.KEY}} and whether it's saved, the config; for a skill
its links, hosts, tool names, mentions of secrets and a diff against
the saved runbook
- Confirming decides once and checks again; a server or tool missing a key
is saved switched off, and the outcome is posted to the bot in chat
- Never from a bot: stdio servers, a URL whose host a template decides,
envs in a prompt tool, a key's value
Co-Authored-By: Claude Opus 5.5 <[email protected]>
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
type Params = {
|
|
before: string;
|
|
after: string;
|
|
/**
|
|
* Lines of unchanged context kept around each change.
|
|
*/
|
|
context?: number;
|
|
};
|
|
|
|
const MaxLines = 2000;
|
|
|
|
/**
|
|
* A line diff of two texts as `+ added`, `- removed` and ` kept` lines,
|
|
* unchanged runs cut down to `context` lines around each change (a `...`
|
|
* line marks a cut), so one line added to a long runbook stands out.
|
|
* Texts past `MaxLines` lines are compared whole, as one removal and one
|
|
* addition. Empty when they're the same.
|
|
*/
|
|
export default function diffLines({ before, after, context = 2 }: Params) {
|
|
if (before == after) return "";
|
|
|
|
const a = before.split("\n");
|
|
const b = after.split("\n");
|
|
|
|
if (a.length > MaxLines || b.length > MaxLines) {
|
|
return [...a.map((line) => `- ${line}`), ...b.map((line) => `+ ${line}`)].join("\n");
|
|
}
|
|
|
|
const lcs: number[][] = Array.from({ length: a.length + 1 }, () => new Array<number>(b.length + 1).fill(0));
|
|
|
|
for (let i = a.length - 1; i >= 0; i--) {
|
|
for (let j = b.length - 1; j >= 0; j--) {
|
|
lcs[i]![j] = a[i] == b[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!);
|
|
}
|
|
}
|
|
|
|
const lines: { mark: " " | "+" | "-"; text: string }[] = [];
|
|
let i = 0;
|
|
let j = 0;
|
|
|
|
while (i < a.length || j < b.length) {
|
|
if (i < a.length && j < b.length && a[i] == b[j]) {
|
|
lines.push({ mark: " ", text: a[i]! });
|
|
i++;
|
|
j++;
|
|
} else if (i < a.length && (j >= b.length || lcs[i + 1]![j]! >= lcs[i]![j + 1]!)) {
|
|
lines.push({ mark: "-", text: a[i]! });
|
|
i++;
|
|
} else {
|
|
lines.push({ mark: "+", text: b[j]! });
|
|
j++;
|
|
}
|
|
}
|
|
|
|
const near = lines.map((_, index) =>
|
|
lines.slice(Math.max(0, index - context), index + context + 1).some((line) => line.mark != " "),
|
|
);
|
|
const out: string[] = [];
|
|
|
|
lines.forEach((line, index) => {
|
|
if (near[index]) out.push(`${line.mark} ${line.text}`);
|
|
else if (out.at(-1) != "...") out.push("...");
|
|
});
|
|
|
|
return out.join("\n");
|
|
}
|