@pugi/cli 0.1.0-alpha.10

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/run.js +2 -0
  4. package/dist/commands/jobs.js +245 -0
  5. package/dist/core/agents/loader.js +104 -0
  6. package/dist/core/agents/registry.js +69 -0
  7. package/dist/core/auto-open-browser.js +128 -0
  8. package/dist/core/bash-classifier.js +1001 -0
  9. package/dist/core/clipboard.js +70 -0
  10. package/dist/core/context/builder.js +114 -0
  11. package/dist/core/context/compaction-events.js +99 -0
  12. package/dist/core/context/compaction.js +602 -0
  13. package/dist/core/context/invariants.js +250 -0
  14. package/dist/core/context/markdown-loader.js +270 -0
  15. package/dist/core/credentials.js +355 -0
  16. package/dist/core/engine/adapter-runner.js +8 -0
  17. package/dist/core/engine/anvil-client.js +156 -0
  18. package/dist/core/engine/compaction-hook.js +154 -0
  19. package/dist/core/engine/index.js +12 -0
  20. package/dist/core/engine/native-pugi.js +369 -0
  21. package/dist/core/engine/noop.js +27 -0
  22. package/dist/core/engine/prompts.js +118 -0
  23. package/dist/core/engine/tool-bridge.js +313 -0
  24. package/dist/core/file-cache.js +29 -0
  25. package/dist/core/hooks.js +415 -0
  26. package/dist/core/index-store.js +260 -0
  27. package/dist/core/jobs/registry.js +462 -0
  28. package/dist/core/mcp/client.js +316 -0
  29. package/dist/core/mcp/registry.js +171 -0
  30. package/dist/core/mcp/trust.js +91 -0
  31. package/dist/core/path-security.js +63 -0
  32. package/dist/core/permission.js +309 -0
  33. package/dist/core/repl/cap-warning.js +91 -0
  34. package/dist/core/repl/clipboard-read.js +174 -0
  35. package/dist/core/repl/history-search.js +175 -0
  36. package/dist/core/repl/history.js +172 -0
  37. package/dist/core/repl/kill-ring.js +138 -0
  38. package/dist/core/repl/session.js +618 -0
  39. package/dist/core/repl/slash-commands.js +227 -0
  40. package/dist/core/repl/workspace-context.js +113 -0
  41. package/dist/core/session.js +258 -0
  42. package/dist/core/settings.js +59 -0
  43. package/dist/core/skills/loader.js +454 -0
  44. package/dist/core/skills/sources.js +480 -0
  45. package/dist/core/skills/trust.js +172 -0
  46. package/dist/core/subagents/dispatcher.js +258 -0
  47. package/dist/core/subagents/index.js +26 -0
  48. package/dist/core/subagents/spawn.js +86 -0
  49. package/dist/core/trust.js +109 -0
  50. package/dist/index.js +8 -0
  51. package/dist/runtime/cli.js +3405 -0
  52. package/dist/runtime/commands/agents.js +385 -0
  53. package/dist/runtime/commands/budget.js +192 -0
  54. package/dist/runtime/commands/config.js +231 -0
  55. package/dist/runtime/commands/privacy.js +107 -0
  56. package/dist/runtime/commands/skills.js +401 -0
  57. package/dist/runtime/commands/undo.js +329 -0
  58. package/dist/runtime/update-check.js +294 -0
  59. package/dist/tools/bash.js +660 -0
  60. package/dist/tools/file-tools.js +346 -0
  61. package/dist/tools/registry.js +25 -0
  62. package/dist/tools/web-fetch.js +535 -0
  63. package/dist/tui/agent-tree.js +66 -0
  64. package/dist/tui/conversation-pane.js +45 -0
  65. package/dist/tui/device-flow.js +142 -0
  66. package/dist/tui/input-box.js +474 -0
  67. package/dist/tui/login-picker.js +69 -0
  68. package/dist/tui/render.js +125 -0
  69. package/dist/tui/repl-render.js +240 -0
  70. package/dist/tui/repl-splash-art.js +64 -0
  71. package/dist/tui/repl-splash.js +111 -0
  72. package/dist/tui/repl.js +214 -0
  73. package/dist/tui/slash-palette.js +106 -0
  74. package/dist/tui/splash-data.js +61 -0
  75. package/dist/tui/splash.js +31 -0
  76. package/dist/tui/status-bar.js +71 -0
  77. package/dist/tui/update-banner.js +8 -0
  78. package/dist/tui/workspace-context.js +105 -0
  79. package/package.json +71 -0
@@ -0,0 +1,346 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, relative } from 'node:path';
4
+ import { globSync } from 'node:fs';
5
+ import { decidePermission } from '../core/permission.js';
6
+ import { createReadRecord, hashContent } from '../core/file-cache.js';
7
+ import { resolveWorkspacePath } from '../core/path-security.js';
8
+ import { recordFileMutation, recordToolCall, recordToolResult } from '../core/session.js';
9
+ /**
10
+ * Re-check the permission decision against the *resolved* real path so
11
+ * a workspace-local symlink (`alias -> .env`) cannot bypass the protected
12
+ * basename check. The first `decidePermission` call sees only the user
13
+ * input (`alias`); this second call sees the realpath relative to root
14
+ * (`.env`), which `protectedTargetReason` recognises.
15
+ *
16
+ * Returns the resolved absolute path. Throws when the resolved path is
17
+ * gated by anything other than `allow`.
18
+ */
19
+ function permissionGatedResolve(ctx, inputPath, action, toolName) {
20
+ const resolved = resolveWorkspacePath(ctx.root, inputPath);
21
+ let realPath;
22
+ try {
23
+ realPath = realpathSync.native(resolved);
24
+ }
25
+ catch (error) {
26
+ // For writes to a file that does not yet exist there is no symlink
27
+ // to follow; fall back to the resolved path so the workspace check
28
+ // already done in `resolveWorkspacePath` is the only gate.
29
+ const code = error.code;
30
+ if (code === 'ENOENT' || code === 'ENOTDIR')
31
+ return resolved;
32
+ throw error;
33
+ }
34
+ if (realPath === resolved)
35
+ return resolved;
36
+ const realRelative = relative(ctx.root, realPath);
37
+ const realDecision = decidePermission({ tool: toolName, kind: action, target: realRelative }, ctx.settings, ctx.root);
38
+ if (realDecision.decision !== 'allow') {
39
+ throw new Error(`Permission ${realDecision.decision} for ${action} ${realRelative} (via symlink ${inputPath}): ${realDecision.reason}`);
40
+ }
41
+ return realPath;
42
+ }
43
+ export function readTool(ctx, path) {
44
+ const toolCallId = recordToolCall(ctx.session, 'read', path);
45
+ const decision = decidePermission({ tool: 'read', kind: 'read', target: path }, ctx.settings, ctx.root);
46
+ if (decision.decision !== 'allow') {
47
+ const reason = `Permission ${decision.decision} for read ${path}: ${decision.reason}`;
48
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
49
+ throw new Error(reason);
50
+ }
51
+ let resolved;
52
+ try {
53
+ resolved = permissionGatedResolve(ctx, path, 'read', 'read');
54
+ }
55
+ catch (error) {
56
+ const reason = error.message;
57
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
58
+ throw error;
59
+ }
60
+ const content = readFileSync(resolved, 'utf8');
61
+ ctx.readCache.set(createReadRecord(ctx.root, path, content, 'read_tool'));
62
+ recordToolResult(ctx.session, toolCallId, 'success', `Read ${path}`);
63
+ return content;
64
+ }
65
+ export function writeTool(ctx, path, content) {
66
+ const toolCallId = recordToolCall(ctx.session, 'write', path);
67
+ const decision = decidePermission({ tool: 'write', kind: 'edit', target: path }, ctx.settings, ctx.root);
68
+ if (decision.decision !== 'allow') {
69
+ const reason = `Permission ${decision.decision} for write ${path}: ${decision.reason}`;
70
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
71
+ throw new Error(reason);
72
+ }
73
+ let resolved;
74
+ try {
75
+ resolved = permissionGatedResolve(ctx, path, 'edit', 'write');
76
+ }
77
+ catch (error) {
78
+ const reason = error.message;
79
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
80
+ throw error;
81
+ }
82
+ const existed = existsSync(resolved);
83
+ const before = existed ? readFileSync(resolved, 'utf8') : undefined;
84
+ const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
85
+ writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
86
+ renameSync(tmp, resolved);
87
+ recordFileMutation(ctx.session, {
88
+ toolCallId,
89
+ path,
90
+ operation: existed ? 'update' : 'create',
91
+ beforeHash: before ? hashContent(before) : undefined,
92
+ afterHash: hashContent(content),
93
+ });
94
+ recordToolResult(ctx.session, toolCallId, 'success', `${existed ? 'Updated' : 'Created'} ${path}`);
95
+ }
96
+ export function editTool(ctx, path, oldString, newString) {
97
+ const toolCallId = recordToolCall(ctx.session, 'edit', path);
98
+ const decision = decidePermission({ tool: 'edit', kind: 'edit', target: path }, ctx.settings, ctx.root);
99
+ if (decision.decision !== 'allow') {
100
+ const reason = `Permission ${decision.decision} for edit ${path}: ${decision.reason}`;
101
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
102
+ throw new Error(reason);
103
+ }
104
+ const readRecord = ctx.readCache.get(ctx.root, path);
105
+ if (!readRecord) {
106
+ throw new Error(`Cannot edit ${path}: file must be read first`);
107
+ }
108
+ let resolved;
109
+ try {
110
+ resolved = permissionGatedResolve(ctx, path, 'edit', 'edit');
111
+ }
112
+ catch (error) {
113
+ const reason = error.message;
114
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
115
+ throw error;
116
+ }
117
+ const before = readFileSync(resolved, 'utf8');
118
+ const currentHash = hashContent(before);
119
+ if (currentHash !== readRecord.sha256) {
120
+ throw new Error(`Cannot edit ${path}: file changed since last read`);
121
+ }
122
+ const matches = before.split(oldString).length - 1;
123
+ if (matches === 0)
124
+ throw new Error(`Cannot edit ${path}: oldString not found`);
125
+ if (matches > 1)
126
+ throw new Error(`Cannot edit ${path}: oldString is not unique`);
127
+ const after = before.replace(oldString, newString);
128
+ const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
129
+ writeFileSync(tmp, after, { encoding: 'utf8', mode: 0o600 });
130
+ renameSync(tmp, resolved);
131
+ ctx.readCache.set(createReadRecord(ctx.root, path, after, 'read_tool'));
132
+ recordFileMutation(ctx.session, {
133
+ toolCallId,
134
+ path,
135
+ operation: 'update',
136
+ beforeHash: currentHash,
137
+ afterHash: hashContent(after),
138
+ });
139
+ recordToolResult(ctx.session, toolCallId, 'success', `Edited ${path}`);
140
+ }
141
+ export function globTool(ctx, pattern) {
142
+ const toolCallId = recordToolCall(ctx.session, 'glob', pattern);
143
+ // Pugi globs are workspace-scoped. Reject any pattern that could enumerate
144
+ // outside the workspace:
145
+ // 1. absolute paths (`/etc/**/*`) — globSync resolves these against `/`
146
+ // regardless of `cwd`, so they fan out outside the repo.
147
+ // 2. `..` as a path SEGMENT (`../*`, `src/../etc`) — parent traversal.
148
+ // A substring check would over-reject legitimate names like
149
+ // `src/v1..v2/*` so we split on `/` instead.
150
+ if (isAbsolute(pattern)) {
151
+ const reason = `Absolute glob patterns are not allowed: ${pattern}`;
152
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
153
+ throw new Error(reason);
154
+ }
155
+ if (pattern.split('/').some((segment) => segment === '..')) {
156
+ const reason = `Glob pattern escapes workspace via '..' segment: ${pattern}`;
157
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
158
+ throw new Error(reason);
159
+ }
160
+ const results = globSync(pattern, {
161
+ cwd: ctx.root,
162
+ withFileTypes: false,
163
+ exclude: ['node_modules/**', 'dist/**', '.git/**', '.pugi/**'],
164
+ })
165
+ .map((entry) => String(entry))
166
+ .slice(0, 500);
167
+ recordToolResult(ctx.session, toolCallId, 'success', `Glob matched ${results.length} paths`);
168
+ return results;
169
+ }
170
+ export function grepTool(ctx, query) {
171
+ const toolCallId = recordToolCall(ctx.session, 'grep', query);
172
+ const files = globTool(ctx, '**/*').filter((path) => !path.endsWith('/'));
173
+ const matches = [];
174
+ for (const path of files) {
175
+ if (matches.length >= 200)
176
+ break;
177
+ // Permission gate every file read individually — grep used to bypass
178
+ // `decidePermission` and could surface lines from protected files
179
+ // (.env, *.sql, *.pem, ~/.ssh/**) when invoked from a directory walk.
180
+ const decision = decidePermission({ tool: 'grep', kind: 'read', target: path }, ctx.settings, ctx.root);
181
+ if (decision.decision !== 'allow')
182
+ continue;
183
+ let fullPath;
184
+ try {
185
+ // `permissionGatedResolve` follows symlinks and re-checks the
186
+ // realpath against the permission rules. Without this an attacker
187
+ // could plant `alias -> .env` inside the workspace and recover
188
+ // secrets through `pugi explain .` because the initial decision
189
+ // only saw the unprotected basename `alias`.
190
+ fullPath = permissionGatedResolve(ctx, path, 'read', 'grep');
191
+ }
192
+ catch {
193
+ continue;
194
+ }
195
+ if (dirname(fullPath).includes('node_modules'))
196
+ continue;
197
+ try {
198
+ const lines = readFileSync(fullPath, 'utf8').split('\n');
199
+ lines.forEach((text, index) => {
200
+ if (matches.length < 200 && text.includes(query)) {
201
+ matches.push({ path: relative(ctx.root, fullPath), line: index + 1, text });
202
+ }
203
+ });
204
+ }
205
+ catch {
206
+ // Binary or unreadable files are ignored by the scaffolded grep tool.
207
+ }
208
+ }
209
+ recordToolResult(ctx.session, toolCallId, 'success', `Grep matched ${matches.length} lines`);
210
+ return matches;
211
+ }
212
+ /**
213
+ * Workspace-scoped bash tool. Sized for the M1 engine adapter:
214
+ * - Runs through `/bin/sh -c <command>` so the model can use pipes,
215
+ * redirection, and shell builtins (`ls | wc -l`, `git status`).
216
+ * - `cwd` is pinned to the workspace root so a stray `cd /` cannot
217
+ * leak commands outside the repo (the child process inherits root
218
+ * filesystem visibility — destructive patterns are blocked by
219
+ * `decidePermission`, not by chroot).
220
+ * - Output capped at 64KB combined stdout/stderr to keep the
221
+ * transcript bounded; the model gets the head + a `(...truncated)`
222
+ * marker if the cap fires.
223
+ * - 30s wall-clock timeout. The engine loop's per-tool error path
224
+ * surfaces the timeout to the model so it can retry with a narrower
225
+ * command or give up.
226
+ *
227
+ * Permission gating: `kind: 'bash'`. The CLI's permission module already
228
+ * hard-denies the destructive-patterns list (rm -rf /, DROP DATABASE,
229
+ * etc) regardless of mode. Plan-mode callers MUST gate the bash tool
230
+ * out before it reaches the registry — `engine-tools.ts` does this.
231
+ */
232
+ export const BASH_OUTPUT_CAP = 64 * 1024;
233
+ export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
234
+ // Child-process stdio buffer — large enough that the model-facing
235
+ // truncation cap (`BASH_OUTPUT_CAP`) is always the gate, never the
236
+ // child's internal buffer. Code Reviewer P2 retro 2026-05-23 flagged
237
+ // `BASH_OUTPUT_CAP * 2` as too tight: real builds (`pnpm build`,
238
+ // `tsc --noEmit`) routinely exceed 128 KB combined and the model
239
+ // then saw a fatal `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` instead of a
240
+ // graceful `(...truncated at N bytes)` tail.
241
+ export const BASH_CHILD_MAXBUFFER = 10 * 1024 * 1024;
242
+ export function bashTool(ctx, command, options = {}) {
243
+ const toolCallId = recordToolCall(ctx.session, 'bash', command);
244
+ const decision = decidePermission({ tool: 'bash', kind: 'bash', target: command }, ctx.settings, ctx.root);
245
+ if (decision.decision !== 'allow') {
246
+ const reason = `Permission ${decision.decision} for bash: ${decision.reason}`;
247
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
248
+ throw new Error(reason);
249
+ }
250
+ // `/bin/sh -c` is portable enough for the M1 alpha; downstream
251
+ // operators on hosts without /bin/sh can override via $SHELL, but
252
+ // that is an explicit opt-in for now.
253
+ //
254
+ // Env sanitisation strategy: build the child env from an explicit
255
+ // allow-list rather than inheriting `process.env` and trying to
256
+ // strip secrets after the fact. Code Reviewer P1 2026-05-23 flagged
257
+ // that the deny-list approach missed ANTHROPIC_API_KEY / GH_TOKEN
258
+ // / AWS_SECRET_ACCESS_KEY / DATABASE_URL / arbitrary *_TOKEN /
259
+ // *_SECRET / *_KEY variables — every CI agent rotation would risk
260
+ // leaking a new secret name. Allow-listed PATH / HOME / USER /
261
+ // SHELL / LANG / LC_* / TERM / TZ + Pugi-internal PUGI_ROOT for
262
+ // tools that need it. Any other env variable is invisible to the
263
+ // child process.
264
+ const childEnv = {};
265
+ const SAFE_ENV_ALLOW = new Set([
266
+ 'PATH',
267
+ 'HOME',
268
+ 'USER',
269
+ 'LOGNAME',
270
+ 'SHELL',
271
+ 'LANG',
272
+ 'TZ',
273
+ 'TERM',
274
+ 'PWD',
275
+ ]);
276
+ for (const [key, value] of Object.entries(process.env)) {
277
+ if (value === undefined)
278
+ continue;
279
+ if (SAFE_ENV_ALLOW.has(key) || key.startsWith('LC_')) {
280
+ childEnv[key] = value;
281
+ }
282
+ }
283
+ const timeoutMs = options.timeoutMs ?? BASH_DEFAULT_TIMEOUT_MS;
284
+ // `spawnSync` (vs `execFileSync`) captures stdout AND stderr on
285
+ // BOTH success and failure paths. Code Reviewer P1 2026-05-23:
286
+ // `execFileSync` returns only stdout on exit 0, silently dropping
287
+ // stderr output from `tsc`, `eslint`, `pytest`, etc. — the model
288
+ // would see `(no output)` for successful runs that emitted real
289
+ // warnings.
290
+ //
291
+ // maxBuffer is generous (10 MB) so the child process is never the
292
+ // truncation gate — the post-hoc `.slice(0, BASH_OUTPUT_CAP)` below
293
+ // is the single source of truth for what the model sees. Code
294
+ // Reviewer P2 retro 2026-05-23: the previous `BASH_OUTPUT_CAP * 2`
295
+ // (128 KB) would hard-throw `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`
296
+ // on noisy commands (`pnpm build`, `tsc --noEmit` on the whole
297
+ // monorepo) instead of returning the truncated head.
298
+ const result = spawnSync('/bin/sh', ['-c', command], {
299
+ cwd: ctx.root,
300
+ env: childEnv,
301
+ encoding: 'utf8',
302
+ stdio: ['ignore', 'pipe', 'pipe'],
303
+ timeout: timeoutMs,
304
+ maxBuffer: BASH_CHILD_MAXBUFFER,
305
+ });
306
+ if (result.error) {
307
+ const err = result.error;
308
+ if (err.code === 'ETIMEDOUT' || result.signal === 'SIGTERM') {
309
+ const reason = `bash command timed out after ${timeoutMs}ms`;
310
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
311
+ throw new Error(reason);
312
+ }
313
+ if (err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
314
+ // maxBuffer overflow — surface as truncated rather than an
315
+ // opaque Node internal code so the model sees the same
316
+ // truncation marker it gets on stdout/stderr cap hits. With the
317
+ // post-Code-Reviewer-P2-retro-2026-05-23 maxBuffer at 10 MB
318
+ // this branch only fires on truly pathological output (>10 MB
319
+ // single command).
320
+ const reason = `bash output exceeded ${BASH_CHILD_MAXBUFFER} byte child-process buffer`;
321
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
322
+ throw new Error(reason);
323
+ }
324
+ const reason = `bash invocation failed: ${err.message ?? String(err)}`;
325
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
326
+ throw new Error(reason);
327
+ }
328
+ const stdout = (result.stdout ?? '').toString();
329
+ const stderr = (result.stderr ?? '').toString();
330
+ const truncatedOut = stdout.length > BASH_OUTPUT_CAP;
331
+ const truncatedErr = stderr.length > BASH_OUTPUT_CAP;
332
+ const truncated = truncatedOut || truncatedErr;
333
+ const out = truncatedOut
334
+ ? `${stdout.slice(0, BASH_OUTPUT_CAP)}\n(...truncated at ${BASH_OUTPUT_CAP} bytes)`
335
+ : stdout;
336
+ const err = truncatedErr
337
+ ? `${stderr.slice(0, BASH_OUTPUT_CAP)}\n(...truncated at ${BASH_OUTPUT_CAP} bytes)`
338
+ : stderr;
339
+ const exitCode = result.status ?? 1;
340
+ // Non-zero exit is a normal outcome (e.g. `grep` finding no match,
341
+ // `test -f` returning 1). Surface it as a success at the audit
342
+ // layer; the engine loop feeds the exit code back to the model.
343
+ recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${exitCode} stdout=${stdout.length} stderr=${stderr.length}`);
344
+ return { stdout: out, stderr: err, exitCode, truncated };
345
+ }
346
+ //# sourceMappingURL=file-tools.js.map
@@ -0,0 +1,25 @@
1
+ const registry = [
2
+ { name: 'bash', permission: 'bash', risk: 'high', concurrencySafe: false, m1: true },
3
+ { name: 'edit', permission: 'edit', risk: 'medium', concurrencySafe: false, m1: true },
4
+ { name: 'glob', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
5
+ { name: 'grep', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
6
+ { name: 'question', permission: 'none', risk: 'low', concurrencySafe: false, m1: true },
7
+ { name: 'read', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
8
+ { name: 'skill', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
9
+ { name: 'task_create', permission: 'none', risk: 'low', concurrencySafe: false, m1: true },
10
+ { name: 'task_get', permission: 'none', risk: 'low', concurrencySafe: true, m1: true },
11
+ { name: 'task_list', permission: 'none', risk: 'low', concurrencySafe: true, m1: true },
12
+ { name: 'task_update', permission: 'none', risk: 'low', concurrencySafe: false, m1: true },
13
+ { name: 'web_fetch', permission: 'network', risk: 'medium', concurrencySafe: true, m1: true },
14
+ { name: 'write', permission: 'edit', risk: 'medium', concurrencySafe: false, m1: true },
15
+ ];
16
+ export const toolRegistry = registry.sort((a, b) => a.name.localeCompare(b.name));
17
+ export function toolSchemaBundleHashInput() {
18
+ return JSON.stringify(toolRegistry.map((tool) => ({
19
+ name: tool.name,
20
+ permission: tool.permission,
21
+ risk: tool.risk,
22
+ concurrencySafe: tool.concurrencySafe,
23
+ })));
24
+ }
25
+ //# sourceMappingURL=registry.js.map