viberoom 0.3.0 → 0.4.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.
Files changed (64) hide show
  1. package/NOTICE +12 -0
  2. package/README.md +119 -96
  3. package/dist/hub.js +18 -0
  4. package/dist/launcher.js +22 -0
  5. package/dist/main.js +4 -3
  6. package/dist/persona.js +2 -2
  7. package/dist/recipes.js +37 -14
  8. package/dist/room.js +3 -2
  9. package/dist/server.js +16 -3
  10. package/package.json +8 -5
  11. package/scripts/vendor-acp.mjs +85 -0
  12. package/ui/app.css +7 -0
  13. package/ui/app.js +116 -26
  14. package/ui/index.html +2 -0
  15. package/vendor/acp/claude-agent-acp/LICENSE +191 -0
  16. package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
  17. package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
  18. package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
  19. package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
  20. package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
  21. package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
  22. package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
  23. package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
  24. package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
  25. package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
  26. package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
  27. package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
  28. package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
  29. package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
  30. package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
  31. package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
  32. package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
  33. package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
  34. package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
  35. package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
  36. package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
  37. package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
  38. package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
  39. package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
  40. package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
  41. package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
  42. package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
  43. package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
  44. package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
  45. package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
  46. package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
  47. package/vendor/acp/claude-agent-acp/package.json +7 -0
  48. package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
  49. package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
  50. package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
  51. package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
  52. package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
  53. package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
  54. package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
  55. package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
  56. package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
  57. package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
  58. package/vendor/acp/claude-agent-sdk/package.json +7 -0
  59. package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
  60. package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
  61. package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
  62. package/vendor/acp/codex-acp/LICENSE +190 -0
  63. package/vendor/acp/codex-acp/dist/index.js +34238 -0
  64. package/vendor/acp/codex-acp/package.json +7 -0
@@ -0,0 +1,100 @@
1
+ const MAX_UPDATES = 100;
2
+ const MAX_RULES_PER_UPDATE = 100;
3
+ const MAX_DIRECTORIES_PER_UPDATE = 100;
4
+ const MAX_TOOL_NAME_LENGTH = 512;
5
+ const MAX_RULE_CONTENT_LENGTH = 10_000;
6
+ const MAX_DIRECTORY_LENGTH = 4_096;
7
+ const DESTINATIONS = new Set([
8
+ "session",
9
+ "cliArg",
10
+ "userSettings",
11
+ "projectSettings",
12
+ "localSettings",
13
+ ]);
14
+ const BEHAVIORS = new Set(["allow", "deny", "ask"]);
15
+ const MODES = new Set([
16
+ "default",
17
+ "acceptEdits",
18
+ "bypassPermissions",
19
+ "plan",
20
+ "dontAsk",
21
+ "auto",
22
+ ]);
23
+ function isRecord(value) {
24
+ return !!value && typeof value === "object" && !Array.isArray(value);
25
+ }
26
+ function isDestination(value) {
27
+ return typeof value === "string" && DESTINATIONS.has(value);
28
+ }
29
+ function safePlainString(value, maxLength) {
30
+ if (typeof value !== "string")
31
+ return false;
32
+ const normalized = value.trim();
33
+ return (normalized.length > 0 &&
34
+ value.length <= maxLength &&
35
+ !Array.from(normalized).some((character) => {
36
+ const code = character.charCodeAt(0);
37
+ return (code <= 0x08 ||
38
+ code === 0x0b ||
39
+ code === 0x0c ||
40
+ (code >= 0x0e && code <= 0x1f) ||
41
+ code === 0x7f);
42
+ }));
43
+ }
44
+ function isRule(value) {
45
+ if (!isRecord(value) || !safePlainString(value.toolName, MAX_TOOL_NAME_LENGTH)) {
46
+ return false;
47
+ }
48
+ return (value.ruleContent === undefined || safePlainString(value.ruleContent, MAX_RULE_CONTENT_LENGTH));
49
+ }
50
+ function isKnownUpdate(value) {
51
+ if (!isRecord(value) || !isDestination(value.destination) || typeof value.type !== "string") {
52
+ return false;
53
+ }
54
+ switch (value.type) {
55
+ case "addRules":
56
+ case "replaceRules":
57
+ case "removeRules":
58
+ return (Array.isArray(value.rules) &&
59
+ value.rules.length <= MAX_RULES_PER_UPDATE &&
60
+ (value.type === "replaceRules" || value.rules.length > 0) &&
61
+ value.rules.every(isRule) &&
62
+ typeof value.behavior === "string" &&
63
+ BEHAVIORS.has(value.behavior));
64
+ case "setMode":
65
+ return typeof value.mode === "string" && MODES.has(value.mode);
66
+ case "addDirectories":
67
+ case "removeDirectories":
68
+ return (Array.isArray(value.directories) &&
69
+ value.directories.length > 0 &&
70
+ value.directories.length <= MAX_DIRECTORIES_PER_UPDATE &&
71
+ value.directories.every((directory) => safePlainString(directory, MAX_DIRECTORY_LENGTH)));
72
+ default:
73
+ return false;
74
+ }
75
+ }
76
+ export function normalizeDurablePermissionChangeSet(suggestions, forcedAsk = false) {
77
+ if (forcedAsk)
78
+ return undefined;
79
+ try {
80
+ if (!Array.isArray(suggestions) ||
81
+ suggestions.length === 0 ||
82
+ suggestions.length > MAX_UPDATES ||
83
+ !suggestions.every(isKnownUpdate)) {
84
+ return undefined;
85
+ }
86
+ return {
87
+ // The ACP prompt may stay open for an arbitrary amount of time. Snapshot the
88
+ // provider payload so the label the user approved and the effect returned to
89
+ // Claude cannot diverge through later mutation of the callback argument.
90
+ updates: structuredClone(suggestions),
91
+ };
92
+ }
93
+ catch {
94
+ // SDK data is expected to be plain structured-cloneable input. Treat an
95
+ // accessor, function, proxy, or other non-wire value as an invalid bundle.
96
+ // Validation belongs in this boundary too because merely reading one of those
97
+ // values can throw before structuredClone gets a chance to reject it.
98
+ return undefined;
99
+ }
100
+ }
@@ -0,0 +1,124 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { plainString, withOptionalUpdate } from "./shared.js";
4
+ function isFileSessionChangeSet(changeSet, operation) {
5
+ return (!!changeSet &&
6
+ changeSet.updates.every((update) => {
7
+ if (update.destination !== "session")
8
+ return false;
9
+ if (update.type === "setMode") {
10
+ return operation === "write" && update.mode === "acceptEdits";
11
+ }
12
+ if (update.type === "addDirectories")
13
+ return operation === "write";
14
+ if (update.type !== "addRules" || update.behavior !== "allow")
15
+ return false;
16
+ return update.rules.every((rule) => operation === "read" ? rule.toolName === "Read" : rule.toolName === "Edit");
17
+ }));
18
+ }
19
+ function inputPath(context, key) {
20
+ return plainString(context.input[key]);
21
+ }
22
+ function isInside(directory, candidate) {
23
+ const relative = path.relative(path.resolve(directory), path.resolve(candidate));
24
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
25
+ }
26
+ function hasSessionAllowRuleFor(changeSet, toolName) {
27
+ return !!changeSet?.updates.some((update) => update.type === "addRules" &&
28
+ update.destination === "session" &&
29
+ update.behavior === "allow" &&
30
+ update.rules.some((rule) => rule.toolName === toolName));
31
+ }
32
+ function hasBroadSessionAllowRuleFor(changeSet, toolName) {
33
+ return changeSet.updates.some((update) => update.type === "addRules" &&
34
+ update.destination === "session" &&
35
+ update.behavior === "allow" &&
36
+ update.rules.some((rule) => rule.toolName === toolName && rule.ruleContent === undefined));
37
+ }
38
+ function hasAcceptEditsMode(changeSet) {
39
+ return changeSet.updates.some((update) => update.type === "setMode" &&
40
+ update.destination === "session" &&
41
+ update.mode === "acceptEdits");
42
+ }
43
+ function permissionPath(ruleContent) {
44
+ const value = plainString(ruleContent);
45
+ if (!value)
46
+ return undefined;
47
+ return value.replace(/[/\\]\*\*$/, "");
48
+ }
49
+ function effectCoversFilePath(changeSet, filePath, operation, cwd) {
50
+ const directory = path.dirname(filePath);
51
+ const comparable = (candidate) => {
52
+ const resolved = path.resolve(cwd, candidate);
53
+ return resolved === directory || isInside(resolved, filePath);
54
+ };
55
+ const paths = changeSet.updates.flatMap((update) => {
56
+ if (update.type === "addDirectories")
57
+ return update.directories;
58
+ if (update.type !== "addRules" || update.behavior !== "allow")
59
+ return [];
60
+ return update.rules
61
+ .filter((rule) => operation === "read" ? rule.toolName === "Read" : rule.toolName === "Edit")
62
+ .map((rule) => permissionPath(rule.ruleContent))
63
+ .filter((value) => value !== undefined);
64
+ });
65
+ return paths.some(comparable);
66
+ }
67
+ function fileSessionLabel(context, filePath, operation) {
68
+ if (!isFileSessionChangeSet(context.durableChangeSet, operation))
69
+ return undefined;
70
+ const resolvedFilePath = filePath ? path.resolve(context.cwd, filePath) : undefined;
71
+ const isClaudePath = operation === "write" &&
72
+ !!resolvedFilePath &&
73
+ (isInside(path.join(context.cwd, ".claude"), resolvedFilePath) ||
74
+ isInside(path.join(os.homedir(), ".claude"), resolvedFilePath));
75
+ const broadRule = hasBroadSessionAllowRuleFor(context.durableChangeSet, operation === "read" ? "Read" : "Edit");
76
+ if (resolvedFilePath) {
77
+ const coversCurrentPath = effectCoversFilePath(context.durableChangeSet, resolvedFilePath, operation, context.cwd) ||
78
+ broadRule ||
79
+ (operation === "write" && !isClaudePath && hasAcceptEditsMode(context.durableChangeSet));
80
+ if (!coversCurrentPath)
81
+ return undefined;
82
+ }
83
+ else if (!broadRule &&
84
+ !(operation === "write" && hasAcceptEditsMode(context.durableChangeSet))) {
85
+ return undefined;
86
+ }
87
+ if (operation === "write" &&
88
+ resolvedFilePath &&
89
+ isClaudePath &&
90
+ hasSessionAllowRuleFor(context.durableChangeSet, "Edit")) {
91
+ return "Yes, and allow Claude to edit its own settings for this session";
92
+ }
93
+ if (!resolvedFilePath || isInside(context.cwd, resolvedFilePath)) {
94
+ return operation === "read"
95
+ ? "Yes, during this session"
96
+ : "Yes, allow all edits during this session";
97
+ }
98
+ const directory = path.dirname(resolvedFilePath);
99
+ const directoryName = path.basename(directory) || "this directory";
100
+ return operation === "read"
101
+ ? `Yes, allow reading from ${directoryName}${path.sep} during this session`
102
+ : `Yes, allow all edits in ${directoryName}${path.sep} during this session`;
103
+ }
104
+ function buildFilePermissionOptions(context, filePath, operation) {
105
+ return withOptionalUpdate(context.durableChangeSet, fileSessionLabel(context, filePath, operation), "Yes", "No");
106
+ }
107
+ export function buildReadPermissionOptions(context) {
108
+ return buildFilePermissionOptions(context, inputPath(context, "file_path"), "read");
109
+ }
110
+ export function buildGlobPermissionOptions(context) {
111
+ return buildFilePermissionOptions(context, inputPath(context, "path") ?? context.cwd, "read");
112
+ }
113
+ export function buildGrepPermissionOptions(context) {
114
+ return buildFilePermissionOptions(context, inputPath(context, "path") ?? context.cwd, "read");
115
+ }
116
+ export function buildEditPermissionOptions(context) {
117
+ return buildFilePermissionOptions(context, inputPath(context, "file_path"), "write");
118
+ }
119
+ export function buildWritePermissionOptions(context) {
120
+ return buildFilePermissionOptions(context, inputPath(context, "file_path"), "write");
121
+ }
122
+ export function buildNotebookEditPermissionOptions(context) {
123
+ return buildFilePermissionOptions(context, inputPath(context, "notebook_path"), "write");
124
+ }
@@ -0,0 +1,64 @@
1
+ export const PERMISSION_OPTION_ID = {
2
+ allowOnce: "allow-once",
3
+ allowWithUpdates: "allow-with-updates",
4
+ allowSkillExact: "allow-skill-exact",
5
+ allowSkillPrefix: "allow-skill-prefix",
6
+ exitPlanBypass: "exit-plan-bypass",
7
+ exitPlanAuto: "exit-plan-auto",
8
+ exitPlanAcceptEdits: "exit-plan-accept-edits",
9
+ exitPlanDefault: "exit-plan-default",
10
+ exitPlanClearAuto: "exit-plan-clear-auto",
11
+ exitPlanClearBypass: "exit-plan-clear-bypass",
12
+ exitPlanClearAcceptEdits: "exit-plan-clear-accept-edits",
13
+ reject: "reject",
14
+ };
15
+ export function allowOnce(name = "Yes") {
16
+ return { optionId: PERMISSION_OPTION_ID.allowOnce, name, kind: "allow_once" };
17
+ }
18
+ export function allowWithUpdates(name) {
19
+ return { optionId: PERMISSION_OPTION_ID.allowWithUpdates, name, kind: "allow_always" };
20
+ }
21
+ export function reject(name = "No") {
22
+ return { optionId: PERMISSION_OPTION_ID.reject, name, kind: "reject_once" };
23
+ }
24
+ export function withOptionalUpdate(changeSet, updateName, allowName = "Yes", rejectName = "No") {
25
+ const options = [allowOnce(allowName)];
26
+ if (changeSet && updateName)
27
+ options.push(allowWithUpdates(updateName));
28
+ options.push(reject(rejectName));
29
+ return options;
30
+ }
31
+ export function plainString(value) {
32
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
33
+ }
34
+ export function exactLocalAllowRule(changeSet, toolName, ruleContent) {
35
+ if (!changeSet || changeSet.updates.length !== 1)
36
+ return false;
37
+ const update = changeSet.updates[0];
38
+ if (update?.type !== "addRules" ||
39
+ update.behavior !== "allow" ||
40
+ update.destination !== "localSettings" ||
41
+ update.rules.length !== 1) {
42
+ return false;
43
+ }
44
+ const rule = update.rules[0];
45
+ return (rule?.toolName === toolName &&
46
+ (ruleContent === undefined
47
+ ? rule.ruleContent === undefined
48
+ : plainString(rule.ruleContent) === ruleContent));
49
+ }
50
+ /**
51
+ * An MCP "don't ask again" option is only truthful when every provider update
52
+ * adds an unrestricted allow rule for the tool currently being prompted.
53
+ */
54
+ export function isMcpAllowChangeSet(changeSet, toolName) {
55
+ return (!!changeSet &&
56
+ changeSet.updates.length > 0 &&
57
+ changeSet.updates.every((update) => update.type === "addRules" &&
58
+ update.behavior === "allow" &&
59
+ update.rules.length > 0 &&
60
+ update.rules.every((rule) => rule.toolName === toolName && rule.ruleContent === undefined)));
61
+ }
62
+ export function withGeneratedUpdate(name, rejectName = "No") {
63
+ return [allowOnce(), allowWithUpdates(name), reject(rejectName)];
64
+ }
@@ -0,0 +1,100 @@
1
+ import path from "node:path";
2
+ import { plainString, withOptionalUpdate } from "./shared.js";
3
+ function permissionRulePrefix(value) {
4
+ return value.endsWith(":*") ? value.slice(0, -2) : value;
5
+ }
6
+ function displayList(values) {
7
+ if (values.join(", ").length > 50)
8
+ return "similar";
9
+ if (values.length === 1)
10
+ return values[0];
11
+ if (values.length === 2)
12
+ return `${values[0]} and ${values[1]}`;
13
+ return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`;
14
+ }
15
+ /** Shorten paths only as much as needed to distinguish them in prompt text. */
16
+ function displayPaths(paths) {
17
+ const normalizedPaths = [...new Set(paths.map((value) => path.normalize(value)))];
18
+ const segments = normalizedPaths.map((value) => value.split(path.sep).filter(Boolean));
19
+ // Compare equally deep suffixes so duplicate basenames gain the smallest
20
+ // useful parent prefix (for example, codex-acp/src/ vs claude-acp/src/).
21
+ const suffix = (index, depth) => segments[index].slice(-depth).join(path.sep) || normalizedPaths[index];
22
+ const names = normalizedPaths.map((value, index) => {
23
+ const parts = segments[index];
24
+ let name = path.basename(value) || value;
25
+ for (let depth = 1; depth <= parts.length; depth++) {
26
+ const candidate = suffix(index, depth);
27
+ if (normalizedPaths.every((_, other) => other === index || suffix(other, depth) !== candidate)) {
28
+ name = candidate;
29
+ break;
30
+ }
31
+ if (depth === parts.length)
32
+ name = value;
33
+ }
34
+ return name.endsWith(path.sep) ? name : `${name}${path.sep}`;
35
+ });
36
+ // Native Claude labels enumerate two paths and summarize larger sets.
37
+ if (names.length <= 2)
38
+ return displayList(names);
39
+ return `${names[0]}, ${names[1]} and ${names.length - 2} more`;
40
+ }
41
+ /** Plain-text port of Claude Code's generateShellSuggestionsLabel. */
42
+ function shellSuggestionsLabel(toolName, changeSet) {
43
+ const representable = changeSet.updates.every((update) => {
44
+ if (update.type === "addDirectories")
45
+ return update.destination === "session";
46
+ if (update.type !== "addRules" || update.behavior !== "allow")
47
+ return false;
48
+ return update.rules.every((rule) => (rule.toolName === toolName || rule.toolName === "Read") &&
49
+ plainString(rule.ruleContent) !== undefined);
50
+ });
51
+ if (!representable)
52
+ return undefined;
53
+ const rules = changeSet.updates
54
+ .filter((update) => update.type === "addRules")
55
+ .flatMap((update) => update.rules);
56
+ const readPaths = rules
57
+ .filter((rule) => rule.toolName === "Read")
58
+ .map((rule) => rule.ruleContent?.replace(/\/\*\*$/, ""))
59
+ .filter((value) => !!value);
60
+ const commands = [
61
+ ...new Set(rules
62
+ .filter((rule) => rule.toolName === toolName && rule.ruleContent)
63
+ .map((rule) => permissionRulePrefix(rule.ruleContent))),
64
+ ];
65
+ const directories = changeSet.updates
66
+ .filter((update) => update.type === "addDirectories")
67
+ .flatMap((update) => update.directories);
68
+ const hasPaths = readPaths.length > 0 || directories.length > 0;
69
+ if (readPaths.length > 0 && directories.length === 0 && commands.length === 0) {
70
+ return `Yes, allow reading from ${displayPaths(readPaths)} from this project`;
71
+ }
72
+ if (directories.length > 0 && readPaths.length === 0 && commands.length === 0) {
73
+ return `Yes, and always allow access to ${displayPaths(directories)} from this project`;
74
+ }
75
+ if (commands.length > 0 && !hasPaths) {
76
+ return `Yes, and don't ask again for ${displayList(commands)} commands`;
77
+ }
78
+ if (hasPaths && commands.length === 0) {
79
+ return `Yes, and always allow access to ${displayPaths([...directories, ...readPaths])} from this project`;
80
+ }
81
+ if (hasPaths && commands.length > 0) {
82
+ const paths = [...directories, ...readPaths];
83
+ return paths.length === 1 && commands.length === 1
84
+ ? `Yes, and allow access to ${displayPaths(paths)} and ${displayList(commands)} commands`
85
+ : `Yes, and allow ${displayPaths(paths)} access and ${displayList(commands)} commands`;
86
+ }
87
+ return undefined;
88
+ }
89
+ function buildShellPermissionOptions(context, toolName) {
90
+ const name = context.durableChangeSet
91
+ ? shellSuggestionsLabel(toolName, context.durableChangeSet)
92
+ : undefined;
93
+ return withOptionalUpdate(context.durableChangeSet, name, "Yes", "No");
94
+ }
95
+ export function buildBashPermissionOptions(context) {
96
+ return buildShellPermissionOptions(context, "Bash");
97
+ }
98
+ export function buildPowerShellPermissionOptions(context) {
99
+ return buildShellPermissionOptions(context, "PowerShell");
100
+ }
@@ -0,0 +1,135 @@
1
+ import { allowOnce, exactLocalAllowRule, isMcpAllowChangeSet, PERMISSION_OPTION_ID, plainString, reject, withGeneratedUpdate, withOptionalUpdate, } from "./shared.js";
2
+ export function buildWebFetchPermissionOptions(context) {
3
+ const url = plainString(context.input.url);
4
+ if (url && context.allowPersistentOptions !== false) {
5
+ try {
6
+ const hostname = new URL(url).hostname;
7
+ if (hostname) {
8
+ return withGeneratedUpdate(`Yes, and don't ask again for ${hostname}`);
9
+ }
10
+ }
11
+ catch {
12
+ // Invalid input cannot produce Claude Code's domain-specific option.
13
+ }
14
+ }
15
+ return [allowOnce(), reject("No")];
16
+ }
17
+ export function buildSkillPermissionOptions(context) {
18
+ const skill = plainString(context.input.skill);
19
+ const options = [allowOnce()];
20
+ if (skill && context.allowPersistentOptions !== false) {
21
+ options.push({
22
+ optionId: PERMISSION_OPTION_ID.allowSkillExact,
23
+ name: `Yes, and don't ask again for ${skill}`,
24
+ kind: "allow_always",
25
+ });
26
+ const spaceIndex = skill.indexOf(" ");
27
+ if (spaceIndex > 0) {
28
+ const prefix = `${skill.slice(0, spaceIndex)}:*`;
29
+ options.push({
30
+ optionId: PERMISSION_OPTION_ID.allowSkillPrefix,
31
+ name: `Yes, and don't ask again for ${prefix} commands`,
32
+ kind: "allow_always",
33
+ });
34
+ }
35
+ }
36
+ options.push(reject("No"));
37
+ return options;
38
+ }
39
+ export function buildEnterPlanModePermissionOptions() {
40
+ return [allowOnce("Yes, enter plan mode"), reject("No, start implementing now")];
41
+ }
42
+ export function buildExitPlanModePermissionOptions(context) {
43
+ const modes = new Set(context.availableModes);
44
+ const options = [];
45
+ const elevatedMode = modes.has("auto")
46
+ ? "auto"
47
+ : modes.has("bypassPermissions")
48
+ ? "bypassPermissions"
49
+ : "acceptEdits";
50
+ if (plainString(context.input.plan)) {
51
+ const usage = context.contextUsedPercent === undefined ? "" : ` (${context.contextUsedPercent}% used)`;
52
+ if (elevatedMode === "auto") {
53
+ options.push({
54
+ optionId: PERMISSION_OPTION_ID.exitPlanClearAuto,
55
+ name: `Yes, clear context${usage} and use auto mode`,
56
+ kind: "allow_always",
57
+ });
58
+ }
59
+ else if (elevatedMode === "bypassPermissions") {
60
+ options.push({
61
+ optionId: PERMISSION_OPTION_ID.exitPlanClearBypass,
62
+ name: `Yes, clear context${usage} and bypass permissions`,
63
+ kind: "allow_always",
64
+ });
65
+ }
66
+ else {
67
+ options.push({
68
+ optionId: PERMISSION_OPTION_ID.exitPlanClearAcceptEdits,
69
+ name: `Yes, clear context${usage} and auto-accept edits`,
70
+ kind: "allow_always",
71
+ });
72
+ }
73
+ }
74
+ if (elevatedMode === "auto") {
75
+ options.push({
76
+ optionId: PERMISSION_OPTION_ID.exitPlanAuto,
77
+ name: "Yes, and use auto mode",
78
+ kind: "allow_always",
79
+ });
80
+ }
81
+ else if (elevatedMode === "bypassPermissions") {
82
+ options.push({
83
+ optionId: PERMISSION_OPTION_ID.exitPlanBypass,
84
+ name: "Yes, and bypass permissions",
85
+ kind: "allow_always",
86
+ });
87
+ }
88
+ else {
89
+ options.push({
90
+ optionId: PERMISSION_OPTION_ID.exitPlanAcceptEdits,
91
+ name: "Yes, auto-accept edits",
92
+ kind: "allow_always",
93
+ });
94
+ }
95
+ options.push({
96
+ optionId: PERMISSION_OPTION_ID.exitPlanDefault,
97
+ name: "Yes, manually approve edits",
98
+ kind: "allow_once",
99
+ });
100
+ options.push(reject("No, keep planning"));
101
+ return options;
102
+ }
103
+ export function buildSandboxNetworkPermissionOptions(context) {
104
+ const host = plainString(context.input.host);
105
+ const name = host && exactLocalAllowRule(context.durableChangeSet, context.toolName, host)
106
+ ? `Yes, and don't ask again for ${host}`
107
+ : undefined;
108
+ return withOptionalUpdate(context.durableChangeSet, name, "Yes", "No");
109
+ }
110
+ export function buildFallbackPermissionOptions(context) {
111
+ const toolLabel = plainString(context.displayName) ?? context.toolName;
112
+ if (context.toolName.startsWith("mcp__")) {
113
+ const changeSet = context.allowPersistentOptions !== false &&
114
+ isMcpAllowChangeSet(context.durableChangeSet, context.toolName)
115
+ ? context.durableChangeSet
116
+ : undefined;
117
+ return withOptionalUpdate(changeSet, changeSet ? `Yes, and don't ask again for ${toolLabel} commands` : undefined);
118
+ }
119
+ if (context.allowPersistentOptions !== false) {
120
+ return withGeneratedUpdate(`Yes, and don't ask again for ${toolLabel} commands`);
121
+ }
122
+ return [allowOnce(), reject("No")];
123
+ }
124
+ const COMPUTER_USE_MCP_TOOL_PREFIX = "mcp__computer-use__";
125
+ export function isComputerUseMcpTool(toolName) {
126
+ return toolName.startsWith(COMPUTER_USE_MCP_TOOL_PREFIX);
127
+ }
128
+ export function buildComputerUseMcpPermissionOptions(context) {
129
+ const toolLabel = plainString(context.displayName) ?? context.toolName;
130
+ const changeSet = context.allowPersistentOptions !== false &&
131
+ isMcpAllowChangeSet(context.durableChangeSet, context.toolName)
132
+ ? context.durableChangeSet
133
+ : undefined;
134
+ return withOptionalUpdate(changeSet, changeSet ? `Yes, and don't ask again for ${toolLabel}` : undefined);
135
+ }
@@ -0,0 +1,60 @@
1
+ import { buildEditPermissionOptions, buildGlobPermissionOptions, buildGrepPermissionOptions, buildNotebookEditPermissionOptions, buildReadPermissionOptions, buildWritePermissionOptions, } from "./options/filesystem.js";
2
+ import { buildBashPermissionOptions, buildPowerShellPermissionOptions } from "./options/shell.js";
3
+ import { buildComputerUseMcpPermissionOptions, buildEnterPlanModePermissionOptions, buildExitPlanModePermissionOptions, buildFallbackPermissionOptions, buildSandboxNetworkPermissionOptions, buildSkillPermissionOptions, buildWebFetchPermissionOptions, isComputerUseMcpTool, } from "./options/tools.js";
4
+ export { PERMISSION_OPTION_ID } from "./options/shared.js";
5
+ export function buildClaudePermissionOptions(context) {
6
+ return buildUnsortedClaudePermissionOptions(context).sort((left, right) => permissionOptionOrder(left) - permissionOptionOrder(right));
7
+ }
8
+ function buildUnsortedClaudePermissionOptions(context) {
9
+ if (isComputerUseMcpTool(context.toolName)) {
10
+ return buildComputerUseMcpPermissionOptions(context);
11
+ }
12
+ switch (context.toolName) {
13
+ case "AskUserQuestion":
14
+ throw new Error("AskUserQuestion must be handled by ACP elicitation, not permission options");
15
+ case "Bash":
16
+ return buildBashPermissionOptions(context);
17
+ case "PowerShell":
18
+ return buildPowerShellPermissionOptions(context);
19
+ case "Read":
20
+ return buildReadPermissionOptions(context);
21
+ case "Glob":
22
+ return buildGlobPermissionOptions(context);
23
+ case "Grep":
24
+ return buildGrepPermissionOptions(context);
25
+ case "Edit":
26
+ return buildEditPermissionOptions(context);
27
+ case "Write":
28
+ return buildWritePermissionOptions(context);
29
+ case "NotebookEdit":
30
+ return buildNotebookEditPermissionOptions(context);
31
+ case "WebFetch":
32
+ return buildWebFetchPermissionOptions(context);
33
+ case "Skill":
34
+ return buildSkillPermissionOptions(context);
35
+ case "EnterPlanMode":
36
+ return buildEnterPlanModePermissionOptions();
37
+ case "ExitPlanMode":
38
+ return buildExitPlanModePermissionOptions(context);
39
+ case "SandboxNetworkAccess":
40
+ return buildSandboxNetworkPermissionOptions(context);
41
+ case "WebSearch":
42
+ case "Agent":
43
+ case "Task":
44
+ default:
45
+ return buildFallbackPermissionOptions(context);
46
+ }
47
+ }
48
+ function permissionOptionOrder(option) {
49
+ switch (option.kind) {
50
+ case "allow_once":
51
+ return 0;
52
+ case "allow_always":
53
+ return 1;
54
+ case "reject_once":
55
+ case "reject_always":
56
+ return 3;
57
+ default:
58
+ return 2;
59
+ }
60
+ }
@@ -0,0 +1,83 @@
1
+ import { toolInfoFromToolUse } from "../tools.js";
2
+ function humanText(value, maxLength, singleLine = false) {
3
+ if (typeof value !== "string")
4
+ return undefined;
5
+ const withoutControls = Array.from(value)
6
+ .filter((character) => {
7
+ const code = character.charCodeAt(0);
8
+ return !(code <= 0x08 ||
9
+ code === 0x0b ||
10
+ code === 0x0c ||
11
+ (code >= 0x0e && code <= 0x1f) ||
12
+ code === 0x7f);
13
+ })
14
+ .join("");
15
+ const normalized = singleLine
16
+ ? withoutControls.replace(/\s+/gu, " ").trim()
17
+ : withoutControls.trim();
18
+ return normalized && normalized.length <= maxLength ? normalized : undefined;
19
+ }
20
+ function compactText(value) {
21
+ return humanText(value, 160, true);
22
+ }
23
+ function withBlockedPath(locations, blockedPath) {
24
+ const path = humanText(blockedPath, 4_096, true);
25
+ if (!path)
26
+ return locations;
27
+ const result = [...(locations ?? [])];
28
+ if (!result.some((location) => location.path === path))
29
+ result.push({ path });
30
+ return result;
31
+ }
32
+ export function buildClaudePermissionPresentation(value) {
33
+ const info = toolInfoFromToolUse({ id: value.toolUseID, name: value.toolName, input: value.input }, value.supportsTerminalOutput ?? false, value.cwd);
34
+ const host = value.toolName === "SandboxNetworkAccess" ? compactText(value.input.host) : undefined;
35
+ const isComputerUse = value.toolName.startsWith("mcp__computer-use__");
36
+ const subjectTitle = host ?? (isComputerUse ? compactText(value.displayName) : undefined);
37
+ const subjectContent = (host || isComputerUse) && info.content.length === 0
38
+ ? [
39
+ {
40
+ type: "content",
41
+ content: {
42
+ type: "text",
43
+ text: `\`\`\`json\n${JSON.stringify(value.input, null, 2)}\n\`\`\``,
44
+ },
45
+ },
46
+ ]
47
+ : info.content;
48
+ // Reuse the exact standard tool-call heading as the permission heading so
49
+ // the approval never maintains a second, divergent name for the operation.
50
+ // decisionReason is temporarily exposed as the permission description so
51
+ // its actual SDK values can be inspected; it remains diagnostic policy text.
52
+ const shellTitle = value.toolName === "Bash" || value.toolName === "PowerShell"
53
+ ? (compactText(value.input.description) ?? value.toolName)
54
+ : undefined;
55
+ const toolCallTitle = shellTitle ?? subjectTitle ?? info.title;
56
+ const permissionTitle = value.toolName === "ExitPlanMode" ? "Ready to code?" : toolCallTitle;
57
+ const title = humanText(permissionTitle, 4_000, true) ?? "Use tool?";
58
+ const decisionReason = humanText(value.decisionReason, 4_000);
59
+ const description = decisionReason ? `Reason: ${decisionReason}` : undefined;
60
+ return {
61
+ toolCall: {
62
+ toolCallId: value.toolUseID,
63
+ name: value.toolName,
64
+ status: "pending",
65
+ rawInput: value.input,
66
+ ...info,
67
+ title: toolCallTitle,
68
+ content: subjectContent,
69
+ locations: withBlockedPath(info.locations, value.blockedPath),
70
+ },
71
+ ...(title
72
+ ? {
73
+ _meta: {
74
+ permission: {
75
+ version: 1,
76
+ title,
77
+ ...(description ? { description } : {}),
78
+ },
79
+ },
80
+ }
81
+ : {}),
82
+ };
83
+ }