@youdotcom-oss/api 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@youdotcom-oss/api",
3
+ "version": "0.0.1",
4
+ "description": "You.com API client with bundled CLI for agents supporting Agent Skills",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=18",
8
+ "bun": ">= 1.2.21"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/youdotcom-oss/dx-toolkit.git",
13
+ "directory": "packages/api"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/youdotcom-oss/dx-toolkit/issues"
17
+ },
18
+ "homepage": "https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api#readme",
19
+ "author": "You.com (https://you.com)",
20
+ "keywords": [
21
+ "you.com",
22
+ "api",
23
+ "cli",
24
+ "search",
25
+ "ai",
26
+ "agent",
27
+ "agent-skills",
28
+ "bash",
29
+ "web-search",
30
+ "crawling",
31
+ "content-extraction"
32
+ ],
33
+ "bin": {
34
+ "ydc": "./bin/cli.js"
35
+ },
36
+ "type": "module",
37
+ "main": "./src/main.ts",
38
+ "exports": {
39
+ ".": "./src/main.ts"
40
+ },
41
+ "files": [
42
+ "./src/**",
43
+ "!./src/**/tests/*",
44
+ "!./src/**/*.spec.ts",
45
+ "bin/cli.js"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "bun build ./src/cli.ts --outfile ./bin/cli.js --target=node",
52
+ "check": "bun run check:biome && bun run check:types && bun run check:package",
53
+ "check:biome": "biome check",
54
+ "check:package": "format-package --check",
55
+ "check:types": "tsc --noEmit",
56
+ "check:write": "biome check --write && bun run format:package",
57
+ "dev": "bun src/cli.ts",
58
+ "format:package": "format-package --write",
59
+ "prepublishOnly": "bun run build",
60
+ "test": "bun test",
61
+ "test:coverage": "bun test --coverage",
62
+ "test:coverage:watch": "bun test --coverage --watch",
63
+ "test:watch": "bun test --watch"
64
+ },
65
+ "dependencies": {
66
+ "zod": "^4.3.5"
67
+ }
68
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ydc - You.com API CLI
4
+ *
5
+ * Commands:
6
+ * search <query> - Search the web with You.com
7
+ * express <input> - Get AI answers with web context
8
+ * contents <url> [url...] - Extract content from URLs
9
+ *
10
+ * Options:
11
+ * --api-key <key> - You.com API key (overrides YDC_API_KEY)
12
+ * --client <name> - Client name for tracking (overrides YDC_CLIENT)
13
+ * --output <json|text> - Output format (default: json)
14
+ * --help, -h - Show help
15
+ */
16
+ import { parseArgs } from 'node:util'
17
+ import packageJson from '../package.json' with { type: 'json' }
18
+ import { contentsCommand } from './commands/contents.ts'
19
+ import { expressCommand } from './commands/express.ts'
20
+ import { searchCommand } from './commands/search.ts'
21
+ import { generateErrorReportLink } from './shared/generate-error-report-link.ts'
22
+ import { useGetUserAgent } from './shared/use-get-user-agents.ts'
23
+
24
+ // Extract command and args (allows flags anywhere)
25
+ const rawArgs = process.argv.slice(2)
26
+ const command = rawArgs.find((arg) => !arg.startsWith('-')) || ''
27
+ const commandIndex = rawArgs.indexOf(command)
28
+ const args = commandIndex >= 0 ? rawArgs.slice(commandIndex + 1) : []
29
+
30
+ // Check for help
31
+ if (rawArgs.includes('--help') || rawArgs.includes('-h') || !command) {
32
+ console.log(`ydc v${packageJson.version} - You.com API CLI
33
+
34
+ Usage: ydc <command> --json <json> [options]
35
+
36
+ Commands:
37
+ search Search the web with You.com
38
+ express Get AI answers with web context
39
+ contents Extract content from URLs
40
+
41
+ Global Options:
42
+ --json <json> JSON string with command parameters (required)
43
+ --api-key <key> You.com API key (overrides YDC_API_KEY)
44
+ --client <name> Client name for tracking and debugging
45
+ --schema Output JSON schema for what can be passed to --json
46
+ --help, -h Show this help
47
+
48
+ Environment Variables:
49
+ YDC_API_KEY You.com API key (required)
50
+
51
+ Output Format:
52
+ Success: API response on stdout (exit 0)
53
+ Error: { success: false, error: {...} } on stderr (exit 1)
54
+ Invalid args: Error message on stderr (exit 2)
55
+
56
+ Examples:
57
+ ydc search "AI developments" --client Openclaw
58
+ ydc search "AI" --client Openclaw | jq '.data.results.web[0].title'
59
+ ydc express "What happened today?" --client MyAgent --tools web_search
60
+ ydc contents https://example.com --formats markdown
61
+ ydc search --schema # Get JSON schema for search --json input
62
+
63
+ More info: https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api
64
+ `)
65
+ process.exit(command ? 0 : 2)
66
+ }
67
+
68
+ try {
69
+ switch (command) {
70
+ case 'search':
71
+ await searchCommand(args)
72
+ break
73
+ case 'express':
74
+ await expressCommand(args)
75
+ break
76
+ case 'contents':
77
+ await contentsCommand(args)
78
+ break
79
+ default:
80
+ console.error(`Unknown command: ${command}`)
81
+ console.error(`Run 'ydc --help' for usage`)
82
+ process.exit(2)
83
+ }
84
+ process.exit(0)
85
+ } catch (error) {
86
+ console.error(error)
87
+ const message = error instanceof Error ? error.message : String(error)
88
+ const { values } = parseArgs({
89
+ args,
90
+ options: {
91
+ client: { type: 'string' },
92
+ },
93
+ })
94
+ const getUserAgent = useGetUserAgent(values.client || process.env.YDC_CLIENT)
95
+ console.error('\nTo report this error, share this mailto link with the user:')
96
+ console.error(generateErrorReportLink({ errorMessage: message, tool: command, clientInfo: getUserAgent() }))
97
+ process.exit(1)
98
+ }
@@ -0,0 +1,52 @@
1
+ import { parseArgs } from 'node:util'
2
+ import * as z from 'zod'
3
+ import { ContentsQuerySchema } from '../contents/contents.schemas.ts'
4
+ import { fetchContents } from '../contents/contents.utils.ts'
5
+ import { useGetUserAgent } from '../shared/use-get-user-agents.ts'
6
+
7
+ export const contentsCommand = async (args: string[]) => {
8
+ // Handle --schema flag
9
+ if (args.includes('--schema')) {
10
+ console.log(JSON.stringify(z.toJSONSchema(ContentsQuerySchema)))
11
+ process.exit(0)
12
+ }
13
+
14
+ // Parse flags with Node's built-in parseArgs
15
+ const { values } = parseArgs({
16
+ args,
17
+ options: {
18
+ json: { type: 'string' },
19
+ 'api-key': { type: 'string' },
20
+ client: { type: 'string' },
21
+ },
22
+ })
23
+
24
+ // --json is required
25
+ if (!values.json) {
26
+ throw new Error('--json flag is required')
27
+ }
28
+
29
+ // Parse JSON and validate with schema
30
+ const query = JSON.parse(values.json)
31
+ const apiKey = values['api-key']
32
+ const client = values.client || process.env.YDC_CLIENT
33
+
34
+ // Get API key from options or environment
35
+ const YDC_API_KEY = apiKey || process.env.YDC_API_KEY
36
+ if (!YDC_API_KEY) {
37
+ throw new Error('YDC_API_KEY environment variable is required')
38
+ }
39
+
40
+ // Validate with schema (includes urls validation)
41
+ const contentsQuery = ContentsQuerySchema.parse(query)
42
+
43
+ // Fetch contents
44
+ const response = await fetchContents({
45
+ contentsQuery,
46
+ YDC_API_KEY,
47
+ getUserAgent: useGetUserAgent(client),
48
+ })
49
+
50
+ // Output response to stdout (success)
51
+ console.log(JSON.stringify(response))
52
+ }
@@ -0,0 +1,52 @@
1
+ import { parseArgs } from 'node:util'
2
+ import * as z from 'zod'
3
+ import { ExpressAgentInputSchema } from '../express/express.schemas.ts'
4
+ import { callExpressAgent } from '../express/express.utils.ts'
5
+ import { useGetUserAgent } from '../shared/use-get-user-agents.ts'
6
+
7
+ export const expressCommand = async (args: string[]) => {
8
+ // Handle --schema flag
9
+ if (args.includes('--schema')) {
10
+ console.log(JSON.stringify(z.toJSONSchema(ExpressAgentInputSchema)))
11
+ process.exit(0)
12
+ }
13
+
14
+ // Parse flags with Node's built-in parseArgs
15
+ const { values } = parseArgs({
16
+ args,
17
+ options: {
18
+ json: { type: 'string' },
19
+ 'api-key': { type: 'string' },
20
+ client: { type: 'string' },
21
+ },
22
+ })
23
+
24
+ // --json is required
25
+ if (!values.json) {
26
+ throw new Error('--json flag is required')
27
+ }
28
+
29
+ // Parse JSON and validate with schema
30
+ const input = JSON.parse(values.json)
31
+ const apiKey = values['api-key']
32
+ const client = values.client || process.env.YDC_CLIENT
33
+
34
+ // Get API key from options or environment
35
+ const YDC_API_KEY = apiKey || process.env.YDC_API_KEY
36
+ if (!YDC_API_KEY) {
37
+ throw new Error('YDC_API_KEY environment variable is required')
38
+ }
39
+
40
+ // Validate with schema (includes input validation)
41
+ const agentInput = ExpressAgentInputSchema.parse(input)
42
+
43
+ // Call agent
44
+ const response = await callExpressAgent({
45
+ agentInput,
46
+ YDC_API_KEY,
47
+ getUserAgent: useGetUserAgent(client),
48
+ })
49
+
50
+ // Output response to stdout (success)
51
+ console.log(JSON.stringify(response))
52
+ }
@@ -0,0 +1,52 @@
1
+ import { parseArgs } from 'node:util'
2
+ import * as z from 'zod'
3
+ import { SearchQuerySchema } from '../search/search.schemas.ts'
4
+ import { fetchSearchResults } from '../search/search.utils.ts'
5
+ import { useGetUserAgent } from '../shared/use-get-user-agents.ts'
6
+
7
+ export const searchCommand = async (args: string[]) => {
8
+ // Handle --schema flag
9
+ if (args.includes('--schema')) {
10
+ console.log(JSON.stringify(z.toJSONSchema(SearchQuerySchema)))
11
+ process.exit(0)
12
+ }
13
+
14
+ // Parse flags with Node's built-in parseArgs
15
+ const { values } = parseArgs({
16
+ args,
17
+ options: {
18
+ json: { type: 'string' },
19
+ 'api-key': { type: 'string' },
20
+ client: { type: 'string' },
21
+ },
22
+ })
23
+
24
+ // --json is required
25
+ if (!values.json) {
26
+ throw new Error('--json flag is required')
27
+ }
28
+
29
+ // Parse JSON and validate with schema
30
+ const query = JSON.parse(values.json)
31
+ const apiKey = values['api-key']
32
+ const client = values.client || process.env.YDC_CLIENT
33
+
34
+ // Get API key from options or environment
35
+ const YDC_API_KEY = apiKey || process.env.YDC_API_KEY
36
+ if (!YDC_API_KEY) {
37
+ throw new Error('YDC_API_KEY environment variable is required')
38
+ }
39
+
40
+ // Validate with schema (includes query validation)
41
+ const searchQuery = SearchQuerySchema.parse(query)
42
+
43
+ // Fetch results
44
+ const response = await fetchSearchResults({
45
+ searchQuery,
46
+ YDC_API_KEY,
47
+ getUserAgent: useGetUserAgent(client),
48
+ })
49
+
50
+ // Output response to stdout (success)
51
+ console.log(JSON.stringify(response))
52
+ }
@@ -0,0 +1,46 @@
1
+ import * as z from 'zod'
2
+
3
+ /**
4
+ * Input schema for the you-contents tool
5
+ * Accepts an array of URLs, optional formats array (or legacy format string), and optional crawl timeout
6
+ */
7
+ export const ContentsQuerySchema = z.object({
8
+ urls: z
9
+ .array(z.string().url())
10
+ .min(1)
11
+ .describe('Array of webpage URLs to extract content from (e.g., ["https://example.com"])'),
12
+ formats: z
13
+ .array(z.enum(['markdown', 'html', 'metadata']))
14
+ .optional()
15
+ .describe('Output formats: array of "markdown" (text), "html" (layout), or "metadata" (structured data)'),
16
+ format: z.enum(['markdown', 'html']).optional().describe('(Deprecated) Output format - use formats array instead'),
17
+ crawl_timeout: z.number().min(1).max(60).optional().describe('Optional timeout in seconds (1-60) for page crawling'),
18
+ })
19
+
20
+ export type ContentsQuery = z.infer<typeof ContentsQuerySchema>
21
+
22
+ /**
23
+ * Schema for a single content item in the API response
24
+ */
25
+ const ContentsItemSchema = z.object({
26
+ url: z.string().describe('URL'),
27
+ title: z.string().optional().describe('Title'),
28
+ html: z.string().optional().describe('HTML content'),
29
+ markdown: z.string().optional().describe('Markdown content'),
30
+ metadata: z
31
+ .object({
32
+ jsonld: z.array(z.record(z.string(), z.unknown())).optional().describe('JSON-LD structured data (Schema.org)'),
33
+ opengraph: z.record(z.string(), z.string()).optional().describe('OpenGraph meta tags'),
34
+ twitter: z.record(z.string(), z.string()).optional().describe('Twitter Card metadata'),
35
+ })
36
+ .optional()
37
+ .describe('Structured metadata when available'),
38
+ })
39
+
40
+ /**
41
+ * API response schema from You.com Contents API
42
+ * Validates the full response array
43
+ */
44
+ export const ContentsApiResponseSchema = z.array(ContentsItemSchema)
45
+
46
+ export type ContentsApiResponse = z.infer<typeof ContentsApiResponseSchema>
@@ -0,0 +1,98 @@
1
+ import { CONTENTS_API_URL } from '../shared/api.constants.ts'
2
+ import type { GetUserAgent } from '../shared/api.types.ts'
3
+ import { checkResponseForErrors } from '../shared/check-response-for-errors.ts'
4
+ import { type ContentsApiResponse, ContentsApiResponseSchema, type ContentsQuery } from './contents.schemas.ts'
5
+
6
+ /**
7
+ * Fetch content from You.com Contents API
8
+ * The API accepts multiple URLs in a single request and returns all results
9
+ * @param contentsQuery - Query parameters including URLs and format
10
+ * @param YDC_API_KEY - You.com API key
11
+ * @param getUserAgent - Function to get User-Agent string
12
+ * @returns Parsed and validated API response
13
+ */
14
+ export const fetchContents = async ({
15
+ contentsQuery: { urls, formats, format, crawl_timeout },
16
+ YDC_API_KEY = process.env.YDC_API_KEY,
17
+ getUserAgent,
18
+ }: {
19
+ contentsQuery: ContentsQuery
20
+ YDC_API_KEY?: string
21
+ getUserAgent: GetUserAgent
22
+ }): Promise<ContentsApiResponse> => {
23
+ if (!YDC_API_KEY) {
24
+ throw new Error('YDC_API_KEY is required for Contents API')
25
+ }
26
+
27
+ // Handle backward compatibility: prefer formats array, fallback to format string, default to ['markdown']
28
+ const requestFormats = formats || (format ? [format] : ['markdown'])
29
+
30
+ // Build request body
31
+ const requestBody: {
32
+ urls: string[]
33
+ formats: string[]
34
+ crawl_timeout?: number
35
+ } = {
36
+ urls,
37
+ formats: requestFormats,
38
+ }
39
+
40
+ if (crawl_timeout !== undefined) {
41
+ requestBody.crawl_timeout = crawl_timeout
42
+ }
43
+
44
+ // Make single API call with all URLs
45
+ const options = {
46
+ method: 'POST',
47
+ headers: new Headers({
48
+ 'X-API-Key': YDC_API_KEY,
49
+ 'Content-Type': 'application/json',
50
+ 'User-Agent': getUserAgent(),
51
+ }),
52
+ body: JSON.stringify(requestBody),
53
+ }
54
+
55
+ const response = await fetch(CONTENTS_API_URL, options)
56
+
57
+ // Handle HTTP errors
58
+ if (!response.ok) {
59
+ const errorCode = response.status
60
+
61
+ // Try to parse error response body
62
+ let errorDetail = `Failed to fetch contents. HTTP ${errorCode}`
63
+ try {
64
+ const errorBody = await response.json()
65
+ if (errorBody && typeof errorBody === 'object' && 'detail' in errorBody) {
66
+ errorDetail = String(errorBody.detail)
67
+ }
68
+ } catch {
69
+ // If parsing fails, use default error message
70
+ }
71
+
72
+ // Handle specific error codes
73
+ if (errorCode === 401) {
74
+ throw new Error(`Authentication failed: ${errorDetail}. Please check your You.com API key.`)
75
+ }
76
+ if (errorCode === 403) {
77
+ throw new Error(`Forbidden: ${errorDetail}. Your API key may not have access to the Contents API.`)
78
+ }
79
+ if (errorCode === 429) {
80
+ throw new Error('Rate limited by You.com API. Please try again later.')
81
+ }
82
+ if (errorCode >= 500) {
83
+ throw new Error(`You.com API server error: ${errorDetail}`)
84
+ }
85
+
86
+ throw new Error(errorDetail)
87
+ }
88
+
89
+ const results = await response.json()
90
+
91
+ // Check for error field in 200 responses
92
+ checkResponseForErrors(results)
93
+
94
+ // Validate schema
95
+ const parsedResults = ContentsApiResponseSchema.parse(results)
96
+
97
+ return parsedResults
98
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { fetchContents } from '../contents.utils.ts'
3
+
4
+ const getUserAgent = () => 'API/test (You.com;TEST)'
5
+
6
+ // NOTE: The following tests require a You.com API key with access to the Contents API
7
+ // Using example.com/example.org as test URLs since You.com blocks self-scraping
8
+ describe('fetchContents', () => {
9
+ test(
10
+ 'returns valid response structure for single URL',
11
+ async () => {
12
+ const result = await fetchContents({
13
+ contentsQuery: {
14
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
15
+ format: 'markdown',
16
+ },
17
+ getUserAgent,
18
+ })
19
+
20
+ expect(Array.isArray(result)).toBe(true)
21
+ expect(result.length).toBeGreaterThan(0)
22
+
23
+ const firstItem = result[0]
24
+ expect(firstItem).toBeDefined()
25
+
26
+ // Should have markdown content
27
+ expect(firstItem?.markdown).toBeDefined()
28
+ expect(typeof firstItem?.markdown).toBe('string')
29
+ },
30
+ { retry: 2 },
31
+ )
32
+
33
+ test(
34
+ 'handles multiple URLs',
35
+ async () => {
36
+ const result = await fetchContents({
37
+ contentsQuery: {
38
+ urls: [
39
+ 'https://documentation.you.com/developer-resources/mcp-server',
40
+ 'https://documentation.you.com/developer-resources/python-sdk',
41
+ ],
42
+ format: 'markdown',
43
+ },
44
+ getUserAgent,
45
+ })
46
+
47
+ expect(Array.isArray(result)).toBe(true)
48
+ expect(result.length).toBe(2)
49
+
50
+ for (const item of result) {
51
+ expect(item).toHaveProperty('url')
52
+ expect(item.markdown).toBeDefined()
53
+ }
54
+ },
55
+ { retry: 2 },
56
+ )
57
+
58
+ test(
59
+ 'handles html format',
60
+ async () => {
61
+ const result = await fetchContents({
62
+ contentsQuery: {
63
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
64
+ format: 'html',
65
+ },
66
+ getUserAgent,
67
+ })
68
+
69
+ expect(Array.isArray(result)).toBe(true)
70
+ const firstItem = result[0]
71
+ expect(firstItem).toBeDefined()
72
+
73
+ expect(firstItem?.html).toBeDefined()
74
+ expect(typeof firstItem?.html).toBe('string')
75
+ },
76
+ { retry: 2 },
77
+ )
78
+ })
@@ -0,0 +1,77 @@
1
+ import * as z from 'zod'
2
+
3
+ export const ExpressAgentInputSchema = z.object({
4
+ input: z.string().min(1, 'Input is required').describe('Query or prompt'),
5
+ tools: z
6
+ .array(
7
+ z.object({
8
+ type: z.enum(['web_search']).describe('Tool type'),
9
+ }),
10
+ )
11
+ .optional()
12
+ .describe('Tools (web search only)'),
13
+ })
14
+
15
+ export type ExpressAgentInput = z.infer<typeof ExpressAgentInputSchema>
16
+
17
+ // API Response Schema - Validates the full response from You.com API
18
+
19
+ // Search result content item from web_search.results
20
+ // Note: thumbnail_url, source_type, and provider are API-only pass-through fields not used in MCP output
21
+ const ApiSearchResultItemSchema = z.object({
22
+ source_type: z.string().optional(),
23
+ citation_uri: z.string().optional(), // Used as fallback for url in transformation
24
+ url: z.string(),
25
+ title: z.string(),
26
+ snippet: z.string(),
27
+ thumbnail_url: z.string().optional(), // API-only, not transformed to MCP output
28
+ provider: z.any().optional(), // API-only, not transformed to MCP output
29
+ })
30
+
31
+ // Union of possible output item types from API
32
+ const ExpressAgentApiOutputItemSchema = z.union([
33
+ // web_search.results type - has content array, no text
34
+ z.object({
35
+ type: z.literal('web_search.results'),
36
+ content: z.array(ApiSearchResultItemSchema),
37
+ }),
38
+ // message.answer type - has text, no content
39
+ z.object({
40
+ type: z.literal('message.answer'),
41
+ text: z.string(),
42
+ }),
43
+ ])
44
+
45
+ export const ExpressAgentApiResponseSchema = z
46
+ .object({
47
+ output: z.array(ExpressAgentApiOutputItemSchema),
48
+ agent: z.string().optional().describe('Agent identifier'),
49
+ mode: z.string().optional().describe('Agent mode'),
50
+ input: z.array(z.any()).optional().describe('Input messages'),
51
+ })
52
+ .passthrough()
53
+
54
+ export type ExpressAgentApiResponse = z.infer<typeof ExpressAgentApiResponseSchema>
55
+
56
+ // MCP Output Schema - Defines what we return to the MCP client (answer + optional search results, token efficient)
57
+
58
+ // Search result item for MCP output
59
+ const McpSearchResultItemSchema = z.object({
60
+ url: z.string().describe('URL'),
61
+ title: z.string().describe('Title'),
62
+ snippet: z.string().describe('Snippet'),
63
+ })
64
+
65
+ // MCP response structure: answer (always) + results (optional when web_search used)
66
+ const ExpressAgentMcpResponseSchema = z.object({
67
+ answer: z.string().describe('AI answer'),
68
+ results: z
69
+ .object({
70
+ web: z.array(McpSearchResultItemSchema).describe('Web results'),
71
+ })
72
+ .optional()
73
+ .describe('Search results'),
74
+ agent: z.string().optional().describe('Agent ID'),
75
+ })
76
+
77
+ export type ExpressAgentMcpResponse = z.infer<typeof ExpressAgentMcpResponseSchema>