surgent 0.7.0-alpha.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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +407 -0
  3. package/bin/surgent.js +211 -0
  4. package/dist/optimizers/LICENSE +21 -0
  5. package/dist/optimizers/index.js +1984 -0
  6. package/dist/optimizers/index.js.map +7 -0
  7. package/dist/optimizers/package.json +31 -0
  8. package/package.json +45 -0
  9. package/src/agent/built-in/documenter.md +58 -0
  10. package/src/agent/built-in/general.md +107 -0
  11. package/src/agent/built-in/planner.md +73 -0
  12. package/src/agent/built-in/scout.md +97 -0
  13. package/src/agent/command.ts +140 -0
  14. package/src/agent/helpers.ts +95 -0
  15. package/src/agent/index.ts +9 -0
  16. package/src/agent/storage.ts +287 -0
  17. package/src/agent/types.ts +28 -0
  18. package/src/checkpoint/git.ts +173 -0
  19. package/src/checkpoint/index.ts +117 -0
  20. package/src/checkpoint/snapshot.ts +28 -0
  21. package/src/checkpoint/stage.ts +59 -0
  22. package/src/checkpoint/store.ts +108 -0
  23. package/src/cleanup/checkpoint.ts +31 -0
  24. package/src/cleanup/helpers.ts +24 -0
  25. package/src/cleanup/index.ts +21 -0
  26. package/src/cleanup/permission.ts +74 -0
  27. package/src/cleanup/subsession.ts +46 -0
  28. package/src/commands/helpers.ts +217 -0
  29. package/src/commands/index.ts +79 -0
  30. package/src/commands/render.ts +95 -0
  31. package/src/commands/types.ts +11 -0
  32. package/src/mcp-client/call-tool.ts +143 -0
  33. package/src/mcp-client/client.ts +90 -0
  34. package/src/mcp-client/command.ts +257 -0
  35. package/src/mcp-client/helpers.ts +153 -0
  36. package/src/mcp-client/index.ts +21 -0
  37. package/src/mcp-client/list-tools.ts +84 -0
  38. package/src/mcp-client/storage.ts +190 -0
  39. package/src/mcp-client/types.ts +34 -0
  40. package/src/mcp-client/validation.ts +115 -0
  41. package/src/optimizers/compactor/bash.ts +159 -0
  42. package/src/optimizers/compactor/grep.ts +141 -0
  43. package/src/optimizers/compactor/index.ts +132 -0
  44. package/src/optimizers/deduplicator/helpers.ts +75 -0
  45. package/src/optimizers/deduplicator/index.ts +23 -0
  46. package/src/optimizers/deduplicator/resources.ts +77 -0
  47. package/src/optimizers/deduplicator/state.ts +119 -0
  48. package/src/optimizers/deduplicator/types.ts +14 -0
  49. package/src/optimizers/entries.ts +104 -0
  50. package/src/optimizers/index.ts +17 -0
  51. package/src/optimizers/inspector/helpers.ts +60 -0
  52. package/src/optimizers/inspector/index.ts +89 -0
  53. package/src/optimizers/inspector/inspect.ts +88 -0
  54. package/src/optimizers/inspector/types.ts +7 -0
  55. package/src/optimizers/languages/go.ts +79 -0
  56. package/src/optimizers/languages/grammar.ts +200 -0
  57. package/src/optimizers/languages/index.ts +75 -0
  58. package/src/optimizers/languages/java.ts +64 -0
  59. package/src/optimizers/languages/python.ts +63 -0
  60. package/src/optimizers/languages/rust.ts +71 -0
  61. package/src/optimizers/languages/symbols.ts +95 -0
  62. package/src/optimizers/languages/tree-sitter-languages.d.ts +23 -0
  63. package/src/optimizers/languages/types.ts +134 -0
  64. package/src/optimizers/languages/typescript.ts +116 -0
  65. package/src/optimizers/mapper/files.ts +94 -0
  66. package/src/optimizers/mapper/index.ts +133 -0
  67. package/src/optimizers/mapper/types.ts +6 -0
  68. package/src/optimizers/pruner/cleanup.ts +121 -0
  69. package/src/optimizers/pruner/context.ts +46 -0
  70. package/src/optimizers/pruner/index.ts +45 -0
  71. package/src/optimizers/pruner/session.ts +34 -0
  72. package/src/optimizers/pruner/types.ts +18 -0
  73. package/src/permission/bash.ts +124 -0
  74. package/src/permission/command.ts +111 -0
  75. package/src/permission/components/prompt.ts +255 -0
  76. package/src/permission/components/rules-list.ts +342 -0
  77. package/src/permission/constants.ts +48 -0
  78. package/src/permission/helpers.ts +156 -0
  79. package/src/permission/index.ts +134 -0
  80. package/src/permission/pattern.ts +51 -0
  81. package/src/permission/piignore.ts +148 -0
  82. package/src/permission/precedence.ts +54 -0
  83. package/src/permission/resolution.ts +116 -0
  84. package/src/permission/storage.ts +142 -0
  85. package/src/permission/types.ts +57 -0
  86. package/src/questionnaire/component.ts +357 -0
  87. package/src/questionnaire/helpers.ts +220 -0
  88. package/src/questionnaire/index.ts +67 -0
  89. package/src/questionnaire/schemas.ts +50 -0
  90. package/src/questionnaire/types.ts +47 -0
  91. package/src/redactor/index.ts +34 -0
  92. package/src/redactor/patterns.ts +234 -0
  93. package/src/redactor/secrets.ts +113 -0
  94. package/src/subagent/helpers.ts +93 -0
  95. package/src/subagent/index.ts +81 -0
  96. package/src/subagent/storage.ts +100 -0
  97. package/src/subagent/subsession.ts +266 -0
  98. package/src/subagent/types.ts +83 -0
  99. package/src/subagent/validation.ts +100 -0
  100. package/src/ui/components/action-select-list.ts +165 -0
  101. package/src/ui/components/bash-mode.ts +281 -0
  102. package/src/ui/components/extended-select-list.ts +166 -0
  103. package/src/ui/components/form-field.ts +184 -0
  104. package/src/ui/components/form.ts +179 -0
  105. package/src/ui/components/frame.ts +60 -0
  106. package/src/ui/components/input-mode-indicator.ts +64 -0
  107. package/src/ui/components/keybound.ts +150 -0
  108. package/src/ui/components/lines.ts +27 -0
  109. package/src/ui/components/placeholder-input.ts +59 -0
  110. package/src/ui/components/scoped-input.ts +78 -0
  111. package/src/ui/components/scrollable-view.ts +155 -0
  112. package/src/ui/index.ts +40 -0
  113. package/src/utils.ts +206 -0
  114. package/src/web-tools/index.ts +15 -0
  115. package/src/web-tools/providers/brave.ts +55 -0
  116. package/src/web-tools/providers/firecrawl.ts +66 -0
  117. package/src/web-tools/providers/index.ts +50 -0
  118. package/src/web-tools/providers/jina.ts +48 -0
  119. package/src/web-tools/providers/native.ts +57 -0
  120. package/src/web-tools/providers/tavily.ts +56 -0
  121. package/src/web-tools/settings.ts +15 -0
  122. package/src/web-tools/web-fetch/helpers.ts +66 -0
  123. package/src/web-tools/web-fetch/index.ts +91 -0
  124. package/src/web-tools/web-fetch/parser.ts +51 -0
  125. package/src/web-tools/web-fetch/storage.ts +65 -0
  126. package/src/web-tools/web-fetch/types.ts +8 -0
  127. package/src/web-tools/web-login/helpers.ts +79 -0
  128. package/src/web-tools/web-login/index.ts +100 -0
  129. package/src/web-tools/web-login/types.ts +4 -0
  130. package/src/web-tools/web-search/helpers.ts +36 -0
  131. package/src/web-tools/web-search/index.ts +98 -0
  132. package/src/web-tools/web-search/types.ts +15 -0
@@ -0,0 +1,51 @@
1
+ import { bashToPattern } from "./bash.js";
2
+ import type { PermissiveToolName } from "./types.js";
3
+
4
+ export function filePathToPattern(path: string): string {
5
+ if (!path || path.endsWith("/")) return path;
6
+
7
+ const lastSlash = path.lastIndexOf("/");
8
+ const dir = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : "";
9
+ const filename = lastSlash >= 0 ? path.slice(lastSlash + 1) : path;
10
+
11
+ if (!filename) return path;
12
+
13
+ const depth = dir.split("/").filter(Boolean).length;
14
+
15
+ if (depth >= 3) return `${dir}**`;
16
+
17
+ if (filename.startsWith(".")) {
18
+ const extDot = filename.lastIndexOf(".");
19
+ return extDot > 0 ? `${dir}.*${filename.slice(extDot)}` : `${dir}.*`;
20
+ }
21
+
22
+ const extDot = filename.lastIndexOf(".");
23
+ return extDot > 0 ? `${dir}*${filename.slice(extDot)}` : `${dir}*`;
24
+ }
25
+
26
+ export function urlToPattern(url: string): string {
27
+ if (!url) return "";
28
+ try {
29
+ const { origin } = new URL(url);
30
+ return `${origin}/**`;
31
+ } catch {
32
+ return url;
33
+ }
34
+ }
35
+
36
+ export function toPattern(toolName: PermissiveToolName, input: string): string {
37
+ const firstLine = input.replace(/\r\n?/g, "\n").split("\n")[0] ?? "";
38
+ switch (toolName) {
39
+ case "read":
40
+ case "write":
41
+ case "edit":
42
+ case "grep":
43
+ return filePathToPattern(firstLine);
44
+ case "web_fetch":
45
+ return urlToPattern(firstLine);
46
+ case "bash":
47
+ return bashToPattern(input);
48
+ case "call_mcp_tool":
49
+ return input;
50
+ }
51
+ }
@@ -0,0 +1,148 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { getRelativePathInRoot } from "./resolution.js";
4
+ import { matchesPattern, specificity } from "./precedence.js";
5
+ import { isMissingFileError } from "../utils.js";
6
+ import type {
7
+ GrepToolCallEvent,
8
+ ReadToolCallEvent,
9
+ ToolCallEvent,
10
+ } from "@earendil-works/pi-coding-agent";
11
+
12
+ const PI_IGNORE_FILE = ".piignore";
13
+
14
+ interface PiIgnoreRule {
15
+ pattern: string;
16
+ matcher: string;
17
+ negated: boolean;
18
+ score: number;
19
+ order: number;
20
+ }
21
+
22
+ const piIgnoreCache: { contents: string; rules: PiIgnoreRule[] } = { contents: "", rules: [] };
23
+
24
+ function normalizePathToRoot(rawPath: string, rootPath: string): string | null {
25
+ const relativePath = getRelativePathInRoot(rawPath, rootPath);
26
+ if (relativePath === null) return null;
27
+
28
+ const posixPath = (relativePath === "" ? "." : relativePath).replace(/\\/g, "/");
29
+ if (posixPath === "/") return posixPath;
30
+
31
+ const trimmedPath = posixPath.replace(/\/+$/, "");
32
+ return trimmedPath || ".";
33
+ }
34
+
35
+ function normalizePiIgnorePattern(pattern: string): string | null {
36
+ let normalizedPattern = pattern.replace(/\\/g, "/");
37
+ const directoryOnly = normalizedPattern.endsWith("/");
38
+
39
+ normalizedPattern = normalizedPattern.replace(/^\/+/, "").replace(/\/+$/, "");
40
+ if (!normalizedPattern || normalizedPattern === ".") {
41
+ return null;
42
+ }
43
+ if (directoryOnly) {
44
+ return `${normalizedPattern}/**`;
45
+ }
46
+
47
+ return normalizedPattern;
48
+ }
49
+
50
+ function parsePiIgnoreRules(contents: string): PiIgnoreRule[] {
51
+ const rules: PiIgnoreRule[] = [];
52
+
53
+ for (const [order, pattern] of contents.split(/\r?\n/).entries()) {
54
+ if (!pattern || pattern.startsWith("#")) continue;
55
+
56
+ const negated = pattern.startsWith("!") && pattern !== "!";
57
+ const rawPattern = negated ? pattern.slice(1) : pattern;
58
+ const matcher = normalizePiIgnorePattern(rawPattern);
59
+ if (!matcher) continue;
60
+
61
+ rules.push({ pattern, matcher, negated, score: specificity(matcher), order });
62
+ }
63
+
64
+ return rules;
65
+ }
66
+
67
+ async function loadPiIgnoreRules(cwd: string): Promise<PiIgnoreRule[]> {
68
+ const piIgnorePath = resolve(cwd, PI_IGNORE_FILE);
69
+ let contents = "";
70
+
71
+ try {
72
+ contents = await readFile(piIgnorePath, "utf8");
73
+ } catch (error) {
74
+ if (isMissingFileError(error)) {
75
+ piIgnoreCache.contents = "";
76
+ piIgnoreCache.rules = [];
77
+ return [];
78
+ }
79
+ throw error;
80
+ }
81
+
82
+ if (piIgnoreCache.contents === contents) {
83
+ return piIgnoreCache.rules;
84
+ }
85
+
86
+ const rules = parsePiIgnoreRules(contents);
87
+ piIgnoreCache.contents = contents;
88
+ piIgnoreCache.rules = rules;
89
+ return rules;
90
+ }
91
+
92
+ function findBestPiIgnoreRule(
93
+ rules: PiIgnoreRule[],
94
+ rawPath: string,
95
+ cwd: string,
96
+ ): PiIgnoreRule | null {
97
+ const normalizedPath = normalizePathToRoot(rawPath, cwd);
98
+ if (!normalizedPath) return null;
99
+
100
+ let bestRule: PiIgnoreRule | null = null;
101
+
102
+ for (const rule of rules) {
103
+ if (!matchesPattern(normalizedPath, rule.matcher)) continue;
104
+
105
+ if (
106
+ bestRule === null ||
107
+ rule.score > bestRule.score ||
108
+ (rule.score === bestRule.score && rule.order > bestRule.order)
109
+ ) {
110
+ bestRule = rule;
111
+ }
112
+ }
113
+
114
+ return bestRule;
115
+ }
116
+
117
+ export async function resolvePiIgnorePathBlock(
118
+ cwd: string,
119
+ rawPath: string,
120
+ ): Promise<string | null> {
121
+ const rules = await loadPiIgnoreRules(cwd);
122
+ if (rules.length === 0) return null;
123
+
124
+ const matchedRule = findBestPiIgnoreRule(rules, rawPath, cwd);
125
+ if (!matchedRule || matchedRule.negated) {
126
+ return null;
127
+ }
128
+
129
+ return `Path blocked by .piignore rule "${matchedRule.pattern}"`;
130
+ }
131
+
132
+ export function getPiIgnoreInputs(event: ToolCallEvent): string[] {
133
+ switch (event.toolName) {
134
+ case "read":
135
+ case "write":
136
+ case "edit":
137
+ return [(event as ReadToolCallEvent).input.path];
138
+ case "grep": {
139
+ const grepEvent = event as GrepToolCallEvent;
140
+ const inputs: string[] = [];
141
+ if (grepEvent.input.path) inputs.push(grepEvent.input.path);
142
+ if (grepEvent.input.glob) inputs.push(grepEvent.input.glob);
143
+ return inputs;
144
+ }
145
+ default:
146
+ return [];
147
+ }
148
+ }
@@ -0,0 +1,54 @@
1
+ import pm from "picomatch";
2
+ import type { FileAccess } from "./types.js";
3
+
4
+ const GLOB_CHARS = /[*?[\]{}]/;
5
+
6
+ export function specificity(pattern: string): number {
7
+ return GLOB_CHARS.test(pattern) ? pattern.length : Infinity;
8
+ }
9
+
10
+ export function matchesPattern(input: string, pattern: string, bash = false): boolean {
11
+ if (pattern === input) return true;
12
+ if (!GLOB_CHARS.test(pattern)) return false;
13
+
14
+ try {
15
+ return pm(pattern, { dot: true, bash })(input);
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ export function findScopedPermission(
22
+ scopes: Array<Record<string, FileAccess | boolean>>,
23
+ input: string,
24
+ bash = false,
25
+ fileOp?: "read" | "write",
26
+ ): "allowed" | "blocked" | "ask" {
27
+ for (const rules of scopes) {
28
+ let best: { permission: "allowed" | "blocked"; score: number } | null = null;
29
+ for (const [pattern, value] of Object.entries(rules)) {
30
+ if (!matchesPattern(input, pattern, bash)) continue;
31
+
32
+ const permission =
33
+ typeof value === "boolean"
34
+ ? value
35
+ ? "allowed"
36
+ : "blocked"
37
+ : value === "write" || value === fileOp
38
+ ? "allowed"
39
+ : "blocked";
40
+ const score = specificity(pattern);
41
+
42
+ if (
43
+ best === null ||
44
+ score > best.score ||
45
+ (score === best.score && permission === "blocked")
46
+ ) {
47
+ best = { permission, score };
48
+ }
49
+ }
50
+ if (best) return best.permission;
51
+ }
52
+
53
+ return "ask";
54
+ }
@@ -0,0 +1,116 @@
1
+ import { homedir } from "node:os";
2
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
3
+ import type { AgentMeta, AgentMode } from "../agent/types.js";
4
+ import { readRules } from "./storage.js";
5
+ import type { Category, FileAccess, PermissionRule, PermissionCheck } from "./types.js";
6
+ import { getPiPath } from "../utils.js";
7
+ import { findSubsession } from "../subagent/storage.js";
8
+ import { findScopedPermission, matchesPattern } from "./precedence.js";
9
+ import { extractOpAndPath } from "./helpers.js";
10
+
11
+ function getSchemaRules(
12
+ schema: PermissionRule | undefined,
13
+ category: Category,
14
+ ): Record<string, FileAccess | boolean> {
15
+ if (!schema) return {};
16
+ if (category === "file") return schema.file ?? {};
17
+ if (category === "web") return schema.web ?? {};
18
+ if (category === "mcp") return schema.mcp ?? {};
19
+ return schema.bash ?? {};
20
+ }
21
+
22
+ function getBlockedRules(schema: PermissionRule): PermissionRule {
23
+ const keys = ["file", "web", "bash", "mcp"] as const;
24
+ const rules: PermissionRule = {};
25
+ for (const key of keys) {
26
+ rules[key] = Object.fromEntries(
27
+ Object.entries(schema[key] ?? {}).filter(([, access]) => !access || access === "blocked"),
28
+ );
29
+ }
30
+ return rules;
31
+ }
32
+
33
+ function isAllowedByPattern(raw: string, allowList?: string[], bash?: boolean): boolean {
34
+ return Boolean(!allowList || allowList.some((pattern) => matchesPattern(raw, pattern, bash)));
35
+ }
36
+
37
+ export function checkAgentRules(meta: AgentMeta, check: PermissionCheck): boolean {
38
+ const { category, extracted } = check;
39
+ if (category === "bash") {
40
+ return extracted.every((item) => isAllowedByPattern(item, meta.bash, true));
41
+ }
42
+ if (category === "file") {
43
+ return extracted.every((item) => {
44
+ const [op, path] = extractOpAndPath(item);
45
+ return isAllowedByPattern(path, meta[`files.${op}`]);
46
+ });
47
+ }
48
+ if (category === "mcp") {
49
+ return extracted.every((item) => isAllowedByPattern(item, meta.mcp_tools));
50
+ }
51
+ return true;
52
+ }
53
+
54
+ export function expandFilePath(path: string, cwd: string): string | null {
55
+ if (!path) return null;
56
+ if (path === "~") return homedir();
57
+ if (path.startsWith("~/")) {
58
+ return resolve(homedir(), path.slice(2));
59
+ }
60
+ return resolve(cwd, path);
61
+ }
62
+
63
+ export function getRelativePathInRoot(path: string, root: string): string | null {
64
+ const resolvedPath = expandFilePath(path, root);
65
+ if (!resolvedPath) return null;
66
+
67
+ const relativePath = relative(root, resolvedPath);
68
+ if (relativePath !== "" && (relativePath.startsWith("..") || isAbsolute(relativePath))) {
69
+ return null;
70
+ }
71
+
72
+ return relativePath;
73
+ }
74
+
75
+ export async function resolvePermission(cwd: string, check: PermissionCheck, mode: AgentMode) {
76
+ let shouldAsk = false;
77
+ const { category, extracted, sessionId } = check;
78
+ const [local, global, subsession] = await Promise.all([
79
+ readRules(cwd),
80
+ readRules(),
81
+ findSubsession(cwd, sessionId),
82
+ ]);
83
+
84
+ const scopes: Array<PermissionRule | undefined> = [local[sessionId]];
85
+ if (subsession?.pid && subsession.pid !== sessionId) {
86
+ scopes.push(local[subsession.pid]);
87
+ }
88
+ scopes.push(local.project);
89
+ scopes.push(mode === "restricted" ? getBlockedRules(global) : global);
90
+
91
+ for (const item of extracted) {
92
+ const [fileOp, normalized] = category === "file" ? extractOpAndPath(item) : [undefined, item];
93
+ let permission: "allowed" | "blocked" | "ask" = findScopedPermission(
94
+ scopes.map((schema) => getSchemaRules(schema, category)),
95
+ normalized,
96
+ category === "bash",
97
+ fileOp,
98
+ );
99
+
100
+ if (
101
+ category === "file" &&
102
+ permission === "ask" &&
103
+ (mode !== "restricted" || fileOp !== "write")
104
+ ) {
105
+ const inAllowedDir =
106
+ getRelativePathInRoot(normalized, cwd) !== null ||
107
+ getRelativePathInRoot(normalized, dirname(getPiPath("settings"))) !== null;
108
+ permission = inAllowedDir ? "allowed" : "ask";
109
+ }
110
+
111
+ if (permission === "blocked") return "blocked";
112
+ if (permission === "ask") shouldAsk = true;
113
+ }
114
+
115
+ return shouldAsk ? "ask" : "allowed";
116
+ }
@@ -0,0 +1,142 @@
1
+ import { readJson, writeJson } from "../utils.js";
2
+ import type {
3
+ Category,
4
+ GroupedDisplayRules,
5
+ FileAccess,
6
+ PermissionRule,
7
+ Scope,
8
+ DisplayRule,
9
+ } from "./types.js";
10
+ import { getPiPath } from "../utils.js";
11
+ import { CATEGORIES } from "./constants.js";
12
+ import type { AgentMode, SettingsSchema } from "../agent/types.js";
13
+
14
+ interface LocalSchema {
15
+ project?: PermissionRule;
16
+ [sessionId: string]: PermissionRule | undefined;
17
+ }
18
+
19
+ export async function writeRules(data: PermissionRule | LocalSchema, cwd: string = "") {
20
+ return writeJson(getPiPath("permissions", cwd), data);
21
+ }
22
+
23
+ export function readRules(cwd: string): Promise<LocalSchema>;
24
+ export function readRules(): Promise<PermissionRule>;
25
+ export function readRules(cwd: string = ""): Promise<LocalSchema | PermissionRule> {
26
+ return readJson<LocalSchema | PermissionRule>(getPiPath("permissions", cwd), {});
27
+ }
28
+
29
+ export async function readAgentMode(): Promise<AgentMode> {
30
+ const settings = await readJson<SettingsSchema>(getPiPath("settings"), {});
31
+ return settings.agent?.mode ?? "assistant";
32
+ }
33
+
34
+ export async function writeAgentMode(agentMode: AgentMode) {
35
+ const settingsPath = getPiPath("settings");
36
+ const settings = await readJson<SettingsSchema>(settingsPath, {});
37
+ settings.agent = { ...settings.agent, mode: agentMode };
38
+ await writeJson(settingsPath, settings);
39
+ }
40
+
41
+ async function mutateRules(
42
+ cwd: string,
43
+ sessionId: string,
44
+ scope: Scope,
45
+ category: Category,
46
+ mutate: (rules: Record<string, FileAccess | boolean>) => void,
47
+ ) {
48
+ if (scope === "always") {
49
+ const global = await readRules();
50
+ const rules = { ...global[category] };
51
+
52
+ mutate(rules);
53
+ global[category] = rules;
54
+ await writeRules(global);
55
+ return;
56
+ }
57
+
58
+ const local = await readRules(cwd);
59
+ const scopeKey = scope === "project" ? "project" : sessionId;
60
+ const scopeSchema: PermissionRule = local[scopeKey] ?? {};
61
+ const rules = { ...scopeSchema[category] };
62
+
63
+ mutate(rules);
64
+ scopeSchema[category] = rules;
65
+ local[scopeKey] = scopeSchema;
66
+ await writeRules(local, cwd);
67
+ }
68
+
69
+ export async function addRules(
70
+ cwd: string,
71
+ sessionId: string,
72
+ scope: Scope,
73
+ category: Category,
74
+ rules: Map<string, FileAccess | boolean>,
75
+ ) {
76
+ await mutateRules(cwd, sessionId, scope, category, (rulesSet) => {
77
+ for (const [pattern, perm] of rules) {
78
+ rulesSet[pattern] = perm;
79
+ }
80
+ });
81
+ }
82
+
83
+ export async function removeRule(
84
+ cwd: string,
85
+ sessionId: string,
86
+ scope: Scope,
87
+ category: Category,
88
+ pattern: string,
89
+ ) {
90
+ await mutateRules(cwd, sessionId, scope, category, (rules) => {
91
+ delete rules[pattern];
92
+ });
93
+ }
94
+
95
+ export async function toggleRule(
96
+ cwd: string,
97
+ sessionId: string,
98
+ scope: Scope,
99
+ category: Category,
100
+ pattern: string,
101
+ ) {
102
+ await mutateRules(cwd, sessionId, scope, category, (rules) => {
103
+ if (category === "file") {
104
+ const cycle: FileAccess[] = ["write", "read", "blocked"];
105
+ const currentValue = rules[pattern] as FileAccess | undefined;
106
+ const cycleIndex = cycle.indexOf(currentValue as FileAccess);
107
+ rules[pattern] = cycle[(cycleIndex + 1) % cycle.length]!;
108
+ return;
109
+ }
110
+
111
+ const currentValue = rules[pattern] as boolean | undefined;
112
+ rules[pattern] = !currentValue;
113
+ });
114
+ }
115
+
116
+ export async function getRulesForDisplay(
117
+ cwd: string,
118
+ sessionId: string,
119
+ ): Promise<GroupedDisplayRules> {
120
+ const [local, global] = await Promise.all([readRules(cwd), readRules()]);
121
+ const rules = Object.fromEntries(
122
+ CATEGORIES.map((cat) => [cat, [] as DisplayRule[]]),
123
+ ) as GroupedDisplayRules;
124
+
125
+ const addFromSchema = (schema: PermissionRule | undefined, scope: Scope) => {
126
+ if (!schema) return;
127
+
128
+ for (const category of CATEGORIES) {
129
+ if (!rules[category]) return;
130
+ const categoryRules = schema[category] ?? {};
131
+ for (const [pattern, value] of Object.entries(categoryRules)) {
132
+ rules[category].push({ pattern, value: value as FileAccess | boolean, scope, category });
133
+ }
134
+ }
135
+ };
136
+
137
+ addFromSchema(local[sessionId], "session");
138
+ addFromSchema(local.project, "project");
139
+ addFromSchema(global, "always");
140
+
141
+ return rules;
142
+ }
@@ -0,0 +1,57 @@
1
+ import { SCOPES, PERMISSIVE_TOOLS } from "./constants.js";
2
+
3
+ type FileOp = "read" | "write";
4
+
5
+ export type PermissiveToolName = keyof typeof PERMISSIVE_TOOLS;
6
+ export type Scope = (typeof SCOPES)[number];
7
+ export type Category = (typeof PERMISSIVE_TOOLS)[PermissiveToolName];
8
+ export type FileAccess = FileOp | "blocked";
9
+
10
+ export interface PermissionRule {
11
+ file?: Record<string, FileAccess>;
12
+ web?: Record<string, boolean>;
13
+ bash?: Record<string, boolean>;
14
+ mcp?: Record<string, boolean>;
15
+ }
16
+
17
+ export interface PermissionCheck {
18
+ sessionId: string;
19
+ toolName: PermissiveToolName;
20
+ category: Category;
21
+ raw: string;
22
+ extracted: string[];
23
+ purpose: string;
24
+ uncertainty?: string;
25
+ }
26
+
27
+ export interface PromptDecision {
28
+ allowed: boolean;
29
+ amended?: string;
30
+ }
31
+
32
+ export interface DisplayRule {
33
+ category: Category;
34
+ pattern: string;
35
+ value: FileAccess | boolean;
36
+ scope: Scope;
37
+ }
38
+
39
+ export type GroupedDisplayRules = {
40
+ file: DisplayRule[];
41
+ web: DisplayRule[];
42
+ bash: DisplayRule[];
43
+ mcp: DisplayRule[];
44
+ };
45
+
46
+ export type PromptOptions = {
47
+ label: string;
48
+ value: PromptDecision;
49
+ persists: boolean;
50
+ separator: string;
51
+ defaultText?: string;
52
+ };
53
+
54
+ export interface BashCommand {
55
+ text: string;
56
+ unresolved: boolean;
57
+ }