Skip to content

Commit 69f2fd6

Browse files
committed
feat(mothership): run_function + streamed file-write handlers registered
Companion to mothership 0c5ec22b. run_function routes to the restored function-execute handler (write-capable sandbox); the streamed file-writing pair returns — workspaceFileServerTool survived the revamp unregistered, editContentServerTool recovered from the pre-revamp tree — and the intact preview machinery keys off their frames again. files grep joins the agent augmentations (v2 read-text based, degraded-file aware). Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent bca8a0f commit 69f2fd6

7 files changed

Lines changed: 526 additions & 1 deletion

File tree

apps/sim/lib/mothership/tool-executor/register-handlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '@/lib/mothership/generated/tool-catalog-v1'
88
import { createServerToolHandler } from '@/lib/mothership/tools/registry/server-tool-adapter'
99
import { getRegisteredServerToolNames } from '@/lib/mothership/tools/server/router'
10+
import { executeFunctionExecute } from '../tools/handlers/function-execute'
1011
import { executeRunCode } from '../tools/handlers/run-code'
1112
import { executeSimCli } from '../tools/handlers/sim-cli'
1213
import {
@@ -54,6 +55,9 @@ function buildHandlerMap(): Record<string, ToolHandler> {
5455
// (E2B/VM, mounts, secret materialization) lives on this side, same as the
5556
// workflow Function block. Compute-only: the handler rejects write vectors.
5657
run_code: h(executeRunCode),
58+
// The write-capable variant: same sandbox, plus outputs.files workspace
59+
// export and outputTable overwrite.
60+
run_function: h(executeFunctionExecute),
5761
// The worker's CLI surface, executed in-process via the CLI's own command
5862
// tree (sim/embed) against this deployment's internal API base.
5963
sim_cli: h(executeSimCli),

apps/sim/lib/mothership/tools/handlers/agent-cli/agent-cli.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,44 @@ describe('workflow views', () => {
120120
})
121121
})
122122

123+
describe('files grep', () => {
124+
const FILES_LIST = {
125+
data: [
126+
{ id: 'f1', name: 'report.md', folderPath: 'docs' },
127+
{ id: 'f2', name: 'logo.png', folderPath: '' },
128+
],
129+
nextCursor: null,
130+
}
131+
const readText = (text: string, degraded = false) => ({
132+
data: { text, degraded },
133+
})
134+
135+
it('greps file contents with line numbers, skipping non-text files', async () => {
136+
const match = matchAgentCliCommand(['files', 'grep', 'quarterly'])
137+
const result = await executeAgentCliCommand(
138+
match!,
139+
runtimeWith({
140+
'/api/v2/files': FILES_LIST,
141+
'/api/v2/files/f1/text': readText('# Report\nQuarterly revenue was up.\n'),
142+
'/api/v2/files/f2/text': readText('', true),
143+
})
144+
)
145+
expect(result.exitCode).toBe(0)
146+
expect(result.stdout).toContain('docs/report.md:2: Quarterly revenue was up.')
147+
expect(result.stdout).not.toContain('logo.png')
148+
})
149+
150+
it('filters by folder prefix', async () => {
151+
const match = matchAgentCliCommand(['files', 'grep', 'Quarterly', 'other'])
152+
const result = await executeAgentCliCommand(
153+
match!,
154+
runtimeWith({ '/api/v2/files': FILES_LIST })
155+
)
156+
expect(result.exitCode).toBe(0)
157+
expect(result.stdout).toContain('No matches')
158+
})
159+
})
160+
123161
describe('workflow grep', () => {
124162
it('reports matches as path: value lines', async () => {
125163
const match = matchAgentCliCommand(['workflow', 'grep', 'wf-1', 'Summarize'])
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { ListFilesResponse, ReadFileTextResponse } from 'sim/embed'
2+
import {
3+
type AgentCliCommand,
4+
type AgentCliRuntime,
5+
agentCliFail,
6+
agentCliOk,
7+
} from '@/lib/mothership/tools/handlers/agent-cli/types'
8+
9+
/**
10+
* Content grep across workspace files (the Go copilot's VFS-wide grep, files
11+
* half). Text extraction rides the v2 read-text endpoint, which already
12+
* handles binary/degraded files honestly, so this stays a pure projection.
13+
*/
14+
15+
const MAX_MATCHES = 200
16+
const MAX_FILES = 300
17+
const MAX_BYTES_PER_FILE = 262_144
18+
const READ_CONCURRENCY = 5
19+
const CONTEXT_CHARS = 120
20+
21+
function compilePattern(raw: string): (value: string) => boolean {
22+
try {
23+
const regex = new RegExp(raw, 'i')
24+
return (value) => regex.test(value)
25+
} catch {
26+
const needle = raw.toLowerCase()
27+
return (value) => value.toLowerCase().includes(needle)
28+
}
29+
}
30+
31+
function matchingLines(
32+
text: string,
33+
matches: (value: string) => boolean,
34+
label: string,
35+
out: string[]
36+
): void {
37+
const lines = text.split('\n')
38+
for (let lineNo = 0; lineNo < lines.length && out.length < MAX_MATCHES; lineNo++) {
39+
const line = lines[lineNo]
40+
if (!matches(line)) continue
41+
const snippet = line.length > CONTEXT_CHARS ? `${line.slice(0, CONTEXT_CHARS)}…` : line
42+
out.push(`${label}:${lineNo + 1}: ${snippet.trim()}`)
43+
}
44+
}
45+
46+
async function listAllFiles(runtime: AgentCliRuntime): Promise<ListFilesResponse['data']> {
47+
const rows: ListFilesResponse['data'] = []
48+
let cursor: string | null = null
49+
do {
50+
const page: ListFilesResponse = await runtime.client.request<ListFilesResponse>(
51+
'/api/v2/files',
52+
{
53+
query: { workspaceId: runtime.workspaceId, ...(cursor ? { cursor } : {}) },
54+
}
55+
)
56+
rows.push(...page.data)
57+
cursor = page.nextCursor
58+
} while (cursor && rows.length < MAX_FILES)
59+
return rows.slice(0, MAX_FILES)
60+
}
61+
62+
export const filesGrepCommand: AgentCliCommand = {
63+
path: ['files', 'grep'],
64+
summary: 'Search the content of every workspace file for a pattern',
65+
usage: 'files grep <pattern> [folder-path-prefix]',
66+
async execute(rest, runtime) {
67+
const [pattern, folderPrefix] = [rest[0], rest[1]]
68+
if (!pattern) return agentCliFail('Usage: sim files grep <pattern> [folder-path-prefix]')
69+
const matches = compilePattern(pattern)
70+
const files = (await listAllFiles(runtime)).filter(
71+
(file) => !folderPrefix || file.folderPath.startsWith(folderPrefix)
72+
)
73+
const out: string[] = []
74+
let unreadable = 0
75+
for (let i = 0; i < files.length && out.length < MAX_MATCHES; i += READ_CONCURRENCY) {
76+
const batch = files.slice(i, i + READ_CONCURRENCY)
77+
const texts = await Promise.all(
78+
batch.map(async (file) => {
79+
try {
80+
const response = await runtime.client.request<ReadFileTextResponse>(
81+
`/api/v2/files/${encodeURIComponent(file.id)}/text`,
82+
{ query: { workspaceId: runtime.workspaceId, maxBytes: String(MAX_BYTES_PER_FILE) } }
83+
)
84+
return { file, text: response.data.degraded ? null : response.data.text }
85+
} catch {
86+
// Binary or unreadable files must not sink the whole search.
87+
return { file, text: null }
88+
}
89+
})
90+
)
91+
for (const { file, text } of texts) {
92+
const label = file.folderPath ? `${file.folderPath}/${file.name}` : file.name
93+
if (matches(file.name) && out.length < MAX_MATCHES) out.push(`${label}: name matches`)
94+
if (text === null) {
95+
unreadable++
96+
continue
97+
}
98+
matchingLines(text, matches, label, out)
99+
}
100+
}
101+
if (out.length === 0) {
102+
return agentCliOk(
103+
unreadable > 0 ? `No matches (${unreadable} non-text files skipped).` : 'No matches.'
104+
)
105+
}
106+
const capped = out.length >= MAX_MATCHES ? [...out, `[capped at ${MAX_MATCHES} matches]`] : out
107+
return agentCliOk(capped.join('\n'))
108+
},
109+
}

apps/sim/lib/mothership/tools/handlers/agent-cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { filesGrepCommand } from '@/lib/mothership/tools/handlers/agent-cli/commands/files-grep'
12
import {
23
workflowGrepCommand,
34
workflowsGrepCommand,
@@ -20,6 +21,7 @@ import {
2021
* automatically.
2122
*/
2223
const AGENT_CLI_COMMANDS: readonly AgentCliCommand[] = [
24+
filesGrepCommand,
2325
workflowBlocksCommand,
2426
workflowEdgesCommand,
2527
workflowGrepCommand,

0 commit comments

Comments
 (0)