Skip to content

Commit 858d7ba

Browse files
committed
Phase A1–A4 (sim half): inventory printer, universal grep, docs search, jq/outline pipes
packages/sim-cli/scripts/print-command-inventory.ts prints the public CLI inventory for the worker card — walks buildProgram(), the same tree --help and the docs read, and resolves each command response shape from the v2 OpenAPI documents. lib/mothership/agent-cli gains the universal-grep engine (every world, pretty-printed as its get returns it, block catalog LRU-cached per workspace, secrets names only), the docs-search engine (wrapping searchDocsServerTool), and typed jq (jq-wasm 1.8.2) and outline pipe stages; a stage that cannot apply fails the invocation with the reason. Still primitives only: no grammar, no flag interpretation. https://claude.ai/code/session_01HFVhPDtRZuCgupPco633w8
1 parent b857f97 commit 858d7ba

15 files changed

Lines changed: 843 additions & 40 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it, vi } from 'vitest'
5+
6+
const { execute } = vi.hoisted(() => ({
7+
execute: vi
8+
.fn()
9+
.mockResolvedValue({ results: [{ path: 'docs/integrations/slack.mdx' }], query: 'q' }),
10+
}))
11+
vi.mock('@/lib/mothership/tools/server/docs/search-docs', () => ({
12+
searchDocsServerTool: { execute },
13+
}))
14+
15+
import { runEngine } from '@/lib/mothership/agent-cli/engines'
16+
import type { AgentCliRuntime } from '@/lib/mothership/agent-cli/types'
17+
18+
const runtime: AgentCliRuntime = {
19+
workspaceId: 'ws-1',
20+
userId: 'user-1',
21+
client: { request: async () => ({}) as never },
22+
}
23+
24+
describe('docs search engine', () => {
25+
it('joins the query words and maps --top/--path onto the docs search tool', async () => {
26+
const result = await runEngine('docs search', ['slack', 'streaming'], runtime, {
27+
top: '3',
28+
path: 'docs/integrations',
29+
})
30+
expect(result.exitCode).toBe(0)
31+
expect(execute).toHaveBeenCalledWith(
32+
{ query: 'slack streaming', topK: 3, path: 'docs/integrations' },
33+
{ userId: 'user-1', workspaceId: 'ws-1' }
34+
)
35+
expect(JSON.parse(result.stdout).results[0].path).toBe('docs/integrations/slack.mdx')
36+
})
37+
38+
it('fails usefully without a query or with a bad --top', async () => {
39+
expect((await runEngine('docs search', [], runtime, {})).exitCode).toBe(1)
40+
expect((await runEngine('docs search', ['x'], runtime, { top: 'many' })).exitCode).toBe(1)
41+
})
42+
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
type AgentCliEngine,
3+
type AgentCliFlags,
4+
agentCliFail,
5+
agentCliOk,
6+
} from '@/lib/mothership/agent-cli/types'
7+
import { searchDocsServerTool } from '@/lib/mothership/tools/server/docs/search-docs'
8+
9+
const DEFAULT_TOP = 6
10+
const MAX_TOP = 20
11+
12+
function topFrom(flags: AgentCliFlags): number | string {
13+
const raw = flags.top
14+
if (raw === undefined || raw === true) return DEFAULT_TOP
15+
const n = Number.parseInt(raw, 10)
16+
if (!Number.isFinite(n) || n < 1) return '--top needs a positive number'
17+
return Math.min(n, MAX_TOP)
18+
}
19+
20+
/**
21+
* `docs search <query> [--top n] [--path prefix]` — the product docs, through the same
22+
* engine the `search_docs` tool used, as a noun in the CLI grammar (18-agent-surface.md
23+
* A3). One knowledge surface: the tips corpus merges into these pages over time.
24+
*/
25+
export const docsSearchCommand: AgentCliEngine = {
26+
async execute(positionals, runtime, flags) {
27+
const query = positionals.join(' ').trim()
28+
if (!query) return agentCliFail('Usage: sim docs search <query> [--top n] [--path prefix]')
29+
const top = topFrom(flags)
30+
if (typeof top === 'string') return agentCliFail(top)
31+
const path = typeof flags.path === 'string' ? flags.path : undefined
32+
const output = await searchDocsServerTool.execute(
33+
{ query, topK: top, ...(path ? { path } : {}) },
34+
{ userId: runtime.userId, workspaceId: runtime.workspaceId }
35+
)
36+
return agentCliOk(JSON.stringify(output, null, 2))
37+
},
38+
}

apps/sim/lib/mothership/agent-cli/engines/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { getErrorMessage } from '@sim/utils/errors'
22
import { workflowDepsCommand } from '@/lib/mothership/agent-cli/engines/deps'
3+
import { docsSearchCommand } from '@/lib/mothership/agent-cli/engines/docs-search'
34
import { filesGrepCommand } from '@/lib/mothership/agent-cli/engines/files-grep'
45
import { workflowGrepCommand, workflowsGrepCommand } from '@/lib/mothership/agent-cli/engines/grep'
56
import { workflowLintCommand } from '@/lib/mothership/agent-cli/engines/lint'
67
import { logsQueryCommand } from '@/lib/mothership/agent-cli/engines/query'
78
import { workflowTraceCommand } from '@/lib/mothership/agent-cli/engines/trace'
9+
import { universalGrepCommand } from '@/lib/mothership/agent-cli/engines/universal-grep'
810
import {
911
workflowBlocksCommand,
1012
workflowEdgesCommand,
@@ -23,7 +25,9 @@ import {
2325
* augmentation-drift check reads these keys.
2426
*/
2527
export const AUGMENTATION_ENGINES: Readonly<Record<string, AgentCliEngine>> = {
28+
'docs search': docsSearchCommand,
2629
'files grep': filesGrepCommand,
30+
grep: universalGrepCommand,
2731
'logs query': logsQueryCommand,
2832
'workflow blocks': workflowBlocksCommand,
2933
'workflow deps': workflowDepsCommand,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { runEngine } from '@/lib/mothership/agent-cli/engines'
6+
import type { AgentCliRuntime } from '@/lib/mothership/agent-cli/types'
7+
8+
const SLACK_V2 = {
9+
id: 'slack_v2',
10+
name: 'Slack',
11+
triggers: [{ id: 'slack_webhook', configFields: { streamOutputs: { type: 'boolean' } } }],
12+
operations: { send_message: { toolId: 'slack_send' } },
13+
}
14+
15+
function runtimeWith(responses: Record<string, unknown>): AgentCliRuntime {
16+
return {
17+
workspaceId: `ws-${Math.random().toString(36).slice(2)}`,
18+
userId: 'user-1',
19+
client: {
20+
request: async <T>(path: string): Promise<T> => {
21+
const hit = responses[path]
22+
if (hit === undefined) throw new Error(`Unexpected request: ${path}`)
23+
return hit as T
24+
},
25+
},
26+
}
27+
}
28+
29+
const CATALOG = {
30+
'/api/v2/blocks': { data: [{ id: 'slack_v2' }, { id: 'agent' }], nextCursor: null },
31+
'/api/v2/blocks/slack_v2': { data: SLACK_V2 },
32+
'/api/v2/blocks/agent': { data: { id: 'agent', name: 'Agent', inputSchema: [{ id: 'model' }] } },
33+
}
34+
35+
describe('universal grep', () => {
36+
it('finds field ids inside block definitions and names the path-shaped line', async () => {
37+
const result = await runEngine('grep', ['stream'], runtimeWith(CATALOG), {
38+
scope: 'blocks',
39+
i: true,
40+
})
41+
expect(result.exitCode).toBe(0)
42+
expect(result.stdout).toContain('blocks/slack_v2:')
43+
expect(result.stdout).toContain('"streamOutputs"')
44+
expect(result.stdout).not.toContain('blocks/agent:')
45+
})
46+
47+
it('narrows to one resource with --in and counts with --count', async () => {
48+
const within = await runEngine('grep', ['id'], runtimeWith(CATALOG), {
49+
scope: 'blocks',
50+
in: 'agent',
51+
})
52+
expect(within.stdout).toContain('blocks/agent:')
53+
expect(within.stdout).not.toContain('blocks/slack_v2:')
54+
const count = await runEngine('grep', ['id'], runtimeWith(CATALOG), {
55+
scope: 'blocks',
56+
count: true,
57+
})
58+
expect(count.stdout).toMatch(/^\d+ \(blocks=\d+\)$/)
59+
})
60+
61+
it('refuses an unknown scope with a did-you-mean and the scope list', async () => {
62+
const result = await runEngine('grep', ['x'], runtimeWith({}), { scope: 'block' })
63+
expect(result.exitCode).toBe(1)
64+
expect(result.stderr).toContain('Did you mean blocks')
65+
expect(result.stderr).toContain('workflows, blocks, tools')
66+
})
67+
68+
it('materializes secrets as names only', async () => {
69+
const result = await runEngine(
70+
'grep',
71+
['OPENAI'],
72+
runtimeWith({
73+
'/api/v2/secrets': {
74+
data: [{ name: 'OPENAI_API_KEY', value: 'sk-should-never-appear' }],
75+
nextCursor: null,
76+
},
77+
}),
78+
{ scope: 'secrets' }
79+
)
80+
expect(result.stdout).toContain('OPENAI_API_KEY')
81+
expect(result.stdout).not.toContain('sk-should-never-appear')
82+
})
83+
84+
it('reports no matches honestly', async () => {
85+
const result = await runEngine('grep', ['zzz-nope'], runtimeWith(CATALOG), { scope: 'blocks' })
86+
expect(result.exitCode).toBe(0)
87+
expect(result.stdout).toContain('No matches for "zzz-nope" in blocks')
88+
})
89+
})

0 commit comments

Comments
 (0)