@pugi/cli 0.1.0-alpha.3
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/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/run.js +2 -0
- package/dist/core/credentials.js +355 -0
- package/dist/core/engine/adapter-runner.js +8 -0
- package/dist/core/engine/anvil-client.js +156 -0
- package/dist/core/engine/index.js +7 -0
- package/dist/core/engine/native-pugi.js +369 -0
- package/dist/core/engine/noop.js +27 -0
- package/dist/core/engine/prompts.js +76 -0
- package/dist/core/engine/tool-bridge.js +215 -0
- package/dist/core/file-cache.js +29 -0
- package/dist/core/index-store.js +260 -0
- package/dist/core/path-security.js +63 -0
- package/dist/core/permission.js +204 -0
- package/dist/core/session.js +90 -0
- package/dist/core/settings.js +46 -0
- package/dist/index.js +8 -0
- package/dist/runtime/cli.js +2935 -0
- package/dist/tools/file-tools.js +346 -0
- package/dist/tools/registry.js +24 -0
- package/package.json +58 -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,24 @@
|
|
|
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: 'write', permission: 'edit', risk: 'medium', concurrencySafe: false, m1: true },
|
|
14
|
+
];
|
|
15
|
+
export const toolRegistry = registry.sort((a, b) => a.name.localeCompare(b.name));
|
|
16
|
+
export function toolSchemaBundleHashInput() {
|
|
17
|
+
return JSON.stringify(toolRegistry.map((tool) => ({
|
|
18
|
+
name: tool.name,
|
|
19
|
+
permission: tool.permission,
|
|
20
|
+
risk: tool.risk,
|
|
21
|
+
concurrencySafe: tool.concurrencySafe,
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=registry.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pugi/cli",
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
|
+
"description": "Pugi CLI — terminal-native software execution system",
|
|
5
|
+
"homepage": "https://pugi.io",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/pugi-io/pugi.git",
|
|
9
|
+
"directory": "apps/pugi-cli"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/pugi-io/pugi/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pugi",
|
|
16
|
+
"cli",
|
|
17
|
+
"ai",
|
|
18
|
+
"agents",
|
|
19
|
+
"engineering",
|
|
20
|
+
"orchestrator"
|
|
21
|
+
],
|
|
22
|
+
"author": "Pugi <hello@pugi.io>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"pugi": "./bin/run.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/run.js",
|
|
30
|
+
"dist/**/*.js",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"zod": "^3.23.0",
|
|
42
|
+
"@pugi/sdk": "0.1.0-alpha.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"tsx": "^4.19.0",
|
|
47
|
+
"typescript": "~5.6.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "pnpm --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/make-bin-executable.mjs",
|
|
51
|
+
"dev": "tsx src/index.ts",
|
|
52
|
+
"typecheck": "pnpm --filter @pugi/sdk build && tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"test": "pnpm run build && node --test --import tsx test/*.spec.ts",
|
|
54
|
+
"version:cli": "tsx src/index.ts version",
|
|
55
|
+
"doctor": "tsx src/index.ts doctor --json",
|
|
56
|
+
"pack:smoke": "node scripts/pack-smoke.mjs"
|
|
57
|
+
}
|
|
58
|
+
}
|