@perstack/base 0.0.54 → 0.0.56

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/README.md CHANGED
@@ -39,22 +39,12 @@ registerWriteTextFile(server)
39
39
  ### File Operations
40
40
  - `readTextFile` - Read text files with optional line range
41
41
  - `writeTextFile` - Create or overwrite text files
42
- - `appendTextFile` - Append content to existing files
43
42
  - `editTextFile` - Replace text in existing files
44
- - `deleteFile` - Remove files
45
- - `moveFile` - Move or rename files
46
- - `getFileInfo` - Get file metadata
47
43
  - `readImageFile` - Read image files (PNG, JPEG, GIF, WebP)
48
44
  - `readPdfFile` - Read PDF files
49
45
 
50
- ### Directory Operations
51
- - `listDirectory` - List directory contents
52
- - `createDirectory` - Create directories
53
- - `deleteDirectory` - Remove directories
54
-
55
46
  ### Utilities
56
47
  - `exec` - Execute system commands
57
- - `healthCheck` - Check Perstack runtime health status
58
48
  - `todo` - Task list management
59
49
  - `clearTodo` - Clear task list
60
50
  - `attemptCompletion` - Signal task completion (validates todos first)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default, createBaseServer } from '../chunk-HUJMWPRE.js';
2
+ import { package_default, createBaseServer } from '../chunk-HXMW3IUI.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { Command } from 'commander';
5
5
 
@@ -0,0 +1,575 @@
1
+ import { z } from 'zod/v4';
2
+ import { realpathSync, existsSync } from 'fs';
3
+ import fs, { constants, stat, mkdir, open, lstat } from 'fs/promises';
4
+ import os from 'os';
5
+ import path, { dirname } from 'path';
6
+ import { execFile } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { getFilteredEnv } from '@perstack/core';
9
+ import mime from 'mime-types';
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+
12
+ // package.json
13
+ var package_default = {
14
+ name: "@perstack/base",
15
+ version: "0.0.56",
16
+ description: "Perstack base skills for agents.",
17
+ author: "Wintermute Technologies, Inc.",
18
+ license: "Apache-2.0",
19
+ type: "module",
20
+ exports: {
21
+ ".": "./src/index.ts"
22
+ },
23
+ publishConfig: {
24
+ access: "public",
25
+ bin: {
26
+ "@perstack/base": "dist/bin/server.js"
27
+ },
28
+ exports: {
29
+ ".": "./dist/src/index.js"
30
+ },
31
+ types: {
32
+ ".": "./dist/src/index.d.ts"
33
+ }
34
+ },
35
+ files: [
36
+ "dist"
37
+ ],
38
+ scripts: {
39
+ clean: "rm -rf dist",
40
+ build: "pnpm run clean && tsup",
41
+ typecheck: "tsc --noEmit"
42
+ },
43
+ dependencies: {
44
+ "@modelcontextprotocol/sdk": "^1.26.0",
45
+ "@perstack/core": "workspace:*",
46
+ commander: "^14.0.3",
47
+ "mime-types": "^3.0.2",
48
+ zod: "^4.3.6"
49
+ },
50
+ devDependencies: {
51
+ "@tsconfig/node22": "^22.0.5",
52
+ "@types/mime-types": "^3.0.1",
53
+ "@types/node": "^25.2.3",
54
+ tsup: "^8.5.1",
55
+ typescript: "^5.9.3",
56
+ vitest: "^4.0.18"
57
+ },
58
+ engines: {
59
+ node: ">=22.0.0"
60
+ }
61
+ };
62
+
63
+ // src/lib/tool-result.ts
64
+ function successToolResult(result) {
65
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
66
+ }
67
+ function errorToolResult(e) {
68
+ return {
69
+ content: [{ type: "text", text: JSON.stringify({ error: e.name, message: e.message }) }]
70
+ };
71
+ }
72
+ var Todo = class {
73
+ currentTodoId = 0;
74
+ todos = [];
75
+ processTodo(input) {
76
+ const { newTodos, completedTodos } = input;
77
+ if (newTodos) {
78
+ this.todos.push(
79
+ ...newTodos.map((title) => ({ id: this.currentTodoId++, title, completed: false }))
80
+ );
81
+ }
82
+ if (completedTodos) {
83
+ this.todos = this.todos.map((todo2) => ({
84
+ ...todo2,
85
+ completed: todo2.completed || completedTodos.includes(todo2.id)
86
+ }));
87
+ }
88
+ return {
89
+ todos: this.todos
90
+ };
91
+ }
92
+ clearTodo() {
93
+ this.todos = [];
94
+ this.currentTodoId = 0;
95
+ return {
96
+ todos: this.todos
97
+ };
98
+ }
99
+ };
100
+ var todoSingleton = new Todo();
101
+ async function todo(input) {
102
+ return todoSingleton.processTodo(input);
103
+ }
104
+ async function clearTodo() {
105
+ return todoSingleton.clearTodo();
106
+ }
107
+ function getRemainingTodos() {
108
+ return todoSingleton.todos.filter((t) => !t.completed);
109
+ }
110
+ function registerTodo(server) {
111
+ server.registerTool(
112
+ "todo",
113
+ {
114
+ title: "todo",
115
+ description: "Manage a todo list: add tasks and mark them completed.",
116
+ inputSchema: {
117
+ newTodos: z.array(z.string()).describe("New todos to add").optional(),
118
+ completedTodos: z.array(z.number()).describe("Todo ids that are completed").optional()
119
+ }
120
+ },
121
+ async (input) => {
122
+ try {
123
+ return successToolResult(await todo(input));
124
+ } catch (e) {
125
+ if (e instanceof Error) return errorToolResult(e);
126
+ throw e;
127
+ }
128
+ }
129
+ );
130
+ }
131
+ function registerClearTodo(server) {
132
+ server.registerTool(
133
+ "clearTodo",
134
+ {
135
+ title: "clearTodo",
136
+ description: "Clear all todos.",
137
+ inputSchema: {}
138
+ },
139
+ async () => {
140
+ try {
141
+ return successToolResult(await clearTodo());
142
+ } catch (e) {
143
+ if (e instanceof Error) return errorToolResult(e);
144
+ throw e;
145
+ }
146
+ }
147
+ );
148
+ }
149
+
150
+ // src/tools/attempt-completion.ts
151
+ async function attemptCompletion() {
152
+ const remainingTodos = getRemainingTodos();
153
+ if (remainingTodos.length > 0) {
154
+ return { remainingTodos };
155
+ }
156
+ return {};
157
+ }
158
+ function registerAttemptCompletion(server) {
159
+ server.registerTool(
160
+ "attemptCompletion",
161
+ {
162
+ title: "Attempt completion",
163
+ description: "Signal task completion. Validates all todos are complete before ending.",
164
+ inputSchema: {}
165
+ },
166
+ async () => {
167
+ try {
168
+ return successToolResult(await attemptCompletion());
169
+ } catch (e) {
170
+ if (e instanceof Error) return errorToolResult(e);
171
+ throw e;
172
+ }
173
+ }
174
+ );
175
+ }
176
+ var workspacePath = realpathSync(expandHome(process.cwd()));
177
+ function expandHome(filepath) {
178
+ if (filepath.startsWith("~/") || filepath === "~") {
179
+ return path.join(os.homedir(), filepath.slice(1));
180
+ }
181
+ return filepath;
182
+ }
183
+ async function validatePath(requestedPath) {
184
+ const expandedPath = expandHome(requestedPath);
185
+ const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath);
186
+ const perstackDir = `${workspacePath}/perstack`.toLowerCase();
187
+ if (absolute.toLowerCase() === perstackDir || absolute.toLowerCase().startsWith(`${perstackDir}/`)) {
188
+ throw new Error("Access denied - perstack directory is not allowed");
189
+ }
190
+ try {
191
+ const realAbsolute = await fs.realpath(absolute);
192
+ if (!isWithinWorkspace(realAbsolute)) {
193
+ throw new Error("Access denied - symlink target outside allowed directories");
194
+ }
195
+ return realAbsolute;
196
+ } catch (_error) {
197
+ const parentDir = path.dirname(absolute);
198
+ try {
199
+ const realParentPath = await fs.realpath(parentDir);
200
+ if (!isWithinWorkspace(realParentPath)) {
201
+ throw new Error("Access denied - parent directory outside allowed directories");
202
+ }
203
+ return absolute;
204
+ } catch {
205
+ if (!isWithinWorkspace(absolute)) {
206
+ throw new Error(
207
+ `Access denied - path outside allowed directories: ${absolute} not in ${workspacePath}`
208
+ );
209
+ }
210
+ throw new Error(`Parent directory does not exist: ${parentDir}`);
211
+ }
212
+ }
213
+ }
214
+ function isWithinWorkspace(absolutePath) {
215
+ return absolutePath === workspacePath || absolutePath.startsWith(`${workspacePath}/`);
216
+ }
217
+ var O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
218
+ typeof constants.O_NOFOLLOW === "number";
219
+ async function checkNotSymlink(path2) {
220
+ const stats = await lstat(path2).catch(() => null);
221
+ if (stats?.isSymbolicLink()) {
222
+ throw new Error("Operation denied: target is a symbolic link");
223
+ }
224
+ }
225
+ async function safeWriteFile(path2, data) {
226
+ let handle;
227
+ try {
228
+ await checkNotSymlink(path2);
229
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | O_NOFOLLOW;
230
+ handle = await open(path2, flags, 420);
231
+ await handle.writeFile(data, "utf-8");
232
+ } finally {
233
+ await handle?.close();
234
+ }
235
+ }
236
+ async function safeReadFile(path2) {
237
+ let handle;
238
+ try {
239
+ await checkNotSymlink(path2);
240
+ const flags = constants.O_RDONLY | O_NOFOLLOW;
241
+ handle = await open(path2, flags);
242
+ const buffer = await handle.readFile("utf-8");
243
+ return buffer;
244
+ } finally {
245
+ await handle?.close();
246
+ }
247
+ }
248
+
249
+ // src/tools/edit-text-file.ts
250
+ async function editTextFile(input) {
251
+ const { path: path2, newText, oldText } = input;
252
+ const validatedPath = await validatePath(path2);
253
+ const stats = await stat(validatedPath).catch(() => null);
254
+ if (!stats) {
255
+ throw new Error(`File ${path2} does not exist.`);
256
+ }
257
+ if (!(stats.mode & 128)) {
258
+ throw new Error(`File ${path2} is not writable`);
259
+ }
260
+ await applyFileEdit(validatedPath, newText, oldText);
261
+ return {
262
+ path: validatedPath,
263
+ newText,
264
+ oldText
265
+ };
266
+ }
267
+ function normalizeLineEndings(text) {
268
+ return text.replace(/\r\n/g, "\n");
269
+ }
270
+ async function applyFileEdit(filePath, newText, oldText) {
271
+ const content = normalizeLineEndings(await safeReadFile(filePath));
272
+ const normalizedOld = normalizeLineEndings(oldText);
273
+ const normalizedNew = normalizeLineEndings(newText);
274
+ if (!content.includes(normalizedOld)) {
275
+ throw new Error(`Could not find exact match for oldText in file ${filePath}`);
276
+ }
277
+ const modifiedContent = content.replace(normalizedOld, normalizedNew);
278
+ await safeWriteFile(filePath, modifiedContent);
279
+ }
280
+ function registerEditTextFile(server) {
281
+ server.registerTool(
282
+ "editTextFile",
283
+ {
284
+ title: "Edit text file",
285
+ description: "Replace exact text in an existing file. Normalizes line endings (CRLF \u2192 LF).",
286
+ inputSchema: {
287
+ path: z.string().describe("Target file path to edit."),
288
+ newText: z.string().describe("Text to replace with."),
289
+ oldText: z.string().describe("Exact text to find and replace.")
290
+ }
291
+ },
292
+ async (input) => {
293
+ try {
294
+ return successToolResult(await editTextFile(input));
295
+ } catch (e) {
296
+ if (e instanceof Error) return errorToolResult(e);
297
+ throw e;
298
+ }
299
+ }
300
+ );
301
+ }
302
+ var execFileAsync = promisify(execFile);
303
+ function isExecError(error) {
304
+ return error instanceof Error && "code" in error;
305
+ }
306
+ async function exec(input) {
307
+ const validatedCwd = await validatePath(input.cwd);
308
+ const { stdout, stderr } = await execFileAsync(input.command, input.args, {
309
+ cwd: validatedCwd,
310
+ env: getFilteredEnv(input.env),
311
+ timeout: input.timeout,
312
+ maxBuffer: 10 * 1024 * 1024
313
+ });
314
+ let output = "";
315
+ if (input.stdout) {
316
+ output += stdout;
317
+ }
318
+ if (input.stderr) {
319
+ output += stderr;
320
+ }
321
+ if (!output.trim()) {
322
+ output = "Command executed successfully, but produced no output.";
323
+ }
324
+ return { output };
325
+ }
326
+ function registerExec(server) {
327
+ server.registerTool(
328
+ "exec",
329
+ {
330
+ title: "Execute Command",
331
+ description: "Execute a system command. Returns stdout/stderr.",
332
+ inputSchema: {
333
+ command: z.string().describe("The command to execute"),
334
+ args: z.array(z.string()).describe("The arguments to pass to the command"),
335
+ env: z.record(z.string(), z.string()).describe("The environment variables to set"),
336
+ cwd: z.string().describe("The working directory to execute the command in"),
337
+ stdout: z.boolean().describe("Whether to capture the standard output"),
338
+ stderr: z.boolean().describe("Whether to capture the standard error"),
339
+ timeout: z.number().optional().default(6e4).describe("Timeout in milliseconds (default: 60000)")
340
+ }
341
+ },
342
+ async (input) => {
343
+ try {
344
+ return successToolResult(await exec(input));
345
+ } catch (error) {
346
+ let message;
347
+ let stdout;
348
+ let stderr;
349
+ if (isExecError(error)) {
350
+ if ((error.killed || error.signal === "SIGTERM") && typeof input.timeout === "number") {
351
+ message = `Command timed out after ${input.timeout}ms.`;
352
+ } else if (error.message.includes("timeout")) {
353
+ message = `Command timed out after ${input.timeout}ms.`;
354
+ } else {
355
+ message = error.message;
356
+ }
357
+ stdout = error.stdout;
358
+ stderr = error.stderr;
359
+ } else if (error instanceof Error) {
360
+ message = error.message;
361
+ } else {
362
+ message = "An unknown error occurred.";
363
+ }
364
+ const result = { error: message };
365
+ if (stdout && input.stdout) {
366
+ result.stdout = stdout;
367
+ }
368
+ if (stderr && input.stderr) {
369
+ result.stderr = stderr;
370
+ }
371
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
372
+ }
373
+ }
374
+ );
375
+ }
376
+ var MAX_IMAGE_SIZE = 15 * 1024 * 1024;
377
+ async function readImageFile(input) {
378
+ const { path: path2 } = input;
379
+ const validatedPath = await validatePath(path2);
380
+ const isFile = existsSync(validatedPath);
381
+ if (!isFile) {
382
+ throw new Error(`File ${path2} does not exist.`);
383
+ }
384
+ const mimeType = mime.lookup(validatedPath);
385
+ if (!mimeType || !["image/png", "image/jpeg", "image/gif", "image/webp"].includes(mimeType)) {
386
+ throw new Error(`File ${path2} is not supported.`);
387
+ }
388
+ const fileStats = await stat(validatedPath);
389
+ const fileSizeMB = fileStats.size / (1024 * 1024);
390
+ if (fileStats.size > MAX_IMAGE_SIZE) {
391
+ throw new Error(
392
+ `Image file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB. Please use a smaller image file.`
393
+ );
394
+ }
395
+ return {
396
+ path: validatedPath,
397
+ mimeType,
398
+ size: fileStats.size
399
+ };
400
+ }
401
+ function registerReadImageFile(server) {
402
+ server.registerTool(
403
+ "readImageFile",
404
+ {
405
+ title: "Read image file",
406
+ description: "Read an image file as base64. Supports PNG, JPEG, GIF, WebP. Max 15MB.",
407
+ inputSchema: {
408
+ path: z.string()
409
+ }
410
+ },
411
+ async (input) => {
412
+ try {
413
+ return successToolResult(await readImageFile(input));
414
+ } catch (e) {
415
+ if (e instanceof Error) return errorToolResult(e);
416
+ throw e;
417
+ }
418
+ }
419
+ );
420
+ }
421
+ var MAX_PDF_SIZE = 30 * 1024 * 1024;
422
+ async function readPdfFile(input) {
423
+ const { path: path2 } = input;
424
+ const validatedPath = await validatePath(path2);
425
+ const isFile = existsSync(validatedPath);
426
+ if (!isFile) {
427
+ throw new Error(`File ${path2} does not exist.`);
428
+ }
429
+ const mimeType = mime.lookup(validatedPath);
430
+ if (mimeType !== "application/pdf") {
431
+ throw new Error(`File ${path2} is not a PDF file.`);
432
+ }
433
+ const fileStats = await stat(validatedPath);
434
+ const fileSizeMB = fileStats.size / (1024 * 1024);
435
+ if (fileStats.size > MAX_PDF_SIZE) {
436
+ throw new Error(
437
+ `PDF file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_PDF_SIZE / (1024 * 1024)}MB. Please use a smaller PDF file.`
438
+ );
439
+ }
440
+ return {
441
+ path: validatedPath,
442
+ mimeType,
443
+ size: fileStats.size
444
+ };
445
+ }
446
+ function registerReadPdfFile(server) {
447
+ server.registerTool(
448
+ "readPdfFile",
449
+ {
450
+ title: "Read PDF file",
451
+ description: "Read a PDF file as base64. Max 30MB.",
452
+ inputSchema: {
453
+ path: z.string()
454
+ }
455
+ },
456
+ async (input) => {
457
+ try {
458
+ return successToolResult(await readPdfFile(input));
459
+ } catch (e) {
460
+ if (e instanceof Error) return errorToolResult(e);
461
+ throw e;
462
+ }
463
+ }
464
+ );
465
+ }
466
+ async function readTextFile(input) {
467
+ const { path: path2, from, to } = input;
468
+ const validatedPath = await validatePath(path2);
469
+ const stats = await stat(validatedPath).catch(() => null);
470
+ if (!stats) {
471
+ throw new Error(`File ${path2} does not exist.`);
472
+ }
473
+ const fileContent = await safeReadFile(validatedPath);
474
+ const lines = fileContent.split("\n");
475
+ const fromLine = from ?? 0;
476
+ const toLine = to ?? lines.length;
477
+ const selectedLines = lines.slice(fromLine, toLine);
478
+ const content = selectedLines.join("\n");
479
+ return {
480
+ path: path2,
481
+ content,
482
+ from: fromLine,
483
+ to: toLine
484
+ };
485
+ }
486
+ function registerReadTextFile(server) {
487
+ server.registerTool(
488
+ "readTextFile",
489
+ {
490
+ title: "Read text file",
491
+ description: "Read a UTF-8 text file. Supports partial reading via line ranges.",
492
+ inputSchema: {
493
+ path: z.string(),
494
+ from: z.number().optional().describe("The line number to start reading from."),
495
+ to: z.number().optional().describe("The line number to stop reading at.")
496
+ }
497
+ },
498
+ async (input) => {
499
+ try {
500
+ return successToolResult(await readTextFile(input));
501
+ } catch (e) {
502
+ if (e instanceof Error) return errorToolResult(e);
503
+ throw e;
504
+ }
505
+ }
506
+ );
507
+ }
508
+ async function writeTextFile(input) {
509
+ const { path: path2, text } = input;
510
+ const validatedPath = await validatePath(path2);
511
+ const stats = await stat(validatedPath).catch(() => null);
512
+ if (stats && !(stats.mode & 128)) {
513
+ throw new Error(`File ${path2} is not writable`);
514
+ }
515
+ const dir = dirname(validatedPath);
516
+ await mkdir(dir, { recursive: true });
517
+ await safeWriteFile(validatedPath, text);
518
+ return {
519
+ path: validatedPath,
520
+ text
521
+ };
522
+ }
523
+ function registerWriteTextFile(server) {
524
+ server.registerTool(
525
+ "writeTextFile",
526
+ {
527
+ title: "writeTextFile",
528
+ description: "Create or overwrite a UTF-8 text file. Creates parent directories as needed.",
529
+ inputSchema: {
530
+ path: z.string().describe("Target file path (relative or absolute)."),
531
+ text: z.string().describe("Text to write to the file.")
532
+ }
533
+ },
534
+ async (input) => {
535
+ try {
536
+ return successToolResult(await writeTextFile(input));
537
+ } catch (e) {
538
+ if (e instanceof Error) return errorToolResult(e);
539
+ throw e;
540
+ }
541
+ }
542
+ );
543
+ }
544
+ var BASE_SKILL_NAME = package_default.name;
545
+ var BASE_SKILL_VERSION = package_default.version;
546
+ function registerAllTools(server) {
547
+ registerAttemptCompletion(server);
548
+ registerTodo(server);
549
+ registerClearTodo(server);
550
+ registerExec(server);
551
+ registerReadTextFile(server);
552
+ registerReadImageFile(server);
553
+ registerReadPdfFile(server);
554
+ registerWriteTextFile(server);
555
+ registerEditTextFile(server);
556
+ }
557
+ function createBaseServer() {
558
+ const server = new McpServer(
559
+ {
560
+ name: BASE_SKILL_NAME,
561
+ version: BASE_SKILL_VERSION
562
+ },
563
+ {
564
+ capabilities: {
565
+ tools: {}
566
+ }
567
+ }
568
+ );
569
+ registerAllTools(server);
570
+ return server;
571
+ }
572
+
573
+ export { BASE_SKILL_NAME, BASE_SKILL_VERSION, attemptCompletion, clearTodo, createBaseServer, editTextFile, errorToolResult, exec, getRemainingTodos, package_default, readImageFile, readPdfFile, readTextFile, registerAllTools, registerAttemptCompletion, registerClearTodo, registerEditTextFile, registerExec, registerReadImageFile, registerReadPdfFile, registerReadTextFile, registerTodo, registerWriteTextFile, successToolResult, todo, validatePath, writeTextFile };
574
+ //# sourceMappingURL=chunk-HXMW3IUI.js.map
575
+ //# sourceMappingURL=chunk-HXMW3IUI.js.map