Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ It does not contain local per-tool REST logic or local schema validation.

## Install

**Bun**
```bash
bun add -g @youdotcom-oss/cli
```
**Node**
```bash
npm add -g @youdotcom-oss/cli
```

Or run it without installing:

**Bun**
```bash
bunx @youdotcom-oss/cli tools
```
**Node**
```bash
npx @youdotcom-oss/cli tools
```

## Commands

Expand Down Expand Up @@ -52,6 +62,8 @@ echo '{"query":"latest bun release"}' | ydc you-search
Prints the resolved URL, tool id, sanitized headers, and JSON arguments.
- `--profile free`
Supported only for `you-search`. In this mode the CLI routes to `?profile=free` and strips auth headers.
- `-h, --help`
Prints usage, available commands, tools, and flags, then exits.

## Environment

Expand Down
41 changes: 36 additions & 5 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,35 @@ const BASE_MCP_SERVER_URL = 'https://api.you.com/mcp'
type McpToolResult = Awaited<ReturnType<Client['callTool']>>
const args = process.argv.slice(2)
const command = args[0]
const usage = `Usage: ydc tools
ydc schema <tool> [input|output]
ydc <tool> <json> [flags]`

const buildHelp = () =>
[
'Usage: ydc tools',
' ydc schema <tool> [input|output]',
" ydc <tool> '<json>' [flags]",
" echo '<json>' | ydc <tool>",
'',
'Agent-first CLI bridge for the hosted You.com MCP server.',
'',
'Commands:',
' tools List the locally allowlisted tool ids',
' schema <tool> [input|output] Fetch the raw remote schema for a tool',
" <tool> '<json>' Execute a remote tool with JSON input",
'',
'Tools:',
` ${TOOL_CONTRACT.tools.map(({ name }) => name).join(', ')}`,
'',
'Flags:',
' --api-key <key> Use this API key instead of YDC_API_KEY',
' --dry-run Print resolved URL, tool id, sanitized headers, and JSON arguments',
' --profile free Route to ?profile=free and strip auth (you-search only)',
' -h, --help Show this help message',
'',
'Environment:',
' YDC_API_KEY Optional default API key',
].join('\n')

const isHelpRequest = command === '--help' || command === '-h'

if (command === 'tools') {
console.log(
Expand Down Expand Up @@ -156,8 +182,13 @@ if (command && tool) {
}
}

if (!command || command === '--help') {
console.log(usage)
if (!command) {
console.error(buildHelp())
process.exit(1)
}

if (isHelpRequest) {
console.log(buildHelp())
process.exit(0)
}

Expand Down
75 changes: 75 additions & 0 deletions packages/cli/src/tests/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,81 @@ describe('ydc help and command validation', () => {
expect(stdout).toContain('Usage: ydc tools')
})

test('prints the same help for the -h short flag', async () => {
const longChild = Bun.spawn({
cmd: ['bun', './src/cli.ts', '--help'],
cwd: `${import.meta.dir}/../..`,
stderr: 'pipe',
stdout: 'pipe',
})
const shortChild = Bun.spawn({
cmd: ['bun', './src/cli.ts', '-h'],
cwd: `${import.meta.dir}/../..`,
stderr: 'pipe',
stdout: 'pipe',
})

const [longOut, , longExit] = await Promise.all([
new Response(longChild.stdout).text(),
new Response(longChild.stderr).text(),
longChild.exited,
])
const [shortOut, shortErr, shortExit] = await Promise.all([
new Response(shortChild.stdout).text(),
new Response(shortChild.stderr).text(),
shortChild.exited,
])

expect(longExit).toBe(0)
expect(shortExit).toBe(0)
expect(shortErr).toBe('')
expect(shortOut).toBe(longOut)
})

test('lists every allowlisted tool and flag in the help output', async () => {
const child = Bun.spawn({
cmd: ['bun', './src/cli.ts', '--help'],
cwd: `${import.meta.dir}/../..`,
stderr: 'pipe',
stdout: 'pipe',
})

const [stdout, stderr, exitCode] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
])

expect(exitCode).toBe(0)
expect(stderr).toBe('')
for (const { name } of TOOL_CONTRACT.tools) {
expect(stdout).toContain(name)
}
expect(stdout).toContain('--api-key')
expect(stdout).toContain('--dry-run')
expect(stdout).toContain('--profile')
expect(stdout).toContain('-h, --help')
})

test('prints help to stderr and exits non-zero when no command is given', async () => {
const child = Bun.spawn({
cmd: ['bun', './src/cli.ts'],
cwd: `${import.meta.dir}/../..`,
stderr: 'pipe',
stdout: 'pipe',
})

const [stdout, stderr, exitCode] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
])

expect(exitCode).toBe(1)
expect(stdout).toBe('')
expect(stderr).toContain('Usage: ydc tools')
})

test('rejects unknown commands immediately', async () => {
const child = Bun.spawn({
cmd: ['bun', './src/cli.ts', 'you-missing'],
Expand Down
Loading