| 138 | |
| 139 | /** Builds the SSH-backed `read`/`write`/`edit`/`bash` tools scoped to `repoPath`. */ |
| 140 | export function buildSshToolSpecs(session: PiSshSession, repoPath: string): PiToolSpec[] { |
| 141 | const { client, sftp } = session |
| 142 | |
| 143 | return [ |
| 144 | { |
| 145 | name: 'read', |
| 146 | description: 'Read the full contents of a file in the repository.', |
| 147 | parameters: { |
| 148 | type: 'object', |
| 149 | properties: { path: { type: 'string', description: 'File path within the repository' } }, |
| 150 | required: ['path'], |
| 151 | }, |
| 152 | execute: (args) => |
| 153 | guard(async () => { |
| 154 | const path = asString(args.path) |
| 155 | if (!path) return { text: 'path is required', isError: true } |
| 156 | const content = await readRemoteFile(sftp, resolveRepoPath(repoPath, path)) |
| 157 | return { text: content, isError: false } |
| 158 | }), |
| 159 | }, |
| 160 | { |
| 161 | name: 'write', |
| 162 | description: 'Write (create or overwrite) a file in the repository.', |
| 163 | parameters: { |
| 164 | type: 'object', |
| 165 | properties: { |
| 166 | path: { type: 'string', description: 'File path within the repository' }, |
| 167 | content: { type: 'string', description: 'Full file contents to write' }, |
| 168 | }, |
| 169 | required: ['path', 'content'], |
| 170 | }, |
| 171 | execute: (args) => |
| 172 | guard(async () => { |
| 173 | const path = asString(args.path) |
| 174 | if (!path) return { text: 'path is required', isError: true } |
| 175 | const resolved = resolveRepoPath(repoPath, path) |
| 176 | await writeRemoteFile(sftp, resolved, asString(args.content)) |
| 177 | return { text: `Wrote ${resolved}`, isError: false } |
| 178 | }), |
| 179 | }, |
| 180 | { |
| 181 | name: 'edit', |
| 182 | description: 'Replace the first occurrence of old_string with new_string in a file.', |
| 183 | parameters: { |
| 184 | type: 'object', |
| 185 | properties: { |
| 186 | path: { type: 'string', description: 'File path within the repository' }, |
| 187 | old_string: { type: 'string', description: 'Exact text to replace' }, |
| 188 | new_string: { type: 'string', description: 'Replacement text' }, |
| 189 | }, |
| 190 | required: ['path', 'old_string', 'new_string'], |
| 191 | }, |
| 192 | execute: (args) => |
| 193 | guard(async () => { |
| 194 | const path = asString(args.path) |
| 195 | if (!path) return { text: 'path is required', isError: true } |
| 196 | const oldString = asString(args.old_string) |
| 197 | const resolved = resolveRepoPath(repoPath, path) |