0xkobold 0.0.1

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 (258) hide show
  1. package/.agents/skills/nextjs-best-practices/SKILL.md +208 -0
  2. package/.agents/skills/sql-optimization-patterns/SKILL.md +509 -0
  3. package/HEARTBEAT.md +45 -0
  4. package/README.md +197 -0
  5. package/USAGE.md +191 -0
  6. package/dist/package.json +77 -0
  7. package/dist/src/agent/pi-adapter.js +307 -0
  8. package/dist/src/agent/pi-adapter.js.map +1 -0
  9. package/dist/src/agent/tool-adapter.js +86 -0
  10. package/dist/src/agent/tool-adapter.js.map +1 -0
  11. package/dist/src/approval/queue.js +114 -0
  12. package/dist/src/approval/queue.js.map +1 -0
  13. package/dist/src/ascii-kobold.js +76 -0
  14. package/dist/src/ascii-kobold.js.map +1 -0
  15. package/dist/src/cli/client.js +217 -0
  16. package/dist/src/cli/client.js.map +1 -0
  17. package/dist/src/cli/commands/agent.js +272 -0
  18. package/dist/src/cli/commands/agent.js.map +1 -0
  19. package/dist/src/cli/commands/chat.js +234 -0
  20. package/dist/src/cli/commands/chat.js.map +1 -0
  21. package/dist/src/cli/commands/config.js +202 -0
  22. package/dist/src/cli/commands/config.js.map +1 -0
  23. package/dist/src/cli/commands/daemon.js +203 -0
  24. package/dist/src/cli/commands/daemon.js.map +1 -0
  25. package/dist/src/cli/commands/gateway.js +184 -0
  26. package/dist/src/cli/commands/gateway.js.map +1 -0
  27. package/dist/src/cli/commands/init.js +175 -0
  28. package/dist/src/cli/commands/init.js.map +1 -0
  29. package/dist/src/cli/commands/kobold.js +21 -0
  30. package/dist/src/cli/commands/kobold.js.map +1 -0
  31. package/dist/src/cli/commands/logs.js +27 -0
  32. package/dist/src/cli/commands/logs.js.map +1 -0
  33. package/dist/src/cli/commands/mode.js +121 -0
  34. package/dist/src/cli/commands/mode.js.map +1 -0
  35. package/dist/src/cli/commands/persona.js +261 -0
  36. package/dist/src/cli/commands/persona.js.map +1 -0
  37. package/dist/src/cli/commands/start.js +66 -0
  38. package/dist/src/cli/commands/start.js.map +1 -0
  39. package/dist/src/cli/commands/status.js +117 -0
  40. package/dist/src/cli/commands/status.js.map +1 -0
  41. package/dist/src/cli/commands/stop.js +27 -0
  42. package/dist/src/cli/commands/stop.js.map +1 -0
  43. package/dist/src/cli/commands/system.js +128 -0
  44. package/dist/src/cli/commands/system.js.map +1 -0
  45. package/dist/src/cli/commands/tui.js +103 -0
  46. package/dist/src/cli/commands/tui.js.map +1 -0
  47. package/dist/src/cli/commands/update.js +133 -0
  48. package/dist/src/cli/commands/update.js.map +1 -0
  49. package/dist/src/cli/extensions/discord.js +113 -0
  50. package/dist/src/cli/extensions/discord.js.map +1 -0
  51. package/dist/src/cli/extensions/env.js +91 -0
  52. package/dist/src/cli/extensions/env.js.map +1 -0
  53. package/dist/src/cli/extensions/heartbeat.js +78 -0
  54. package/dist/src/cli/extensions/heartbeat.js.map +1 -0
  55. package/dist/src/cli/index.js +24 -0
  56. package/dist/src/cli/index.js.map +1 -0
  57. package/dist/src/cli/program.js +70 -0
  58. package/dist/src/cli/program.js.map +1 -0
  59. package/dist/src/cli/repl.js +184 -0
  60. package/dist/src/cli/repl.js.map +1 -0
  61. package/dist/src/cli/shared/discord-service.js +102 -0
  62. package/dist/src/cli/shared/discord-service.js.map +1 -0
  63. package/dist/src/config/index.js +10 -0
  64. package/dist/src/config/index.js.map +1 -0
  65. package/dist/src/config/loader.js +401 -0
  66. package/dist/src/config/loader.js.map +1 -0
  67. package/dist/src/config/paths.js +84 -0
  68. package/dist/src/config/paths.js.map +1 -0
  69. package/dist/src/config/types.js +8 -0
  70. package/dist/src/config/types.js.map +1 -0
  71. package/dist/src/context-detector.js +60 -0
  72. package/dist/src/context-detector.js.map +1 -0
  73. package/dist/src/discord/index.js +376 -0
  74. package/dist/src/discord/index.js.map +1 -0
  75. package/dist/src/event-bus/index.js +97 -0
  76. package/dist/src/event-bus/index.js.map +1 -0
  77. package/dist/src/extensions/command-args.js +68 -0
  78. package/dist/src/extensions/command-args.js.map +1 -0
  79. package/dist/src/extensions/core/agent-registry-extension.js +541 -0
  80. package/dist/src/extensions/core/agent-registry-extension.js.map +1 -0
  81. package/dist/src/extensions/core/agent-worker.js +148 -0
  82. package/dist/src/extensions/core/agent-worker.js.map +1 -0
  83. package/dist/src/extensions/core/compaction-safeguard.js +154 -0
  84. package/dist/src/extensions/core/compaction-safeguard.js.map +1 -0
  85. package/dist/src/extensions/core/confirm-destructive.js +43 -0
  86. package/dist/src/extensions/core/confirm-destructive.js.map +1 -0
  87. package/dist/src/extensions/core/context-aware-extension.js +124 -0
  88. package/dist/src/extensions/core/context-aware-extension.js.map +1 -0
  89. package/dist/src/extensions/core/context-pruning/extension.js +124 -0
  90. package/dist/src/extensions/core/context-pruning/extension.js.map +1 -0
  91. package/dist/src/extensions/core/context-pruning/pruner.js +312 -0
  92. package/dist/src/extensions/core/context-pruning/pruner.js.map +1 -0
  93. package/dist/src/extensions/core/context-pruning/runtime.js +48 -0
  94. package/dist/src/extensions/core/context-pruning/runtime.js.map +1 -0
  95. package/dist/src/extensions/core/context-pruning/settings.js +105 -0
  96. package/dist/src/extensions/core/context-pruning/settings.js.map +1 -0
  97. package/dist/src/extensions/core/dirty-repo-guard.js +42 -0
  98. package/dist/src/extensions/core/dirty-repo-guard.js.map +1 -0
  99. package/dist/src/extensions/core/discord-channel-extension.js +205 -0
  100. package/dist/src/extensions/core/discord-channel-extension.js.map +1 -0
  101. package/dist/src/extensions/core/discord-extension.js +142 -0
  102. package/dist/src/extensions/core/discord-extension.js.map +1 -0
  103. package/dist/src/extensions/core/env-loader-extension.js +157 -0
  104. package/dist/src/extensions/core/env-loader-extension.js.map +1 -0
  105. package/dist/src/extensions/core/fileops-extension.js +699 -0
  106. package/dist/src/extensions/core/fileops-extension.js.map +1 -0
  107. package/dist/src/extensions/core/gateway-extension.js +730 -0
  108. package/dist/src/extensions/core/gateway-extension.js.map +1 -0
  109. package/dist/src/extensions/core/git-checkpoint.js +46 -0
  110. package/dist/src/extensions/core/git-checkpoint.js.map +1 -0
  111. package/dist/src/extensions/core/handoff-extension.js +206 -0
  112. package/dist/src/extensions/core/handoff-extension.js.map +1 -0
  113. package/dist/src/extensions/core/heartbeat-extension.js +373 -0
  114. package/dist/src/extensions/core/heartbeat-extension.js.map +1 -0
  115. package/dist/src/extensions/core/mcp-extension.js +413 -0
  116. package/dist/src/extensions/core/mcp-extension.js.map +1 -0
  117. package/dist/src/extensions/core/mode-manager-extension.js +562 -0
  118. package/dist/src/extensions/core/mode-manager-extension.js.map +1 -0
  119. package/dist/src/extensions/core/multi-channel-extension.js +435 -0
  120. package/dist/src/extensions/core/multi-channel-extension.js.map +1 -0
  121. package/dist/src/extensions/core/ollama-provider-extension.js +66 -0
  122. package/dist/src/extensions/core/ollama-provider-extension.js.map +1 -0
  123. package/dist/src/extensions/core/onboarding-extension.js +122 -0
  124. package/dist/src/extensions/core/onboarding-extension.js.map +1 -0
  125. package/dist/src/extensions/core/persona-loader-extension.js +139 -0
  126. package/dist/src/extensions/core/persona-loader-extension.js.map +1 -0
  127. package/dist/src/extensions/core/pi-notify-extension.js +70 -0
  128. package/dist/src/extensions/core/pi-notify-extension.js.map +1 -0
  129. package/dist/src/extensions/core/protected-paths.js +24 -0
  130. package/dist/src/extensions/core/protected-paths.js.map +1 -0
  131. package/dist/src/extensions/core/questionnaire-extension.js +242 -0
  132. package/dist/src/extensions/core/questionnaire-extension.js.map +1 -0
  133. package/dist/src/extensions/core/self-update-extension.js +181 -0
  134. package/dist/src/extensions/core/self-update-extension.js.map +1 -0
  135. package/dist/src/extensions/core/session-bridge-extension.js +78 -0
  136. package/dist/src/extensions/core/session-bridge-extension.js.map +1 -0
  137. package/dist/src/extensions/core/session-manager-extension.js +319 -0
  138. package/dist/src/extensions/core/session-manager-extension.js.map +1 -0
  139. package/dist/src/extensions/core/session-name-extension.js +88 -0
  140. package/dist/src/extensions/core/session-name-extension.js.map +1 -0
  141. package/dist/src/extensions/core/session-pruning-extension.js +480 -0
  142. package/dist/src/extensions/core/session-pruning-extension.js.map +1 -0
  143. package/dist/src/extensions/core/task-manager-extension.js +661 -0
  144. package/dist/src/extensions/core/task-manager-extension.js.map +1 -0
  145. package/dist/src/extensions/core/update-extension.js +438 -0
  146. package/dist/src/extensions/core/update-extension.js.map +1 -0
  147. package/dist/src/extensions/core/websearch-extension.js +463 -0
  148. package/dist/src/extensions/core/websearch-extension.js.map +1 -0
  149. package/dist/src/extensions/index.js +5 -0
  150. package/dist/src/extensions/index.js.map +1 -0
  151. package/dist/src/extensions/loader.js +80 -0
  152. package/dist/src/extensions/loader.js.map +1 -0
  153. package/dist/src/gateway/index.js +353 -0
  154. package/dist/src/gateway/index.js.map +1 -0
  155. package/dist/src/index.js +150 -0
  156. package/dist/src/index.js.map +1 -0
  157. package/dist/src/llm/anthropic.js +86 -0
  158. package/dist/src/llm/anthropic.js.map +1 -0
  159. package/dist/src/llm/index.js +9 -0
  160. package/dist/src/llm/index.js.map +1 -0
  161. package/dist/src/llm/ollama.js +113 -0
  162. package/dist/src/llm/ollama.js.map +1 -0
  163. package/dist/src/llm/router.js +145 -0
  164. package/dist/src/llm/router.js.map +1 -0
  165. package/dist/src/llm/types.js +7 -0
  166. package/dist/src/llm/types.js.map +1 -0
  167. package/dist/src/memory/index.js +5 -0
  168. package/dist/src/memory/index.js.map +1 -0
  169. package/dist/src/memory/store.js +91 -0
  170. package/dist/src/memory/store.js.map +1 -0
  171. package/dist/src/pi-config.js +80 -0
  172. package/dist/src/pi-config.js.map +1 -0
  173. package/dist/src/skills/builtin/file.js +184 -0
  174. package/dist/src/skills/builtin/file.js.map +1 -0
  175. package/dist/src/skills/builtin/shell.js +100 -0
  176. package/dist/src/skills/builtin/shell.js.map +1 -0
  177. package/dist/src/skills/builtin/subagent.js +62 -0
  178. package/dist/src/skills/builtin/subagent.js.map +1 -0
  179. package/dist/src/skills/hello.js +42 -0
  180. package/dist/src/skills/hello.js.map +1 -0
  181. package/dist/src/skills/index.js +11 -0
  182. package/dist/src/skills/index.js.map +1 -0
  183. package/dist/src/skills/loader.js +382 -0
  184. package/dist/src/skills/loader.js.map +1 -0
  185. package/dist/src/skills/types.js +8 -0
  186. package/dist/src/skills/types.js.map +1 -0
  187. package/dist/src/utils/working-dir.js +71 -0
  188. package/dist/src/utils/working-dir.js.map +1 -0
  189. package/package.json +77 -0
  190. package/skills/1password/SKILL.md +70 -0
  191. package/skills/1password/references/cli-examples.md +29 -0
  192. package/skills/1password/references/get-started.md +17 -0
  193. package/skills/apple-notes/SKILL.md +77 -0
  194. package/skills/apple-reminders/SKILL.md +118 -0
  195. package/skills/bear-notes/SKILL.md +107 -0
  196. package/skills/blogwatcher/SKILL.md +69 -0
  197. package/skills/blucli/SKILL.md +47 -0
  198. package/skills/bluebubbles/SKILL.md +131 -0
  199. package/skills/camsnap/SKILL.md +45 -0
  200. package/skills/canvas/SKILL.md +198 -0
  201. package/skills/clawhub/SKILL.md +77 -0
  202. package/skills/coding-agent/SKILL.md +284 -0
  203. package/skills/discord/SKILL.md +197 -0
  204. package/skills/eightctl/SKILL.md +50 -0
  205. package/skills/food-order/SKILL.md +48 -0
  206. package/skills/gemini/SKILL.md +43 -0
  207. package/skills/gh-issues/SKILL.md +865 -0
  208. package/skills/gifgrep/SKILL.md +79 -0
  209. package/skills/github/SKILL.md +163 -0
  210. package/skills/gog/SKILL.md +116 -0
  211. package/skills/goplaces/SKILL.md +52 -0
  212. package/skills/healthcheck/SKILL.md +245 -0
  213. package/skills/himalaya/SKILL.md +257 -0
  214. package/skills/himalaya/references/configuration.md +184 -0
  215. package/skills/himalaya/references/message-composition.md +199 -0
  216. package/skills/imsg/SKILL.md +122 -0
  217. package/skills/mcporter/SKILL.md +61 -0
  218. package/skills/model-usage/SKILL.md +69 -0
  219. package/skills/model-usage/references/codexbar-cli.md +33 -0
  220. package/skills/model-usage/scripts/model_usage.py +310 -0
  221. package/skills/nano-banana-pro/SKILL.md +58 -0
  222. package/skills/nano-banana-pro/scripts/generate_image.py +184 -0
  223. package/skills/nano-pdf/SKILL.md +38 -0
  224. package/skills/notion/SKILL.md +172 -0
  225. package/skills/obsidian/SKILL.md +81 -0
  226. package/skills/openai-image-gen/SKILL.md +89 -0
  227. package/skills/openai-image-gen/scripts/gen.py +240 -0
  228. package/skills/openai-whisper/SKILL.md +38 -0
  229. package/skills/openai-whisper-api/SKILL.md +52 -0
  230. package/skills/openai-whisper-api/scripts/transcribe.sh +85 -0
  231. package/skills/openhue/SKILL.md +112 -0
  232. package/skills/oracle/SKILL.md +125 -0
  233. package/skills/ordercli/SKILL.md +78 -0
  234. package/skills/peekaboo/SKILL.md +190 -0
  235. package/skills/sag/SKILL.md +87 -0
  236. package/skills/session-logs/SKILL.md +115 -0
  237. package/skills/sherpa-onnx-tts/SKILL.md +103 -0
  238. package/skills/sherpa-onnx-tts/bin/sherpa-onnx-tts +178 -0
  239. package/skills/skill-creator/SKILL.md +370 -0
  240. package/skills/skill-creator/license.txt +202 -0
  241. package/skills/skill-creator/scripts/init_skill.py +378 -0
  242. package/skills/skill-creator/scripts/package_skill.py +111 -0
  243. package/skills/skill-creator/scripts/quick_validate.py +101 -0
  244. package/skills/slack/SKILL.md +144 -0
  245. package/skills/songsee/SKILL.md +49 -0
  246. package/skills/sonoscli/SKILL.md +46 -0
  247. package/skills/spotify-player/SKILL.md +64 -0
  248. package/skills/summarize/SKILL.md +87 -0
  249. package/skills/things-mac/SKILL.md +86 -0
  250. package/skills/tmux/SKILL.md +153 -0
  251. package/skills/tmux/scripts/find-sessions.sh +112 -0
  252. package/skills/tmux/scripts/wait-for-text.sh +83 -0
  253. package/skills/trello/SKILL.md +95 -0
  254. package/skills/video-frames/SKILL.md +46 -0
  255. package/skills/video-frames/scripts/frame.sh +81 -0
  256. package/skills/voice-call/SKILL.md +45 -0
  257. package/skills/wacli/SKILL.md +72 -0
  258. package/skills/weather/SKILL.md +112 -0
@@ -0,0 +1,699 @@
1
+ /**
2
+ * File Operations Extension for 0xKobold
3
+ *
4
+ * Provides file system operations as pi-coding-agent tools
5
+ * Ported and enhanced from core/tools.ts
6
+ */
7
+ import { $ } from 'bun';
8
+ import { join, dirname } from 'path';
9
+ import { existsSync, statSync, readdirSync, mkdirSync, } from 'fs';
10
+ import { readFile, writeFile as writeFileAsync } from 'fs/promises';
11
+ import { glob } from 'glob';
12
+ import { getWorkingDir, validatePathWithinWorkspace, } from '../../utils/working-dir.js';
13
+ // Security configuration
14
+ const MAX_FILE_SIZE = 1024 * 1024; // 1MB
15
+ const SHELL_TIMEOUT_DEFAULT = 30000; // 30 seconds
16
+ // Dangerous shell commands to block
17
+ const DANGEROUS_COMMANDS = [
18
+ 'rm -rf /',
19
+ 'dd if=',
20
+ 'mkfs.',
21
+ ':(){ :|:& };:', // fork bomb
22
+ 'curl.*|.*sh', // pipe curl to shell
23
+ 'wget.*|.*sh', // pipe wget to shell
24
+ 'curl.*|.*bash',
25
+ 'wget.*|.*bash',
26
+ '> /dev/',
27
+ '/dev/null;',
28
+ 'chmod 000',
29
+ 'chmod -R 000',
30
+ ];
31
+ /**
32
+ * Validate path - delegates to shared utility with extension-specific error handling
33
+ */
34
+ function validatePath(inputPath) {
35
+ const result = validatePathWithinWorkspace(inputPath);
36
+ if ('error' in result) {
37
+ return { valid: false, error: result.error, resolvedPath: result.resolvedPath };
38
+ }
39
+ return { valid: true, resolvedPath: result.resolvedPath };
40
+ }
41
+ /**
42
+ * Check if command is safe to execute
43
+ */
44
+ function isCommandSafe(command) {
45
+ const lowerCommand = command.toLowerCase();
46
+ for (const pattern of DANGEROUS_COMMANDS) {
47
+ const regex = new RegExp(pattern, 'i');
48
+ if (regex.test(lowerCommand)) {
49
+ return { safe: false, reason: `Command matches dangerous pattern: ${pattern}` };
50
+ }
51
+ }
52
+ return { safe: true };
53
+ }
54
+ /**
55
+ * File Operations Extension
56
+ */
57
+ export default function fileOpsExtension(pi) {
58
+ // Initialize working directory from CLI flag if provided so that tests and
59
+ // local mode can control the workspace root for all file operations.
60
+ const workingDirFlag = pi.getFlag?.('working-dir');
61
+ if (workingDirFlag) {
62
+ process.env.KOBOLD_WORKING_DIR = String(workingDirFlag);
63
+ }
64
+ /**
65
+ * Tool: read_file_with_line_numbers
66
+ * Read file with line numbers for context
67
+ */
68
+ pi.registerTool({
69
+ name: 'read_file_with_line_numbers',
70
+ description: 'Read the contents of a file with line numbers prepended. Useful for viewing code with context. Max file size 1MB.',
71
+ // @ts-ignore TSchema mismatch
72
+ parameters: {
73
+ type: 'object',
74
+ properties: {
75
+ path: {
76
+ type: 'string',
77
+ description: 'The path to the file to read',
78
+ },
79
+ offset: {
80
+ type: 'number',
81
+ description: 'Line number to start reading from (0-indexed, default: 0)',
82
+ },
83
+ limit: {
84
+ type: 'number',
85
+ description: 'Maximum number of lines to read (default: read all)',
86
+ },
87
+ },
88
+ required: ['path'],
89
+ },
90
+ async execute(args) {
91
+ try {
92
+ const filePath = String(args.path);
93
+ const offset = typeof args.offset === 'number' ? args.offset : 0;
94
+ const limit = typeof args.limit === 'number' ? args.limit : undefined;
95
+ // Validate path
96
+ const pathCheck = validatePath(filePath);
97
+ if (!pathCheck.valid) {
98
+ // If the resolved path does not exist, surface a file_not_found error
99
+ // instead of a workspace escape error (for test and UX friendliness).
100
+ if (!existsSync(pathCheck.resolvedPath)) {
101
+ return {
102
+ content: [{ type: 'text', text: `File not found: ${filePath}` }],
103
+ details: { error: 'file_not_found' },
104
+ };
105
+ }
106
+ return {
107
+ content: [{ type: 'text', text: `Path validation failed: ${pathCheck.error}` }],
108
+ details: { error: pathCheck.error },
109
+ };
110
+ }
111
+ const resolvedPath = pathCheck.resolvedPath;
112
+ // Check if file exists
113
+ if (!existsSync(resolvedPath)) {
114
+ return {
115
+ content: [{ type: 'text', text: `File not found: ${filePath}` }],
116
+ details: { error: 'file_not_found' },
117
+ };
118
+ }
119
+ const stats = statSync(resolvedPath);
120
+ // Check if it's a directory
121
+ if (stats.isDirectory()) {
122
+ return {
123
+ content: [{ type: 'text', text: `Path is a directory: ${filePath}` }],
124
+ details: { error: 'is_directory' },
125
+ };
126
+ }
127
+ // Check file size
128
+ if (stats.size > MAX_FILE_SIZE) {
129
+ return {
130
+ content: [
131
+ {
132
+ type: 'text',
133
+ text: `File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Max size is 1MB. Use offset and limit parameters.`,
134
+ },
135
+ ],
136
+ details: { error: 'file_too_large', size: stats.size },
137
+ };
138
+ }
139
+ // Read file content
140
+ const content = await readFile(resolvedPath, 'utf-8');
141
+ const lines = content.split('\n');
142
+ const endLine = limit ? Math.min(offset + limit, lines.length) : lines.length;
143
+ const selectedLines = lines.slice(offset, endLine);
144
+ // Prepend line numbers (1-indexed for human readability)
145
+ const numberedLines = selectedLines.map((line, idx) => {
146
+ const lineNumber = offset + idx + 1;
147
+ const padding = String(endLine).length;
148
+ return `${String(lineNumber).padStart(padding, ' ')} | ${line}`;
149
+ });
150
+ const result = numberedLines.join('\n');
151
+ return {
152
+ content: [
153
+ { type: 'text', text: `File: ${filePath} (${lines.length} lines)` },
154
+ { type: 'text', text: result },
155
+ ],
156
+ details: {
157
+ path: filePath,
158
+ size: stats.size,
159
+ totalLines: lines.length,
160
+ displayedLines: selectedLines.length,
161
+ startLine: offset + 1,
162
+ endLine,
163
+ },
164
+ };
165
+ }
166
+ catch (error) {
167
+ return {
168
+ content: [{ type: 'text', text: `Error reading file: ${error}` }],
169
+ details: { error: String(error) },
170
+ };
171
+ }
172
+ },
173
+ });
174
+ /**
175
+ * Tool: write_file
176
+ * Write content to a file
177
+ */
178
+ pi.registerTool({
179
+ name: 'write_file',
180
+ description: 'Write content to a file at the specified path. Creates directories if needed. Can append to existing files.',
181
+ // @ts-ignore TSchema mismatch
182
+ parameters: {
183
+ type: 'object',
184
+ properties: {
185
+ path: {
186
+ type: 'string',
187
+ description: 'The path to the file to write',
188
+ },
189
+ content: {
190
+ type: 'string',
191
+ description: 'The content to write to the file',
192
+ },
193
+ append: {
194
+ type: 'boolean',
195
+ description: 'If true, append to existing file instead of overwriting (default: false)',
196
+ },
197
+ },
198
+ required: ['path', 'content'],
199
+ },
200
+ async execute(args) {
201
+ try {
202
+ const filePath = String(args.path);
203
+ const content = String(args.content);
204
+ const append = args.append === true;
205
+ // Validate path
206
+ const pathCheck = validatePath(filePath);
207
+ if (!pathCheck.valid) {
208
+ return {
209
+ content: [{ type: 'text', text: `Path validation failed: ${pathCheck.error}` }],
210
+ details: { error: pathCheck.error },
211
+ };
212
+ }
213
+ const resolvedPath = pathCheck.resolvedPath;
214
+ const dir = dirname(resolvedPath);
215
+ // Create directory if needed
216
+ if (!existsSync(dir)) {
217
+ mkdirSync(dir, { recursive: true });
218
+ }
219
+ // Write or append file
220
+ if (append && existsSync(resolvedPath)) {
221
+ const existing = await readFile(resolvedPath, 'utf-8');
222
+ await writeFileAsync(resolvedPath, existing + content, 'utf-8');
223
+ }
224
+ else {
225
+ await writeFileAsync(resolvedPath, content, 'utf-8');
226
+ }
227
+ const bytes = Buffer.byteLength(content, 'utf-8');
228
+ return {
229
+ content: [
230
+ {
231
+ type: 'text',
232
+ text: `Successfully ${append ? 'appended to' : 'wrote'} file: ${filePath}`,
233
+ },
234
+ ],
235
+ details: {
236
+ path: filePath,
237
+ bytes,
238
+ append,
239
+ },
240
+ };
241
+ }
242
+ catch (error) {
243
+ return {
244
+ content: [{ type: 'text', text: `Error writing file: ${error}` }],
245
+ details: { error: String(error) },
246
+ };
247
+ }
248
+ },
249
+ });
250
+ /**
251
+ * Tool: list_directory
252
+ * List directory contents
253
+ */
254
+ pi.registerTool({
255
+ name: 'list_directory',
256
+ description: 'List the contents of a directory. Shows file and folder icons. Can optionally recurse into subdirectories.',
257
+ // @ts-ignore TSchema mismatch
258
+ parameters: {
259
+ type: 'object',
260
+ properties: {
261
+ path: {
262
+ type: 'string',
263
+ description: 'The path to the directory to list',
264
+ },
265
+ recursive: {
266
+ type: 'boolean',
267
+ description: 'If true, recursively list all subdirectories (default: false)',
268
+ },
269
+ },
270
+ required: ['path'],
271
+ },
272
+ async execute(args) {
273
+ try {
274
+ const dirPath = String(args.path);
275
+ const recursive = args.recursive === true;
276
+ // Validate path
277
+ const pathCheck = validatePath(dirPath);
278
+ if (!pathCheck.valid) {
279
+ // Treat non-existent paths as directory_not_found for friendlier errors
280
+ if (!existsSync(pathCheck.resolvedPath)) {
281
+ return {
282
+ content: [{ type: 'text', text: `Directory not found: ${dirPath}` }],
283
+ details: { error: 'directory_not_found' },
284
+ };
285
+ }
286
+ return {
287
+ content: [{ type: 'text', text: `Path validation failed: ${pathCheck.error}` }],
288
+ details: { error: pathCheck.error },
289
+ };
290
+ }
291
+ const resolvedPath = pathCheck.resolvedPath;
292
+ // Check if directory exists
293
+ if (!existsSync(resolvedPath)) {
294
+ return {
295
+ content: [{ type: 'text', text: `Directory not found: ${dirPath}` }],
296
+ details: { error: 'directory_not_found' },
297
+ };
298
+ }
299
+ const stats = statSync(resolvedPath);
300
+ // Check if it's actually a directory
301
+ if (!stats.isDirectory()) {
302
+ return {
303
+ content: [{ type: 'text', text: `Path is not a directory: ${dirPath}` }],
304
+ details: { error: 'not_a_directory' },
305
+ };
306
+ }
307
+ if (recursive) {
308
+ const entries = [];
309
+ function walk(dir, prefix = '') {
310
+ const items = readdirSync(dir);
311
+ for (const item of items) {
312
+ // Skip hidden files/directories
313
+ if (item.startsWith('.'))
314
+ continue;
315
+ const fullPath = join(dir, item);
316
+ const relPath = prefix ? `${prefix}/${item}` : item;
317
+ const itemStats = statSync(fullPath);
318
+ entries.push(`${relPath}${itemStats.isDirectory() ? '/' : ''}`);
319
+ if (itemStats.isDirectory()) {
320
+ walk(fullPath, relPath);
321
+ }
322
+ }
323
+ }
324
+ walk(resolvedPath);
325
+ return {
326
+ content: [
327
+ { type: 'text', text: `Directory: ${dirPath} (${entries.length} entries)` },
328
+ { type: 'text', text: entries.join('\n') || '(empty)' },
329
+ ],
330
+ details: {
331
+ path: dirPath,
332
+ count: entries.length,
333
+ recursive: true,
334
+ },
335
+ };
336
+ }
337
+ else {
338
+ const items = readdirSync(resolvedPath);
339
+ // Sort: directories first, then files
340
+ const sorted = items
341
+ .filter((item) => !item.startsWith('.'))
342
+ .sort((a, b) => {
343
+ const aPath = join(resolvedPath, a);
344
+ const bPath = join(resolvedPath, b);
345
+ const aIsDir = statSync(aPath).isDirectory();
346
+ const bIsDir = statSync(bPath).isDirectory();
347
+ if (aIsDir && !bIsDir)
348
+ return -1;
349
+ if (!aIsDir && bIsDir)
350
+ return 1;
351
+ return a.localeCompare(b);
352
+ });
353
+ const formatted = sorted
354
+ .map((item) => {
355
+ const fullPath = join(resolvedPath, item);
356
+ const isDir = statSync(fullPath).isDirectory();
357
+ return `${isDir ? '📁' : '📄'} ${item}${isDir ? '/' : ''}`;
358
+ })
359
+ .join('\n');
360
+ return {
361
+ content: [
362
+ { type: 'text', text: `Directory: ${dirPath} (${sorted.length} items)` },
363
+ { type: 'text', text: formatted || '(empty)' },
364
+ ],
365
+ details: {
366
+ path: dirPath,
367
+ count: sorted.length,
368
+ recursive: false,
369
+ },
370
+ };
371
+ }
372
+ }
373
+ catch (error) {
374
+ return {
375
+ content: [{ type: 'text', text: `Error listing directory: ${error}` }],
376
+ details: { error: String(error) },
377
+ };
378
+ }
379
+ },
380
+ });
381
+ /**
382
+ * Tool: search_files
383
+ * Search for patterns in files
384
+ */
385
+ pi.registerTool({
386
+ name: 'search_files',
387
+ description: 'Search for text patterns in files using glob patterns. Returns list of matching file paths.',
388
+ // @ts-ignore TSchema mismatch
389
+ parameters: {
390
+ type: 'object',
391
+ properties: {
392
+ pattern: {
393
+ type: 'string',
394
+ description: 'The text pattern to search for (supports regex)',
395
+ },
396
+ path: {
397
+ type: 'string',
398
+ description: 'The directory path to search in',
399
+ },
400
+ glob: {
401
+ type: 'string',
402
+ description: 'Glob pattern for file matching (default: "**/*")',
403
+ },
404
+ },
405
+ required: ['pattern', 'path'],
406
+ },
407
+ async execute(args) {
408
+ try {
409
+ const searchPattern = String(args.pattern);
410
+ const searchPath = String(args.path);
411
+ const globPattern = args.glob ? String(args.glob) : '**/*';
412
+ // Validate path
413
+ const pathCheck = validatePath(searchPath);
414
+ if (!pathCheck.valid) {
415
+ return {
416
+ content: [{ type: 'text', text: `Path validation failed: ${pathCheck.error}` }],
417
+ details: { error: pathCheck.error },
418
+ };
419
+ }
420
+ const resolvedPath = pathCheck.resolvedPath;
421
+ // Check if directory exists
422
+ if (!existsSync(resolvedPath)) {
423
+ return {
424
+ content: [{ type: 'text', text: `Directory not found: ${searchPath}` }],
425
+ details: { error: 'directory_not_found' },
426
+ };
427
+ }
428
+ // Find files matching glob pattern
429
+ const files = await glob(globPattern, {
430
+ cwd: resolvedPath,
431
+ absolute: true,
432
+ nodir: true,
433
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
434
+ });
435
+ // Search for pattern in files
436
+ const regex = new RegExp(searchPattern, 'i');
437
+ const matches = [];
438
+ for (const file of files.slice(0, 100)) {
439
+ // Limit to 100 files for performance
440
+ try {
441
+ const stats = statSync(file);
442
+ if (stats.size > MAX_FILE_SIZE)
443
+ continue;
444
+ const content = await readFile(file, 'utf-8');
445
+ const lines = content.split('\n');
446
+ for (let i = 0; i < lines.length; i++) {
447
+ if (regex.test(lines[i])) {
448
+ matches.push({
449
+ file: file.replace(resolvedPath + '/', ''),
450
+ line: i + 1,
451
+ content: lines[i].trim().substring(0, 100),
452
+ });
453
+ }
454
+ }
455
+ }
456
+ catch {
457
+ // Skip files that can't be read
458
+ continue;
459
+ }
460
+ }
461
+ // Group matches by file
462
+ const grouped = matches.reduce((acc, match) => {
463
+ if (!acc[match.file])
464
+ acc[match.file] = [];
465
+ acc[match.file].push(match);
466
+ return acc;
467
+ }, {});
468
+ const formatted = Object.entries(grouped)
469
+ .map(([file, fileMatches]) => {
470
+ const lines = fileMatches.map((m) => ` Line ${m.line}: ${m.content}`).join('\n');
471
+ return `${file}\n${lines}`;
472
+ })
473
+ .join('\n\n');
474
+ return {
475
+ content: [
476
+ {
477
+ type: 'text',
478
+ text: `Found ${matches.length} matches in ${Object.keys(grouped).length} files`,
479
+ },
480
+ { type: 'text', text: formatted || 'No matches found' },
481
+ ],
482
+ details: {
483
+ pattern: searchPattern,
484
+ filesScanned: files.length,
485
+ matchesFound: matches.length,
486
+ uniqueFiles: Object.keys(grouped).length,
487
+ },
488
+ };
489
+ }
490
+ catch (error) {
491
+ return {
492
+ content: [{ type: 'text', text: `Error searching files: ${error}` }],
493
+ details: { error: String(error) },
494
+ };
495
+ }
496
+ },
497
+ });
498
+ /**
499
+ * Tool: batch_edit
500
+ * Edit multiple files matching a pattern
501
+ */
502
+ pi.registerTool({
503
+ name: 'batch_edit',
504
+ description: 'Find and replace text across multiple files matching a glob pattern. Returns list of edited files.',
505
+ // @ts-ignore TSchema mismatch
506
+ parameters: {
507
+ type: 'object',
508
+ properties: {
509
+ glob: {
510
+ type: 'string',
511
+ description: 'Glob pattern to match files (e.g., "**/*.ts")',
512
+ },
513
+ search: {
514
+ type: 'string',
515
+ description: 'The text to search for',
516
+ },
517
+ replace: {
518
+ type: 'string',
519
+ description: 'The replacement text',
520
+ },
521
+ regex: {
522
+ type: 'boolean',
523
+ description: 'If true, treat search as regex pattern (default: false)',
524
+ },
525
+ },
526
+ required: ['glob', 'search', 'replace'],
527
+ },
528
+ async execute(args) {
529
+ try {
530
+ const globPattern = String(args.glob);
531
+ const searchPattern = String(args.search);
532
+ const replacement = String(args.replace);
533
+ const useRegex = args.regex === true;
534
+ // Validate glob pattern (prevent directory traversal in glob)
535
+ if (globPattern.includes('..')) {
536
+ return {
537
+ content: [{ type: 'text', text: 'Invalid glob pattern: contains traversal sequence' }],
538
+ details: { error: 'invalid_glob' },
539
+ };
540
+ }
541
+ // Find matching files from the working directory
542
+ const files = await glob(globPattern, {
543
+ cwd: getWorkingDir(),
544
+ absolute: true,
545
+ nodir: true,
546
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
547
+ });
548
+ if (files.length === 0) {
549
+ return {
550
+ content: [{ type: 'text', text: 'No files matched the glob pattern' }],
551
+ details: { filesMatched: 0 },
552
+ };
553
+ }
554
+ const searchRegex = useRegex
555
+ ? new RegExp(searchPattern, 'g')
556
+ : new RegExp(escapeRegExp(searchPattern), 'g');
557
+ const edited = [];
558
+ const errors = [];
559
+ for (const file of files) {
560
+ try {
561
+ // Check file size
562
+ const stats = statSync(file);
563
+ if (stats.size > MAX_FILE_SIZE) {
564
+ errors.push(`${file}: File too large`);
565
+ continue;
566
+ }
567
+ const content = await readFile(file, 'utf-8');
568
+ if (searchRegex.test(content)) {
569
+ // Reset regex lastIndex for replace
570
+ searchRegex.lastIndex = 0;
571
+ const newContent = content.replace(searchRegex, replacement);
572
+ await writeFileAsync(file, newContent, 'utf-8');
573
+ edited.push(file);
574
+ }
575
+ }
576
+ catch (err) {
577
+ errors.push(`${file}: ${err}`);
578
+ }
579
+ }
580
+ const resultText = [
581
+ `Edited ${edited.length} of ${files.length} files`,
582
+ '',
583
+ ...edited.map((f) => ` ✓ ${f}`),
584
+ errors.length > 0 ? '' : '',
585
+ ...(errors.length > 0 ? [`Errors (${errors.length}):`, ...errors.map((e) => ` ✗ ${e}`)] : []),
586
+ ].join('\n');
587
+ return {
588
+ content: [{ type: 'text', text: resultText }],
589
+ details: {
590
+ filesMatched: files.length,
591
+ filesEdited: edited.length,
592
+ errors: errors.length,
593
+ edited,
594
+ errorList: errors,
595
+ },
596
+ };
597
+ }
598
+ catch (error) {
599
+ return {
600
+ content: [{ type: 'text', text: `Error in batch edit: ${error}` }],
601
+ details: { error: String(error) },
602
+ };
603
+ }
604
+ },
605
+ });
606
+ /**
607
+ * Tool: shell
608
+ * Execute shell command with safety checks
609
+ */
610
+ pi.registerTool({
611
+ name: 'shell',
612
+ description: 'Execute a shell command using Bun. Returns command output. Has safety checks for dangerous commands.',
613
+ // @ts-ignore TSchema mismatch
614
+ parameters: {
615
+ type: 'object',
616
+ properties: {
617
+ command: {
618
+ type: 'string',
619
+ description: 'The shell command to execute',
620
+ },
621
+ cwd: {
622
+ type: 'string',
623
+ description: 'Working directory for the command (default: current directory)',
624
+ },
625
+ timeout: {
626
+ type: 'number',
627
+ description: 'Timeout in milliseconds (default: 30000)',
628
+ },
629
+ },
630
+ required: ['command'],
631
+ },
632
+ async execute(args) {
633
+ try {
634
+ const command = String(args.command);
635
+ const cwd = args.cwd ? String(args.cwd) : getWorkingDir();
636
+ const timeout = typeof args.timeout === 'number' ? args.timeout : SHELL_TIMEOUT_DEFAULT;
637
+ // Validate cwd
638
+ const cwdCheck = validatePath(cwd);
639
+ if (!cwdCheck.valid) {
640
+ return {
641
+ content: [{ type: 'text', text: `CWD validation failed: ${cwdCheck.error}` }],
642
+ details: { error: cwdCheck.error },
643
+ };
644
+ }
645
+ // Security check
646
+ const safety = isCommandSafe(command);
647
+ if (!safety.safe) {
648
+ return {
649
+ content: [{ type: 'text', text: `Command blocked: ${safety.reason}` }],
650
+ details: { error: 'command_blocked', reason: safety.reason },
651
+ };
652
+ }
653
+ // Execute command using Bun shell with working directory
654
+ const result = await $ `sh -c ${command}`.cwd(cwd).text();
655
+ return {
656
+ content: [
657
+ { type: 'text', text: result || '(Command executed successfully with no output)' },
658
+ ],
659
+ details: {
660
+ command,
661
+ cwd,
662
+ timeout,
663
+ },
664
+ };
665
+ }
666
+ catch (error) {
667
+ return {
668
+ content: [{ type: 'text', text: `Command failed: ${error}` }],
669
+ details: { error: String(error), command: args.command },
670
+ };
671
+ }
672
+ },
673
+ });
674
+ // Register status bar item
675
+ // @ts-ignore ExtensionAPI property (not present in all runtimes)
676
+ if (typeof pi.registerStatusBarItem === 'function') {
677
+ // @ts-ignore ExtensionAPI property
678
+ pi.registerStatusBarItem('fileops', {
679
+ render() {
680
+ return '📁 FileOps Ready';
681
+ },
682
+ });
683
+ }
684
+ // Log initialization
685
+ console.log('[FileOps] Extension loaded with 6 tools:');
686
+ console.log(' - read_file_with_line_numbers');
687
+ console.log(' - write_file');
688
+ console.log(' - list_directory');
689
+ console.log(' - search_files');
690
+ console.log(' - batch_edit');
691
+ console.log(' - shell');
692
+ }
693
+ /**
694
+ * Escape special regex characters
695
+ */
696
+ function escapeRegExp(string) {
697
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
698
+ }
699
+ //# sourceMappingURL=fileops-extension.js.map