Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@
"bin": {
"mux": "bin/mux"
},
"pi": {
"skills": [
"skills/mux-cli"
]
},
"files": [
"bin",
"skills",
"README.md",
"LICENSE"
],
Expand Down
92 changes: 92 additions & 0 deletions skills/mux-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
name: mux-cli
description: Use for all questions about Mux APIs, Mux SDKs, Mux CLI, webhooks, assets, uploads, playback, live streaming, Mux Data, Mux Robots, or Mux docs. The agent must use `mux docs search --json` and `mux docs read` before web search or answering from memory.
---

# Mux CLI

Use the `mux` command for Mux API operations and documentation lookup.

## Mandatory docs workflow

For any Mux API, SDK, CLI, webhook, playback, upload, asset, live streaming, Data, Robots, or docs question, use the local Mux docs commands before using web search or answering from memory.

Do not search the web first.

First run:

```bash
mux docs search "<topic>" --json
```

Then read the most relevant doc:

```bash
mux docs read "<doc-id>" --format markdown
```

Only use web search if:

1. `mux docs search` fails,
2. the docs index has no relevant result, or
3. the user explicitly asks for external or web results.

## Command resolution

If `mux` is available, use:

```bash
mux docs search "<topic>" --json
mux docs read "<doc-id>" --format markdown
```

If working inside the Mux CLI repo and `mux` is not installed, use:

```bash
pnpm dev docs search "<topic>" --json
pnpm dev docs read "<doc-id>" --format markdown
```

If the docs cache has not been populated, run:

```bash
mux docs update
```

For local pre-deploy testing with generated artifacts, run:

```bash
mux docs update --artifact-path ../mux.com/apps/web/public/.well-known/mux-docs
```

## Docs sources

The CLI resolves docs in this order:

1. cached published manifest/index populated by `mux docs update`
2. `--source <path>`
3. `MUX_DOCS_PATH`
4. a sibling `../mux.com` checkout for local Mux development

The published docs index is downloaded from `docs.mux.com` and contains only `.mdx` files from `apps/web/app/docs/_guides`.

## Example

User: "How do I verify Mux webhook signatures?"

Run:

```bash
mux docs search "verify webhook signatures" --json
mux docs read verify-webhook-signatures --format markdown
```

Then answer using the retrieved docs. Cite the docs page by `doc.id` and `doc.url` when possible.

## Agent usage guidelines

- Use `mux docs search --json` before answering Mux-specific questions.
- Use `mux docs read <doc-id>` before providing detailed API behavior or code examples.
- Cite docs by `doc.id` and `doc.url` when possible.
- Do not mutate Mux resources unless the user explicitly asks you to.
- For write/delete/update operations, explain the intended action and confirm if there is any ambiguity.
91 changes: 91 additions & 0 deletions src/commands/docs/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, test } from 'bun:test';
import { docsCommand } from './index.ts';
import { readCommand } from './read.ts';
import { searchCommand } from './search.ts';
import { sourceCommand } from './source.ts';
import { updateCommand } from './update.ts';

describe('mux docs command', () => {
test('has a docs-focused description', () => {
expect(docsCommand.getDescription()).toMatch(/Mux docs/i);
});

test('registers docs subcommands', () => {
const commandNames = docsCommand
.getCommands()
.map((command) => command.getName());

expect(commandNames).toContain('search');
expect(commandNames).toContain('read');
expect(commandNames).toContain('update');
expect(commandNames).toContain('source');
});
});

describe('mux docs search command', () => {
test('describes docs search', () => {
expect(searchCommand.getDescription()).toMatch(/search.*docs/i);
});

test('accepts a query argument', () => {
const args = searchCommand.getArguments();

expect(args.length).toBeGreaterThanOrEqual(1);
expect(args[0]?.name).toMatch(/query/i);
});

test('has agent-friendly output and source options', () => {
const optionNames = searchCommand.getOptions().map((option) => option.name);

expect(optionNames).toContain('json');
expect(optionNames).toContain('limit');
expect(optionNames).toContain('source');
});
});

describe('mux docs read command', () => {
test('describes reading a docs page', () => {
expect(readCommand.getDescription()).toMatch(/read.*docs/i);
});

test('accepts a doc id argument', () => {
const args = readCommand.getArguments();

expect(args.length).toBe(1);
expect(args[0]?.name).toMatch(/doc.*id|id/i);
});

test('has markdown and JSON output options', () => {
const optionNames = readCommand.getOptions().map((option) => option.name);

expect(optionNames).toContain('format');
expect(optionNames).toContain('json');
expect(optionNames).toContain('source');
});
});

describe('mux docs update command', () => {
test('describes refreshing the docs cache', () => {
expect(updateCommand.getDescription()).toMatch(/update|refresh/i);
});

test('has source, force, and JSON options', () => {
const optionNames = updateCommand.getOptions().map((option) => option.name);

expect(optionNames).toContain('source');
expect(optionNames).toContain('force');
expect(optionNames).toContain('json');
});
});

describe('mux docs source command', () => {
test('describes showing the active docs source', () => {
expect(sourceCommand.getDescription()).toMatch(/source/i);
});

test('has a JSON option', () => {
const optionNames = sourceCommand.getOptions().map((option) => option.name);

expect(optionNames).toContain('json');
});
});
15 changes: 15 additions & 0 deletions src/commands/docs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Command } from '@cliffy/command';
import { readCommand } from './read.ts';
import { searchCommand } from './search.ts';
import { sourceCommand } from './source.ts';
import { updateCommand } from './update.ts';

export const docsCommand = new Command()
.description('Search, read, and refresh Mux docs')
.action(function () {
this.showHelp();
})
.command('search', searchCommand)
.command('read', readCommand)
.command('update', updateCommand)
.command('source', sourceCommand);
76 changes: 76 additions & 0 deletions src/commands/docs/read.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Command } from '@cliffy/command';
import {
findDocById,
formatDocContent,
loadDocsIndex,
readDocContent,
} from '@/lib/docs.ts';

interface ReadOptions {
format?: 'markdown' | 'raw';
json?: boolean;
source?: string;
}

export const readCommand = new Command()
.description('Read a Mux docs page')
.arguments('<doc-id:string>')
.option('--format <format:string>', 'Output format: markdown or raw', {
default: 'markdown',
value: (value: string): 'markdown' | 'raw' => {
if (value !== 'markdown' && value !== 'raw') {
throw new Error('Invalid format. Must be "markdown" or "raw".');
}
return value;
},
})
.option('--json', 'Output JSON instead of pretty format')
.option('--source <path:string>', 'Path to a local mux.com docs repository')
.action(async (options: ReadOptions, docId: string) => {
try {
const { index, source } = await loadDocsIndex({
explicitPath: options.source,
});
const doc = findDocById(index, docId);

if (!doc) {
throw new Error(`Docs page not found: ${docId}`);
}

const content = formatDocContent(
await readDocContent(doc),
options.format,
);

if (options.json) {
console.log(
JSON.stringify(
{
source,
doc,
content,
},
null,
2,
),
);
return;
}

console.log(content);
Comment thread
cursor[bot] marked this conversation as resolved.
} catch (error) {
printDocsError(error, options);
}
});

function printDocsError(error: unknown, options: ReadOptions): never {
const message = error instanceof Error ? error.message : String(error);

if (options.json) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(`Error: ${message}`);
}

process.exit(1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identical error handler duplicated across command files

Low Severity

printDocsError is defined identically in read.ts and search.ts, and the same error-handling pattern is also inlined in source.ts and update.ts. All four implementations format and print errors the same way based on a json option. This duplicated logic risks inconsistent future fixes and could be a single shared utility.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bc0d437. Configure here.

83 changes: 83 additions & 0 deletions src/commands/docs/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Command } from '@cliffy/command';
import { loadDocsIndex, searchDocsIndex } from '@/lib/docs.ts';

interface SearchOptions {
json?: boolean;
limit?: number;
source?: string;
}

export const searchCommand = new Command()
.description('Search Mux docs')
.arguments('<query:string>')
.option('--json', 'Output JSON instead of pretty format')
.option('--limit <number:number>', 'Number of results to return', {
default: 10,
})
.option('--source <path:string>', 'Path to a local mux.com docs repository')
.action(async (options: SearchOptions, query: string) => {
try {
const { index, source } = await loadDocsIndex({
explicitPath: options.source,
});
const results = searchDocsIndex(index, query, { limit: options.limit });

if (options.json) {
console.log(
JSON.stringify(
{
query,
source,
results: results.map((result) => ({
score: result.score,
snippet: result.snippet,
doc: {
id: result.entry.id,
title: result.entry.title,
description: result.entry.description,
product: result.entry.product,
route: result.entry.route,
url: result.entry.url,
relativePath: result.entry.relativePath,
headings: result.entry.headings,
},
})),
},
null,
2,
),
);
return;
}

if (results.length === 0) {
console.log('No docs found.');
return;
}

for (const result of results) {
console.log(`${result.entry.title} (${result.entry.id})`);
console.log(` ${result.entry.url}`);
if (result.entry.description) {
console.log(` ${result.entry.description}`);
} else {
console.log(` ${result.snippet}`);
}
console.log('');
}
} catch (error) {
printDocsError(error, options);
}
});

function printDocsError(error: unknown, options: SearchOptions): never {
const message = error instanceof Error ? error.message : String(error);

if (options.json) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(`Error: ${message}`);
}

process.exit(1);
}
Loading
Loading