@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.
@@ -1,1225 +0,0 @@
1
- import { realpathSync, existsSync, statSync } from 'fs';
2
- import fs, { constants, stat, mkdir, rm, rmdir, unlink, readdir, rename, open, lstat } from 'fs/promises';
3
- import os from 'os';
4
- import path, { dirname, extname, basename, resolve, join } from 'path';
5
- import { dedent } from 'ts-dedent';
6
- import { z } from 'zod/v4';
7
- import { execFile } from 'child_process';
8
- import { promisify } from 'util';
9
- import { getFilteredEnv } from '@perstack/core';
10
- import mime from 'mime-types';
11
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
-
13
- // package.json
14
- var package_default = {
15
- name: "@perstack/base",
16
- version: "0.0.54",
17
- description: "Perstack base skills for agents.",
18
- author: "Wintermute Technologies, Inc.",
19
- license: "Apache-2.0",
20
- type: "module",
21
- exports: {
22
- ".": "./src/index.ts"
23
- },
24
- publishConfig: {
25
- access: "public",
26
- bin: {
27
- "@perstack/base": "dist/bin/server.js"
28
- },
29
- exports: {
30
- ".": "./dist/src/index.js"
31
- },
32
- types: {
33
- ".": "./dist/src/index.d.ts"
34
- }
35
- },
36
- files: [
37
- "dist"
38
- ],
39
- scripts: {
40
- clean: "rm -rf dist",
41
- build: "pnpm run clean && tsup",
42
- typecheck: "tsc --noEmit"
43
- },
44
- dependencies: {
45
- "@modelcontextprotocol/sdk": "^1.25.3",
46
- "@perstack/core": "workspace:*",
47
- commander: "^14.0.2",
48
- "mime-types": "^3.0.2",
49
- "ts-dedent": "^2.2.0",
50
- zod: "^4.3.6"
51
- },
52
- devDependencies: {
53
- "@tsconfig/node22": "^22.0.5",
54
- "@types/mime-types": "^3.0.1",
55
- "@types/node": "^25.0.10",
56
- tsup: "^8.5.1",
57
- typescript: "^5.9.3",
58
- vitest: "^4.0.18"
59
- },
60
- engines: {
61
- node: ">=22.0.0"
62
- }
63
- };
64
- var workspacePath = realpathSync(expandHome(process.cwd()));
65
- function expandHome(filepath) {
66
- if (filepath.startsWith("~/") || filepath === "~") {
67
- return path.join(os.homedir(), filepath.slice(1));
68
- }
69
- return filepath;
70
- }
71
- async function validatePath(requestedPath) {
72
- const expandedPath = expandHome(requestedPath);
73
- const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath);
74
- const perstackDir = `${workspacePath}/perstack`.toLowerCase();
75
- if (absolute.toLowerCase() === perstackDir || absolute.toLowerCase().startsWith(`${perstackDir}/`)) {
76
- throw new Error("Access denied - perstack directory is not allowed");
77
- }
78
- try {
79
- const realAbsolute = await fs.realpath(absolute);
80
- if (!isWithinWorkspace(realAbsolute)) {
81
- throw new Error("Access denied - symlink target outside allowed directories");
82
- }
83
- return realAbsolute;
84
- } catch (_error) {
85
- const parentDir = path.dirname(absolute);
86
- try {
87
- const realParentPath = await fs.realpath(parentDir);
88
- if (!isWithinWorkspace(realParentPath)) {
89
- throw new Error("Access denied - parent directory outside allowed directories");
90
- }
91
- return absolute;
92
- } catch {
93
- if (!isWithinWorkspace(absolute)) {
94
- throw new Error(
95
- `Access denied - path outside allowed directories: ${absolute} not in ${workspacePath}`
96
- );
97
- }
98
- throw new Error(`Parent directory does not exist: ${parentDir}`);
99
- }
100
- }
101
- }
102
- function isWithinWorkspace(absolutePath) {
103
- return absolutePath === workspacePath || absolutePath.startsWith(`${workspacePath}/`);
104
- }
105
-
106
- // src/lib/tool-result.ts
107
- function successToolResult(result) {
108
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
109
- }
110
- function errorToolResult(e) {
111
- return {
112
- content: [{ type: "text", text: JSON.stringify({ error: e.name, message: e.message }) }]
113
- };
114
- }
115
- var O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
116
- typeof constants.O_NOFOLLOW === "number";
117
- async function checkNotSymlink(path2) {
118
- const stats = await lstat(path2).catch(() => null);
119
- if (stats?.isSymbolicLink()) {
120
- throw new Error("Operation denied: target is a symbolic link");
121
- }
122
- }
123
- async function safeWriteFile(path2, data) {
124
- let handle;
125
- try {
126
- await checkNotSymlink(path2);
127
- const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | O_NOFOLLOW;
128
- handle = await open(path2, flags, 420);
129
- await handle.writeFile(data, "utf-8");
130
- } finally {
131
- await handle?.close();
132
- }
133
- }
134
- async function safeReadFile(path2) {
135
- let handle;
136
- try {
137
- await checkNotSymlink(path2);
138
- const flags = constants.O_RDONLY | O_NOFOLLOW;
139
- handle = await open(path2, flags);
140
- const buffer = await handle.readFile("utf-8");
141
- return buffer;
142
- } finally {
143
- await handle?.close();
144
- }
145
- }
146
- async function safeAppendFile(path2, data) {
147
- let handle;
148
- try {
149
- await checkNotSymlink(path2);
150
- const flags = constants.O_WRONLY | constants.O_APPEND | O_NOFOLLOW;
151
- handle = await open(path2, flags);
152
- await handle.writeFile(data, "utf-8");
153
- } finally {
154
- await handle?.close();
155
- }
156
- }
157
-
158
- // src/tools/append-text-file.ts
159
- async function appendTextFile({ path: path2, text }) {
160
- const validatedPath = await validatePath(path2);
161
- const stats = await stat(validatedPath).catch(() => null);
162
- if (!stats) {
163
- throw new Error(`File ${path2} does not exist.`);
164
- }
165
- if (!(stats.mode & 128)) {
166
- throw new Error(`File ${path2} is not writable`);
167
- }
168
- await safeAppendFile(validatedPath, text);
169
- return { path: validatedPath, text };
170
- }
171
- function registerAppendTextFile(server) {
172
- server.registerTool(
173
- "appendTextFile",
174
- {
175
- title: "Append text file",
176
- description: dedent`
177
- Adding content to the end of existing files.
178
-
179
- Use cases:
180
- - Adding entries to log files
181
- - Appending data to CSV or JSON files
182
- - Adding new sections to documentation
183
- - Extending configuration files
184
- - Building files incrementally
185
-
186
- How it works:
187
- - Appends text to the end of an existing file
188
- - Does not modify existing content
189
- - Creates a new line before appending if needed
190
- - Returns the appended file path
191
-
192
- Rules:
193
- - FILE MUST EXIST BEFORE APPENDING
194
- - YOU MUST PROVIDE A VALID UTF-8 STRING FOR THE TEXT
195
- `,
196
- inputSchema: {
197
- path: z.string().describe("Target file path to append to."),
198
- text: z.string().describe("Text to append to the file.")
199
- }
200
- },
201
- async ({ path: path2, text }) => {
202
- try {
203
- return successToolResult(await appendTextFile({ path: path2, text }));
204
- } catch (e) {
205
- if (e instanceof Error) return errorToolResult(e);
206
- throw e;
207
- }
208
- }
209
- );
210
- }
211
- var Todo = class {
212
- currentTodoId = 0;
213
- todos = [];
214
- processTodo(input) {
215
- const { newTodos, completedTodos } = input;
216
- if (newTodos) {
217
- this.todos.push(
218
- ...newTodos.map((title) => ({ id: this.currentTodoId++, title, completed: false }))
219
- );
220
- }
221
- if (completedTodos) {
222
- this.todos = this.todos.map((todo2) => ({
223
- ...todo2,
224
- completed: todo2.completed || completedTodos.includes(todo2.id)
225
- }));
226
- }
227
- return {
228
- todos: this.todos
229
- };
230
- }
231
- clearTodo() {
232
- this.todos = [];
233
- this.currentTodoId = 0;
234
- return {
235
- todos: this.todos
236
- };
237
- }
238
- };
239
- var todoSingleton = new Todo();
240
- async function todo(input) {
241
- return todoSingleton.processTodo(input);
242
- }
243
- async function clearTodo() {
244
- return todoSingleton.clearTodo();
245
- }
246
- function getRemainingTodos() {
247
- return todoSingleton.todos.filter((t) => !t.completed);
248
- }
249
- function registerTodo(server) {
250
- server.registerTool(
251
- "todo",
252
- {
253
- title: "todo",
254
- description: dedent`
255
- Todo list manager that tracks tasks and their completion status.
256
-
257
- Use cases:
258
- - Creating new tasks or action items
259
- - Marking tasks as completed
260
- - Viewing current task list and status
261
-
262
- How it works:
263
- - Each todo gets a unique ID when created
264
- - Returns the full todo list after every operation
265
- - Maintains state across multiple calls
266
-
267
- Parameters:
268
- - newTodos: Array of task descriptions to add
269
- - completedTodos: Array of todo IDs to mark as completed
270
- `,
271
- inputSchema: {
272
- newTodos: z.array(z.string()).describe("New todos to add").optional(),
273
- completedTodos: z.array(z.number()).describe("Todo ids that are completed").optional()
274
- }
275
- },
276
- async (input) => {
277
- try {
278
- return successToolResult(await todo(input));
279
- } catch (e) {
280
- if (e instanceof Error) return errorToolResult(e);
281
- throw e;
282
- }
283
- }
284
- );
285
- }
286
- function registerClearTodo(server) {
287
- server.registerTool(
288
- "clearTodo",
289
- {
290
- title: "clearTodo",
291
- description: dedent`
292
- Clears the todo list.
293
-
294
- Use cases:
295
- - Resetting the todo list to an empty state
296
- - Starting fresh with a new task list
297
- - Clearing all tasks for a new day or project
298
-
299
- How it works:
300
- - Resets the todo list to an empty state
301
- - Returns an empty todo list
302
- `,
303
- inputSchema: {}
304
- },
305
- async () => {
306
- try {
307
- return successToolResult(await clearTodo());
308
- } catch (e) {
309
- if (e instanceof Error) return errorToolResult(e);
310
- throw e;
311
- }
312
- }
313
- );
314
- }
315
- async function attemptCompletion() {
316
- const remainingTodos = getRemainingTodos();
317
- if (remainingTodos.length > 0) {
318
- return { remainingTodos };
319
- }
320
- return {};
321
- }
322
- function registerAttemptCompletion(server) {
323
- server.registerTool(
324
- "attemptCompletion",
325
- {
326
- title: "Attempt completion",
327
- description: dedent`
328
- Task completion signal with automatic todo validation.
329
- Use cases:
330
- - Signaling task completion to Perstack runtime
331
- - Validating all todos are complete before ending
332
- - Ending the current expert's work cycle
333
- How it works:
334
- - Checks the current todo list for incomplete items
335
- - If incomplete todos exist: returns them and continues the agent loop
336
- - If no incomplete todos: returns empty object and ends the agent loop
337
- Notes:
338
- - Mark all todos as complete before calling
339
- - Use clearTodo if you want to reset and start fresh
340
- - Prevents premature completion by surfacing forgotten tasks
341
- `,
342
- inputSchema: {}
343
- },
344
- async () => {
345
- try {
346
- return successToolResult(await attemptCompletion());
347
- } catch (e) {
348
- if (e instanceof Error) return errorToolResult(e);
349
- throw e;
350
- }
351
- }
352
- );
353
- }
354
- async function createDirectory(input) {
355
- const { path: path2 } = input;
356
- const validatedPath = await validatePath(path2);
357
- const exists = existsSync(validatedPath);
358
- if (exists) {
359
- throw new Error(`Directory ${path2} already exists`);
360
- }
361
- const parentDir = dirname(validatedPath);
362
- if (existsSync(parentDir)) {
363
- const parentStats = statSync(parentDir);
364
- if (!(parentStats.mode & 128)) {
365
- throw new Error(`Parent directory ${parentDir} is not writable`);
366
- }
367
- }
368
- await mkdir(validatedPath, { recursive: true });
369
- return {
370
- path: validatedPath
371
- };
372
- }
373
- function registerCreateDirectory(server) {
374
- server.registerTool(
375
- "createDirectory",
376
- {
377
- title: "Create directory",
378
- description: dedent`
379
- Directory creator for establishing folder structures in the workspace.
380
-
381
- Use cases:
382
- - Setting up project directory structure
383
- - Creating output folders for generated content
384
- - Organizing files into logical groups
385
- - Preparing directory hierarchies
386
-
387
- How it works:
388
- - Creates directories recursively
389
- - Handles existing directories gracefully
390
- - Creates parent directories as needed
391
- - Returns creation status
392
-
393
- Parameters:
394
- - path: Directory path to create
395
- `,
396
- inputSchema: {
397
- path: z.string()
398
- }
399
- },
400
- async (input) => {
401
- try {
402
- return successToolResult(await createDirectory(input));
403
- } catch (e) {
404
- if (e instanceof Error) return errorToolResult(e);
405
- throw e;
406
- }
407
- }
408
- );
409
- }
410
- async function deleteDirectory(input) {
411
- const { path: path2, recursive } = input;
412
- const validatedPath = await validatePath(path2);
413
- if (!existsSync(validatedPath)) {
414
- throw new Error(`Directory ${path2} does not exist.`);
415
- }
416
- const stats = statSync(validatedPath);
417
- if (!stats.isDirectory()) {
418
- throw new Error(`Path ${path2} is not a directory. Use deleteFile tool instead.`);
419
- }
420
- if (!(stats.mode & 128)) {
421
- throw new Error(`Directory ${path2} is not writable`);
422
- }
423
- if (recursive) {
424
- await rm(validatedPath, { recursive: true });
425
- } else {
426
- await rmdir(validatedPath);
427
- }
428
- return {
429
- path: validatedPath
430
- };
431
- }
432
- function registerDeleteDirectory(server) {
433
- server.registerTool(
434
- "deleteDirectory",
435
- {
436
- title: "Delete directory",
437
- description: dedent`
438
- Directory deleter for removing directories from the workspace.
439
-
440
- Use cases:
441
- - Removing temporary directories
442
- - Cleaning up build artifacts
443
- - Deleting empty directories after moving files
444
-
445
- How it works:
446
- - Validates directory existence and permissions
447
- - Removes directory (and contents if recursive is true)
448
- - Returns deletion status
449
-
450
- Parameters:
451
- - path: Directory path to delete
452
- - recursive: Set to true to delete non-empty directories
453
- `,
454
- inputSchema: {
455
- path: z.string(),
456
- recursive: z.boolean().optional().describe("Whether to delete contents recursively. Required for non-empty directories.")
457
- }
458
- },
459
- async (input) => {
460
- try {
461
- return successToolResult(await deleteDirectory(input));
462
- } catch (e) {
463
- if (e instanceof Error) return errorToolResult(e);
464
- throw e;
465
- }
466
- }
467
- );
468
- }
469
- async function deleteFile(input) {
470
- const { path: path2 } = input;
471
- const validatedPath = await validatePath(path2);
472
- if (!existsSync(validatedPath)) {
473
- throw new Error(`File ${path2} does not exist.`);
474
- }
475
- const stats = statSync(validatedPath);
476
- if (stats.isDirectory()) {
477
- throw new Error(`Path ${path2} is a directory. Use delete directory tool instead.`);
478
- }
479
- if (!(stats.mode & 128)) {
480
- throw new Error(`File ${path2} is not writable`);
481
- }
482
- await unlink(validatedPath);
483
- return {
484
- path: validatedPath
485
- };
486
- }
487
- function registerDeleteFile(server) {
488
- server.registerTool(
489
- "deleteFile",
490
- {
491
- title: "Delete file",
492
- description: dedent`
493
- File deleter for removing files from the workspace.
494
-
495
- Use cases:
496
- - Removing temporary files
497
- - Cleaning up generated files
498
- - Deleting outdated configuration files
499
- - Removing unwanted artifacts
500
-
501
- How it works:
502
- - Validates file existence and permissions
503
- - Performs atomic delete operation
504
- - Returns deletion status
505
-
506
- Parameters:
507
- - path: File path to delete
508
- `,
509
- inputSchema: {
510
- path: z.string()
511
- }
512
- },
513
- async (input) => {
514
- try {
515
- return successToolResult(await deleteFile(input));
516
- } catch (e) {
517
- if (e instanceof Error) return errorToolResult(e);
518
- throw e;
519
- }
520
- }
521
- );
522
- }
523
- async function editTextFile(input) {
524
- const { path: path2, newText, oldText } = input;
525
- const validatedPath = await validatePath(path2);
526
- const stats = await stat(validatedPath).catch(() => null);
527
- if (!stats) {
528
- throw new Error(`File ${path2} does not exist.`);
529
- }
530
- if (!(stats.mode & 128)) {
531
- throw new Error(`File ${path2} is not writable`);
532
- }
533
- await applyFileEdit(validatedPath, newText, oldText);
534
- return {
535
- path: validatedPath,
536
- newText,
537
- oldText
538
- };
539
- }
540
- function normalizeLineEndings(text) {
541
- return text.replace(/\r\n/g, "\n");
542
- }
543
- async function applyFileEdit(filePath, newText, oldText) {
544
- const content = normalizeLineEndings(await safeReadFile(filePath));
545
- const normalizedOld = normalizeLineEndings(oldText);
546
- const normalizedNew = normalizeLineEndings(newText);
547
- if (!content.includes(normalizedOld)) {
548
- throw new Error(`Could not find exact match for oldText in file ${filePath}`);
549
- }
550
- const modifiedContent = content.replace(normalizedOld, normalizedNew);
551
- await safeWriteFile(filePath, modifiedContent);
552
- }
553
- function registerEditTextFile(server) {
554
- server.registerTool(
555
- "editTextFile",
556
- {
557
- title: "Edit text file",
558
- description: dedent`
559
- Text file editor for modifying existing files with precise text replacement.
560
-
561
- Use cases:
562
- - Updating configuration values
563
- - Modifying code snippets
564
- - Replacing specific text blocks
565
- - Making targeted edits to files
566
-
567
- How it works:
568
- - Reads existing file content
569
- - Performs exact text replacement of oldText with newText
570
- - Normalizes line endings for consistent behavior
571
- - Returns summary of changes made
572
- - For appending text to files, use the appendTextFile tool instead
573
-
574
- Rules:
575
- - YOU MUST PROVIDE A VALID UTF-8 STRING FOR THE TEXT
576
- - DO NOT USE THIS TOOL FOR APPENDING TEXT TO FILES - USE appendTextFile TOOL INSTEAD
577
- `,
578
- inputSchema: {
579
- path: z.string().describe("Target file path to edit."),
580
- newText: z.string().describe("Text to replace with."),
581
- oldText: z.string().describe("Exact text to find and replace.")
582
- }
583
- },
584
- async (input) => {
585
- try {
586
- return successToolResult(await editTextFile(input));
587
- } catch (e) {
588
- if (e instanceof Error) return errorToolResult(e);
589
- throw e;
590
- }
591
- }
592
- );
593
- }
594
- var execFileAsync = promisify(execFile);
595
- function isExecError(error) {
596
- return error instanceof Error && "code" in error;
597
- }
598
- async function exec(input) {
599
- const validatedCwd = await validatePath(input.cwd);
600
- const { stdout, stderr } = await execFileAsync(input.command, input.args, {
601
- cwd: validatedCwd,
602
- env: getFilteredEnv(input.env),
603
- timeout: input.timeout,
604
- maxBuffer: 10 * 1024 * 1024
605
- });
606
- let output = "";
607
- if (input.stdout) {
608
- output += stdout;
609
- }
610
- if (input.stderr) {
611
- output += stderr;
612
- }
613
- if (!output.trim()) {
614
- output = "Command executed successfully, but produced no output.";
615
- }
616
- return { output };
617
- }
618
- function registerExec(server) {
619
- server.registerTool(
620
- "exec",
621
- {
622
- title: "Execute Command",
623
- description: dedent`
624
- Command executor for running system commands and scripts.
625
-
626
- Use cases:
627
- - Running system tasks or scripts
628
- - Automating command-line tools or utilities
629
- - Executing build commands or test runners
630
-
631
- How it works:
632
- - Executes the specified command with arguments
633
- - Captures stdout and/or stderr based on flags
634
- - Returns command output or error information
635
-
636
- Parameters:
637
- - command: The command to execute (e.g., ls, python)
638
- - args: Arguments to pass to the command
639
- - env: Environment variables for the execution
640
- - cwd: Working directory for command execution
641
- - stdout: Whether to capture standard output
642
- - stderr: Whether to capture standard error
643
- - timeout: Timeout in milliseconds (optional)
644
-
645
- Rules:
646
- - Only execute commands from trusted sources
647
- - Do not execute long-running foreground commands (e.g., tail -f)
648
- - Be cautious with resource-intensive commands
649
- `,
650
- inputSchema: {
651
- command: z.string().describe("The command to execute"),
652
- args: z.array(z.string()).describe("The arguments to pass to the command"),
653
- env: z.record(z.string(), z.string()).describe("The environment variables to set"),
654
- cwd: z.string().describe("The working directory to execute the command in"),
655
- stdout: z.boolean().describe("Whether to capture the standard output"),
656
- stderr: z.boolean().describe("Whether to capture the standard error"),
657
- timeout: z.number().optional().default(6e4).describe("Timeout in milliseconds (default: 60000)")
658
- }
659
- },
660
- async (input) => {
661
- try {
662
- return successToolResult(await exec(input));
663
- } catch (error) {
664
- let message;
665
- let stdout;
666
- let stderr;
667
- if (isExecError(error)) {
668
- if ((error.killed || error.signal === "SIGTERM") && typeof input.timeout === "number") {
669
- message = `Command timed out after ${input.timeout}ms.`;
670
- } else if (error.message.includes("timeout")) {
671
- message = `Command timed out after ${input.timeout}ms.`;
672
- } else {
673
- message = error.message;
674
- }
675
- stdout = error.stdout;
676
- stderr = error.stderr;
677
- } else if (error instanceof Error) {
678
- message = error.message;
679
- } else {
680
- message = "An unknown error occurred.";
681
- }
682
- const result = { error: message };
683
- if (stdout && input.stdout) {
684
- result.stdout = stdout;
685
- }
686
- if (stderr && input.stderr) {
687
- result.stderr = stderr;
688
- }
689
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
690
- }
691
- }
692
- );
693
- }
694
- async function getFileInfo(input) {
695
- const { path: path2 } = input;
696
- const validatedPath = await validatePath(path2);
697
- if (!existsSync(validatedPath)) {
698
- throw new Error(`File or directory ${path2} does not exist`);
699
- }
700
- const stats = statSync(validatedPath);
701
- const isDirectory = stats.isDirectory();
702
- const mimeType = isDirectory ? null : mime.lookup(validatedPath) || "application/octet-stream";
703
- const formatSize = (bytes) => {
704
- if (bytes === 0) return "0 B";
705
- const units = ["B", "KB", "MB", "GB", "TB"];
706
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
707
- return `${(bytes / 1024 ** i).toFixed(2)} ${units[i]}`;
708
- };
709
- return {
710
- exists: true,
711
- path: validatedPath,
712
- absolutePath: resolve(validatedPath),
713
- name: basename(validatedPath),
714
- directory: dirname(validatedPath),
715
- extension: isDirectory ? null : extname(validatedPath),
716
- type: isDirectory ? "directory" : "file",
717
- mimeType,
718
- size: stats.size,
719
- sizeFormatted: formatSize(stats.size),
720
- created: stats.birthtime.toISOString(),
721
- modified: stats.mtime.toISOString(),
722
- accessed: stats.atime.toISOString(),
723
- permissions: {
724
- readable: true,
725
- writable: Boolean(stats.mode & 128),
726
- executable: Boolean(stats.mode & 64)
727
- }
728
- };
729
- }
730
- function registerGetFileInfo(server) {
731
- server.registerTool(
732
- "getFileInfo",
733
- {
734
- title: "Get file info",
735
- description: dedent`
736
- File information retriever for detailed metadata about files and directories.
737
-
738
- Use cases:
739
- - Checking file existence and type
740
- - Getting file size and timestamps
741
- - Determining MIME types
742
- - Validating file accessibility
743
-
744
- How it works:
745
- - Retrieves comprehensive file system metadata
746
- - Detects MIME type from file extension
747
- - Provides both absolute and relative paths
748
- - Returns human-readable file sizes
749
-
750
- Parameters:
751
- - path: File or directory path to inspect
752
- `,
753
- inputSchema: {
754
- path: z.string()
755
- }
756
- },
757
- async (input) => {
758
- try {
759
- return successToolResult(await getFileInfo(input));
760
- } catch (e) {
761
- if (e instanceof Error) return errorToolResult(e);
762
- throw e;
763
- }
764
- }
765
- );
766
- }
767
- async function listDirectory(input) {
768
- const { path: path2 } = input;
769
- const validatedPath = await validatePath(path2);
770
- if (!existsSync(validatedPath)) {
771
- throw new Error(`Directory ${path2} does not exist.`);
772
- }
773
- const stats = statSync(validatedPath);
774
- if (!stats.isDirectory()) {
775
- throw new Error(`Path ${path2} is not a directory.`);
776
- }
777
- const entries = await readdir(validatedPath);
778
- const items = [];
779
- for (const entry of entries.sort()) {
780
- try {
781
- const fullPath = await validatePath(join(validatedPath, entry));
782
- const entryStats = statSync(fullPath);
783
- const item = {
784
- name: entry,
785
- path: entry,
786
- type: entryStats.isDirectory() ? "directory" : "file",
787
- size: entryStats.size,
788
- modified: entryStats.mtime.toISOString()
789
- };
790
- items.push(item);
791
- } catch (e) {
792
- if (e instanceof Error && e.message.includes("perstack directory is not allowed")) {
793
- continue;
794
- }
795
- throw e;
796
- }
797
- }
798
- return {
799
- path: validatedPath,
800
- items
801
- };
802
- }
803
- function registerListDirectory(server) {
804
- server.registerTool(
805
- "listDirectory",
806
- {
807
- title: "List directory",
808
- description: dedent`
809
- Directory content lister with detailed file information.
810
-
811
- Use cases:
812
- - Exploring project structure
813
- - Finding files in a directory
814
- - Checking directory contents before operations
815
- - Understanding file organization
816
-
817
- How it works:
818
- - Lists all files and subdirectories in specified directory only
819
- - Provides file type, size, and modification time
820
- - Sorts entries alphabetically
821
- - Handles empty directories
822
-
823
- Parameters:
824
- - path: Directory path to list (optional, defaults to workspace root)
825
- `,
826
- inputSchema: {
827
- path: z.string()
828
- }
829
- },
830
- async (input) => {
831
- try {
832
- return successToolResult(await listDirectory(input));
833
- } catch (e) {
834
- if (e instanceof Error) return errorToolResult(e);
835
- throw e;
836
- }
837
- }
838
- );
839
- }
840
- async function moveFile(input) {
841
- const { source, destination } = input;
842
- const validatedSource = await validatePath(source);
843
- const validatedDestination = await validatePath(destination);
844
- if (!existsSync(validatedSource)) {
845
- throw new Error(`Source file ${source} does not exist.`);
846
- }
847
- const sourceStats = statSync(validatedSource);
848
- if (!(sourceStats.mode & 128)) {
849
- throw new Error(`Source file ${source} is not writable`);
850
- }
851
- if (existsSync(validatedDestination)) {
852
- throw new Error(`Destination ${destination} already exists.`);
853
- }
854
- const destDir = dirname(validatedDestination);
855
- await mkdir(destDir, { recursive: true });
856
- await rename(validatedSource, validatedDestination);
857
- return {
858
- source: validatedSource,
859
- destination: validatedDestination
860
- };
861
- }
862
- function registerMoveFile(server) {
863
- server.registerTool(
864
- "moveFile",
865
- {
866
- title: "Move file",
867
- description: dedent`
868
- File mover for relocating or renaming files within the workspace.
869
-
870
- Use cases:
871
- - Renaming files to follow naming conventions
872
- - Moving files to different directories
873
- - Organizing project structure
874
- - Backing up files before modifications
875
-
876
- How it works:
877
- - Validates source file existence
878
- - Creates destination directory if needed
879
- - Performs atomic move operation
880
- - Preserves file permissions and timestamps
881
-
882
- Parameters:
883
- - source: Current file path
884
- - destination: Target file path
885
- `,
886
- inputSchema: {
887
- source: z.string(),
888
- destination: z.string()
889
- }
890
- },
891
- async (input) => {
892
- try {
893
- return successToolResult(await moveFile(input));
894
- } catch (e) {
895
- if (e instanceof Error) return errorToolResult(e);
896
- throw e;
897
- }
898
- }
899
- );
900
- }
901
- var MAX_IMAGE_SIZE = 15 * 1024 * 1024;
902
- async function readImageFile(input) {
903
- const { path: path2 } = input;
904
- const validatedPath = await validatePath(path2);
905
- const isFile = existsSync(validatedPath);
906
- if (!isFile) {
907
- throw new Error(`File ${path2} does not exist.`);
908
- }
909
- const mimeType = mime.lookup(validatedPath);
910
- if (!mimeType || !["image/png", "image/jpeg", "image/gif", "image/webp"].includes(mimeType)) {
911
- throw new Error(`File ${path2} is not supported.`);
912
- }
913
- const fileStats = await stat(validatedPath);
914
- const fileSizeMB = fileStats.size / (1024 * 1024);
915
- if (fileStats.size > MAX_IMAGE_SIZE) {
916
- throw new Error(
917
- `Image file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB. Please use a smaller image file.`
918
- );
919
- }
920
- return {
921
- path: validatedPath,
922
- mimeType,
923
- size: fileStats.size
924
- };
925
- }
926
- function registerReadImageFile(server) {
927
- server.registerTool(
928
- "readImageFile",
929
- {
930
- title: "Read image file",
931
- description: dedent`
932
- Image file reader that converts images to base64 encoded strings with MIME type validation.
933
-
934
- Use cases:
935
- - Loading images for LLM to process
936
- - Retrieving image data for analysis or display
937
- - Converting workspace image files to base64 format
938
-
939
- How it works:
940
- - Validates file existence and MIME type before reading
941
- - Encodes file content as base64 string
942
- - Returns image data with correct MIME type for proper handling
943
- - Rejects unsupported formats with clear error messages
944
-
945
- Supported formats:
946
- - PNG (image/png)
947
- - JPEG/JPG (image/jpeg)
948
- - GIF (image/gif) - static only, animated not supported
949
- - WebP (image/webp)
950
-
951
- Notes:
952
- - Maximum file size: 15MB (larger files will be rejected)
953
- `,
954
- inputSchema: {
955
- path: z.string()
956
- }
957
- },
958
- async (input) => {
959
- try {
960
- return successToolResult(await readImageFile(input));
961
- } catch (e) {
962
- if (e instanceof Error) return errorToolResult(e);
963
- throw e;
964
- }
965
- }
966
- );
967
- }
968
- var MAX_PDF_SIZE = 30 * 1024 * 1024;
969
- async function readPdfFile(input) {
970
- const { path: path2 } = input;
971
- const validatedPath = await validatePath(path2);
972
- const isFile = existsSync(validatedPath);
973
- if (!isFile) {
974
- throw new Error(`File ${path2} does not exist.`);
975
- }
976
- const mimeType = mime.lookup(validatedPath);
977
- if (mimeType !== "application/pdf") {
978
- throw new Error(`File ${path2} is not a PDF file.`);
979
- }
980
- const fileStats = await stat(validatedPath);
981
- const fileSizeMB = fileStats.size / (1024 * 1024);
982
- if (fileStats.size > MAX_PDF_SIZE) {
983
- throw new Error(
984
- `PDF file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_PDF_SIZE / (1024 * 1024)}MB. Please use a smaller PDF file.`
985
- );
986
- }
987
- return {
988
- path: validatedPath,
989
- mimeType,
990
- size: fileStats.size
991
- };
992
- }
993
- function registerReadPdfFile(server) {
994
- server.registerTool(
995
- "readPdfFile",
996
- {
997
- title: "Read PDF file",
998
- description: dedent`
999
- PDF file reader that converts documents to base64 encoded resources.
1000
-
1001
- Use cases:
1002
- - Extracting content from PDF documents for analysis
1003
- - Loading PDF files for LLM processing
1004
- - Retrieving PDF data for conversion or manipulation
1005
-
1006
- How it works:
1007
- - Validates file existence and MIME type (application/pdf)
1008
- - Encodes PDF content as base64 blob
1009
- - Returns as resource type with proper MIME type and URI
1010
- - Rejects non-PDF files with clear error messages
1011
-
1012
- Notes:
1013
- - Returns entire PDF content, no page range support
1014
- - Maximum file size: 30MB (larger files will be rejected)
1015
- - Text extraction not performed, returns raw PDF data
1016
- `,
1017
- inputSchema: {
1018
- path: z.string()
1019
- }
1020
- },
1021
- async (input) => {
1022
- try {
1023
- return successToolResult(await readPdfFile(input));
1024
- } catch (e) {
1025
- if (e instanceof Error) return errorToolResult(e);
1026
- throw e;
1027
- }
1028
- }
1029
- );
1030
- }
1031
- async function readTextFile(input) {
1032
- const { path: path2, from, to } = input;
1033
- const validatedPath = await validatePath(path2);
1034
- const stats = await stat(validatedPath).catch(() => null);
1035
- if (!stats) {
1036
- throw new Error(`File ${path2} does not exist.`);
1037
- }
1038
- const fileContent = await safeReadFile(validatedPath);
1039
- const lines = fileContent.split("\n");
1040
- const fromLine = from ?? 0;
1041
- const toLine = to ?? lines.length;
1042
- const selectedLines = lines.slice(fromLine, toLine);
1043
- const content = selectedLines.join("\n");
1044
- return {
1045
- path: path2,
1046
- content,
1047
- from: fromLine,
1048
- to: toLine
1049
- };
1050
- }
1051
- function registerReadTextFile(server) {
1052
- server.registerTool(
1053
- "readTextFile",
1054
- {
1055
- title: "Read text file",
1056
- description: dedent`
1057
- Text file reader with line range support for UTF-8 encoded files.
1058
-
1059
- Use cases:
1060
- - Reading source code files for analysis
1061
- - Extracting specific sections from large text files
1062
- - Loading configuration or documentation files
1063
- - Viewing log files or data files
1064
-
1065
- How it works:
1066
- - Reads files as UTF-8 encoded text without format validation
1067
- - Supports partial file reading via line number ranges
1068
- - Returns content wrapped in JSON with metadata
1069
- - WARNING: Binary files will cause errors or corrupted output
1070
-
1071
- Common file types:
1072
- - Source code: .ts, .js, .py, .java, .cpp, etc.
1073
- - Documentation: .md, .txt, .rst
1074
- - Configuration: .json, .yaml, .toml, .ini
1075
- - Data files: .csv, .log, .sql
1076
- `,
1077
- inputSchema: {
1078
- path: z.string(),
1079
- from: z.number().optional().describe("The line number to start reading from."),
1080
- to: z.number().optional().describe("The line number to stop reading at.")
1081
- }
1082
- },
1083
- async (input) => {
1084
- try {
1085
- return successToolResult(await readTextFile(input));
1086
- } catch (e) {
1087
- if (e instanceof Error) return errorToolResult(e);
1088
- throw e;
1089
- }
1090
- }
1091
- );
1092
- }
1093
- async function writeTextFile(input) {
1094
- const { path: path2, text } = input;
1095
- const validatedPath = await validatePath(path2);
1096
- const stats = await stat(validatedPath).catch(() => null);
1097
- if (stats && !(stats.mode & 128)) {
1098
- throw new Error(`File ${path2} is not writable`);
1099
- }
1100
- const dir = dirname(validatedPath);
1101
- await mkdir(dir, { recursive: true });
1102
- await safeWriteFile(validatedPath, text);
1103
- return {
1104
- path: validatedPath,
1105
- text
1106
- };
1107
- }
1108
- function registerWriteTextFile(server) {
1109
- server.registerTool(
1110
- "writeTextFile",
1111
- {
1112
- title: "writeTextFile",
1113
- description: dedent`
1114
- Text file writer that creates or overwrites files with UTF-8 content.
1115
-
1116
- Use cases:
1117
- - Creating new configuration files
1118
- - Writing generated code or documentation
1119
- - Saving processed data or results
1120
- - Creating log files or reports
1121
-
1122
- How it works:
1123
- - Writes content as UTF-8 encoded text
1124
- - Returns success status with file path
1125
-
1126
- Rules:
1127
- - IF THE FILE ALREADY EXISTS, IT WILL BE OVERWRITTEN
1128
- - YOU MUST PROVIDE A VALID UTF-8 STRING FOR THE TEXT
1129
- `,
1130
- inputSchema: {
1131
- path: z.string().describe("Target file path (relative or absolute)."),
1132
- text: z.string().describe("Text to write to the file.")
1133
- }
1134
- },
1135
- async (input) => {
1136
- try {
1137
- return successToolResult(await writeTextFile(input));
1138
- } catch (e) {
1139
- if (e instanceof Error) return errorToolResult(e);
1140
- throw e;
1141
- }
1142
- }
1143
- );
1144
- }
1145
- var startTime = Date.now();
1146
- async function healthCheck() {
1147
- const uptime = Math.floor((Date.now() - startTime) / 1e3);
1148
- const memoryUsage = process.memoryUsage();
1149
- return {
1150
- status: "ok",
1151
- workspace: workspacePath,
1152
- uptime: `${uptime}s`,
1153
- memory: {
1154
- heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)}MB`,
1155
- heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)}MB`
1156
- },
1157
- pid: process.pid
1158
- };
1159
- }
1160
- function registerHealthCheck(server) {
1161
- server.registerTool(
1162
- "healthCheck",
1163
- {
1164
- title: "Perstack Runtime Health Check",
1165
- description: dedent`
1166
- Returns Perstack runtime health status and diagnostics.
1167
- Use cases:
1168
- - Verify Perstack runtime is running and responsive
1169
- - Check workspace configuration
1170
- - Monitor runtime uptime and memory usage
1171
- How it works:
1172
- - Returns runtime status, workspace path, uptime, and memory usage
1173
- - Always returns "ok" status if runtime can respond
1174
- - Useful for debugging connection issues
1175
- Notes:
1176
- - This is a diagnostic tool for Perstack runtime itself
1177
- - Does not access or modify files
1178
- `,
1179
- inputSchema: {}
1180
- },
1181
- async () => successToolResult(await healthCheck())
1182
- );
1183
- }
1184
-
1185
- // src/server.ts
1186
- var BASE_SKILL_NAME = package_default.name;
1187
- var BASE_SKILL_VERSION = package_default.version;
1188
- function registerAllTools(server) {
1189
- registerAttemptCompletion(server);
1190
- registerTodo(server);
1191
- registerClearTodo(server);
1192
- registerExec(server);
1193
- registerGetFileInfo(server);
1194
- registerHealthCheck(server);
1195
- registerReadTextFile(server);
1196
- registerReadImageFile(server);
1197
- registerReadPdfFile(server);
1198
- registerWriteTextFile(server);
1199
- registerAppendTextFile(server);
1200
- registerEditTextFile(server);
1201
- registerMoveFile(server);
1202
- registerDeleteFile(server);
1203
- registerListDirectory(server);
1204
- registerCreateDirectory(server);
1205
- registerDeleteDirectory(server);
1206
- }
1207
- function createBaseServer() {
1208
- const server = new McpServer(
1209
- {
1210
- name: BASE_SKILL_NAME,
1211
- version: BASE_SKILL_VERSION
1212
- },
1213
- {
1214
- capabilities: {
1215
- tools: {}
1216
- }
1217
- }
1218
- );
1219
- registerAllTools(server);
1220
- return server;
1221
- }
1222
-
1223
- export { BASE_SKILL_NAME, BASE_SKILL_VERSION, appendTextFile, attemptCompletion, clearTodo, createBaseServer, createDirectory, deleteDirectory, deleteFile, editTextFile, errorToolResult, exec, getFileInfo, getRemainingTodos, listDirectory, moveFile, package_default, readImageFile, readPdfFile, readTextFile, registerAllTools, registerAppendTextFile, registerAttemptCompletion, registerClearTodo, registerCreateDirectory, registerDeleteDirectory, registerDeleteFile, registerEditTextFile, registerExec, registerGetFileInfo, registerListDirectory, registerMoveFile, registerReadImageFile, registerReadPdfFile, registerReadTextFile, registerTodo, registerWriteTextFile, successToolResult, todo, validatePath, writeTextFile };
1224
- //# sourceMappingURL=chunk-HUJMWPRE.js.map
1225
- //# sourceMappingURL=chunk-HUJMWPRE.js.map