@nshipster/sosumi 1.0.0

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.
@@ -0,0 +1,184 @@
1
+ # sosumi.ai - Apple Docs for LLMs
2
+
3
+ Ever notice Claude struggling to write Swift code?
4
+ It might not be their fault!
5
+
6
+ Apple Developer docs are locked behind JavaScript,
7
+ making them invisible to most LLMs.
8
+ If they try to fetch it, all they see is:
9
+ > This page requires JavaScript.
10
+ > Please turn on JavaScript in your browser and refresh the page to view its content.
11
+
12
+ This service translates Apple Developer documentation pages into AI-friendly Markdown.
13
+
14
+ ## HTTP Usage
15
+
16
+ Replace `developer.apple.com` with `sosumi.ai`:
17
+
18
+ **Original:**
19
+ https://developer.apple.com/documentation/swift/array
20
+
21
+ **AI-readable:**
22
+ https://sosumi.ai/documentation/swift/array
23
+
24
+ ### Examples
25
+
26
+ **Developer Documentation:**
27
+ - Swift: https://sosumi.ai/documentation/swift
28
+ - SwiftUI: https://sosumi.ai/documentation/swiftui
29
+ - Human Interface Guidelines: https://sosumi.ai/design/human-interface-guidelines
30
+ - WWDC sessions: https://sosumi.ai/videos/play/wwdc2021/10133
31
+
32
+ ## MCP Usage
33
+
34
+ Connect your MCP client to `https://sosumi.ai/mcp`:
35
+
36
+ ### GitHub Copilot for Xcode
37
+
38
+ 1. Open **GitHub Copilot for Xcode** and go to **Settings…**
39
+ 2. Select the **MCP** tab
40
+ 3. Click **Edit Config**
41
+ 4. Enter the following configuration:
42
+
43
+ ```json
44
+ {
45
+ "servers": {
46
+ "sosumi": {
47
+ "type": "http",
48
+ "url": "https://sosumi.ai/mcp"
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### Cursor
55
+
56
+ Open this deep link to automatically install the sosumi MCP server:
57
+
58
+ [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=sosumi&config=eyJ0eXBlIjoiaHR0cCIsInVybCI6Imh0dHBzOi8vc29zdW1pLmFpL21jcCJ9)
59
+
60
+ ### VSCode
61
+
62
+ Create a `.vscode/mcp.json` file in your workspace and enter the following configuration:
63
+
64
+ ```json
65
+ {
66
+ "servers": {
67
+ "sosumi": {
68
+ "type": "http",
69
+ "url": "https://sosumi.ai/mcp"
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### Claude Desktop
76
+
77
+ 1. Open **Claude Desktop**
78
+ 2. Go to **Settings → Connectors**
79
+ 3. Click **"Add custom connector"**
80
+ 4. Set **"Name"** to `"sosumi"`
81
+ 5. Set **"Remote MCP server URL"** to `"https://sosumi.ai/mcp"`
82
+ 6. Click "Add"
83
+
84
+ ### Claude Code
85
+
86
+ Run the following command in your terminal:
87
+
88
+ ```shell
89
+ claude mcp add --transport http sosumi https://sosumi.ai/mcp
90
+ ```
91
+
92
+ ### Other
93
+
94
+ Sosumi's MCP server supports Streamable HTTP and Server-Sent Events (SSE) transport.
95
+ **If your client supports either of these,
96
+ configure it to connect directly to `https://sosumi.ai/mcp`.**
97
+
98
+ Otherwise,
99
+ you can run this command to proxy over stdio:
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "sosumi": {
105
+ "command": "npx",
106
+ "args": ["-y", "mcp-remote", "https://sosumi.ai/mcp"]
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### Available Tools
113
+
114
+ - `searchAppleDocumentation` - Searches Apple Developer documentation
115
+ - Parameters: `query` (string)
116
+ - Returns structured results with titles,
117
+ URLs,
118
+ descriptions,
119
+ breadcrumbs,
120
+ and tags
121
+
122
+ - `fetchAppleDocumentation` - Fetches Apple Developer documentation and Human Interface Guidelines by path
123
+ - Parameters: `path` (string) - Documentation path (e.g., '/documentation/swift', 'design/human-interface-guidelines/foundations/color')
124
+ - Returns content as Markdown
125
+
126
+ - `fetchAppleVideoTranscript` - Fetches video transcripts, including WWDC sessions
127
+ - Parameters: `path` (string) - video path (e.g., `/videos/play/wwdc2021/10133`)
128
+ - Returns content as Markdown
129
+
130
+ - `fetchExternalDocumentation` - Fetches external Swift-DocC documentation by absolute HTTPS URL
131
+ - Parameters: `url` (string) - External URL (e.g., `https://apple.github.io/swift-argument-parser/documentation/argumentparser`)
132
+ - Returns content as Markdown
133
+
134
+ ## Troubleshooting
135
+
136
+ If you're experiencing connection timeouts or network issues with the MCP server,
137
+ you may need to configure a proxy.
138
+ This is particularly common in corporate environments
139
+ or regions with restricted internet access.
140
+
141
+ Configure your MCP client to use a proxy by adding environment variables:
142
+
143
+ ```json
144
+ {
145
+ "mcpServers": {
146
+ "sosumi": {
147
+ "command": "npx",
148
+ "args": ["-y", "mcp-remote", "https://sosumi.ai/mcp"],
149
+ "env": {
150
+ "HTTP_PROXY": "http://proxy.example.com:8080",
151
+ "HTTPS_PROXY": "http://proxy.example.com:8080"
152
+ }
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ Replace `proxy.example.com:8080` with your actual proxy server details.
159
+ For authenticated proxies, use the format:
160
+ `http://username:password@proxy.example.com:8080`
161
+
162
+ ## About
163
+
164
+ This is an unofficial,
165
+ independent project and is not affiliated with or endorsed by Apple Inc.
166
+ "Apple", "Xcode", and related marks are trademarks of Apple Inc.
167
+
168
+ This service is an accessibility-first,
169
+ on‑demand renderer.
170
+ It converts a single Apple Developer page to Markdown only when requested by a user.
171
+ It does not crawl, spider, or bulk download;
172
+ it does not attempt to bypass authentication or security;
173
+ and it implements rate limiting to avoid imposing unreasonable load.
174
+
175
+ Content is fetched transiently and may be cached briefly to improve performance.
176
+ No permanent archives are maintained.
177
+ All copyrights and other rights in the underlying content remain with Apple Inc.
178
+ Each page links back to the original source.
179
+
180
+ Your use of this service must comply with Apple's Terms of Use and applicable law.
181
+ You are solely responsible for how you access and use Apple's content through this tool.
182
+ Do not use this service to circumvent technical measures or for redistribution.
183
+
184
+ **Contact:** info@sosumi.ai
Binary file
package/src/cli.ts ADDED
@@ -0,0 +1,214 @@
1
+ import { parseCliArgs, resolveFetchEndpoint } from "./lib/cli-endpoints"
2
+ import { fetchExternalDocumentationMarkdown } from "./lib/external"
3
+ import { NotFoundError } from "./lib/fetch"
4
+ import {
5
+ extractHIGPaths,
6
+ fetchHIGPageData,
7
+ fetchHIGTableOfContents,
8
+ renderHIGFromJSON,
9
+ renderHIGTableOfContents,
10
+ } from "./lib/hig"
11
+ import { fetchJSONData, renderFromJSON } from "./lib/reference"
12
+ import { searchAppleDeveloperDocs } from "./lib/search"
13
+ import { generateAppleDocUrl, normalizeDocumentationPath } from "./lib/url"
14
+ import { fetchVideoTranscriptMarkdown } from "./lib/video"
15
+
16
+ function printUsage() {
17
+ console.error(`Usage:
18
+ sosumi fetch <url-or-path> [--json]
19
+ sosumi search <query> [--json]
20
+ sosumi serve [wrangler-dev-args...]
21
+
22
+ Examples:
23
+ npx @nshipster/sosumi fetch https://developer.apple.com/documentation/swift/array
24
+ npx @nshipster/sosumi fetch /videos/play/wwdc2021/10133
25
+ npx @nshipster/sosumi search "SwiftData"
26
+ npx @nshipster/sosumi serve --port 8787
27
+ `)
28
+ }
29
+
30
+ function printTextOutput(text: string) {
31
+ process.stdout.write(text)
32
+ }
33
+
34
+ function printJsonOutput(value: unknown) {
35
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
36
+ }
37
+
38
+ async function runFetch(input: string, json: boolean) {
39
+ const endpoint = resolveFetchEndpoint(input)
40
+
41
+ if (endpoint.startsWith("/documentation/")) {
42
+ const documentationPath = endpoint.replace(/^\/documentation\//, "")
43
+ const normalizedPath = normalizeDocumentationPath(documentationPath)
44
+ const appleUrl = generateAppleDocUrl(normalizedPath)
45
+ const jsonData = await fetchJSONData(normalizedPath)
46
+ const markdown = await renderFromJSON(jsonData, appleUrl)
47
+ if (json) {
48
+ printJsonOutput({ url: appleUrl, content: markdown })
49
+ } else {
50
+ printTextOutput(markdown)
51
+ }
52
+ return
53
+ }
54
+
55
+ if (endpoint === "/design/human-interface-guidelines") {
56
+ const markdown = await renderHIGTableOfContents(await fetchHIGTableOfContents())
57
+ const sourceUrl = "https://developer.apple.com/design/human-interface-guidelines/"
58
+ if (json) {
59
+ printJsonOutput({ url: sourceUrl, content: markdown })
60
+ } else {
61
+ printTextOutput(markdown)
62
+ }
63
+ return
64
+ }
65
+
66
+ if (endpoint.startsWith("/design/human-interface-guidelines/")) {
67
+ const higPath = endpoint.replace(/^\/design\/human-interface-guidelines\//, "")
68
+ const { sourceUrl, markdown } = await fetchHigMarkdown(higPath)
69
+ if (json) {
70
+ printJsonOutput({ url: sourceUrl, content: markdown })
71
+ } else {
72
+ printTextOutput(markdown)
73
+ }
74
+ return
75
+ }
76
+
77
+ if (endpoint.startsWith("/videos/play/")) {
78
+ const segments = endpoint.split("/")
79
+ const collection = segments[3] as string
80
+ const videoId = segments[4] as string
81
+ const sourceUrl = `https://developer.apple.com/videos/play/${collection}/${videoId}/`
82
+ const markdown = await fetchVideoTranscriptMarkdown(sourceUrl, collection, videoId)
83
+ if (json) {
84
+ printJsonOutput({ url: sourceUrl, content: markdown })
85
+ } else {
86
+ printTextOutput(markdown)
87
+ }
88
+ return
89
+ }
90
+
91
+ if (endpoint.startsWith("/external/")) {
92
+ const externalUrl = endpoint.replace(/^\/external\//, "")
93
+ const markdown = await fetchExternalDocumentationMarkdown(externalUrl, {
94
+ EXTERNAL_DOC_HOST_ALLOWLIST: process.env.EXTERNAL_DOC_HOST_ALLOWLIST,
95
+ EXTERNAL_DOC_HOST_BLOCKLIST: process.env.EXTERNAL_DOC_HOST_BLOCKLIST,
96
+ })
97
+ if (json) {
98
+ printJsonOutput({ url: externalUrl, content: markdown })
99
+ } else {
100
+ printTextOutput(markdown)
101
+ }
102
+ return
103
+ }
104
+
105
+ throw new Error(`Unsupported fetch endpoint: ${endpoint}`)
106
+ }
107
+
108
+ async function fetchHigMarkdown(higPath: string): Promise<{ sourceUrl: string; markdown: string }> {
109
+ const sourceUrlFor = (path: string) =>
110
+ `https://developer.apple.com/design/human-interface-guidelines/${path}`
111
+
112
+ const resolvedPath = await resolveHigPathForFetch(higPath)
113
+
114
+ try {
115
+ const sourceUrl = sourceUrlFor(resolvedPath)
116
+ const markdown = await renderHIGFromJSON(await fetchHIGPageData(resolvedPath), sourceUrl)
117
+ return { sourceUrl, markdown }
118
+ } catch (error) {
119
+ if (error instanceof NotFoundError && resolvedPath !== higPath) {
120
+ const sourceUrl = sourceUrlFor(higPath)
121
+ const markdown = await renderHIGFromJSON(await fetchHIGPageData(higPath), sourceUrl)
122
+ return { sourceUrl, markdown }
123
+ }
124
+ throw error
125
+ }
126
+ }
127
+
128
+ async function resolveHigPathForFetch(higPath: string): Promise<string> {
129
+ if (!higPath.includes("/")) {
130
+ return higPath
131
+ }
132
+
133
+ // HIG moved many topics from grouped paths (e.g. foundations/color -> color).
134
+ // Resolve legacy grouped paths by leaf slug from the live ToC when unique.
135
+ const leaf = higPath.split("/").filter(Boolean).pop()
136
+ if (!leaf) {
137
+ return higPath
138
+ }
139
+
140
+ const paths = extractHIGPaths(await fetchHIGTableOfContents())
141
+ const matches = paths.filter((path) => path === leaf || path.endsWith(`/${leaf}`))
142
+ return matches.length === 1 ? matches[0] : higPath
143
+ }
144
+
145
+ async function runSearch(query: string, json: boolean) {
146
+ const trimmedQuery = query.trim()
147
+ if (!trimmedQuery) {
148
+ console.error("Search query cannot be empty")
149
+ process.exitCode = 1
150
+ return
151
+ }
152
+
153
+ const searchResponse = await searchAppleDeveloperDocs(trimmedQuery)
154
+ if (json) {
155
+ printJsonOutput(searchResponse)
156
+ return
157
+ }
158
+
159
+ if (searchResponse.results.length === 0) {
160
+ console.error(`No results found for "${trimmedQuery}"`)
161
+ process.exitCode = 2
162
+ return
163
+ }
164
+
165
+ const resultText =
166
+ `Found ${searchResponse.results.length} result(s) for "${trimmedQuery}":\n\n` +
167
+ searchResponse.results
168
+ .map(
169
+ (result, index) =>
170
+ `${index + 1}. ${result.title}\n ${result.url}\n ${result.description || "No description"}`,
171
+ )
172
+ .join("\n\n")
173
+ printTextOutput(resultText)
174
+ }
175
+
176
+ export async function main(argv: string[] = process.argv.slice(2)) {
177
+ const { help, flags, positionals } = parseCliArgs(argv)
178
+ if (help) {
179
+ printUsage()
180
+ return
181
+ }
182
+
183
+ if (positionals.length < 2) {
184
+ printUsage()
185
+ process.exitCode = 1
186
+ return
187
+ }
188
+
189
+ const [command, ...rest] = positionals
190
+ const payload = rest.join(" ")
191
+
192
+ if (command === "fetch") {
193
+ await runFetch(payload, flags.json)
194
+ return
195
+ }
196
+
197
+ if (command === "search") {
198
+ await runSearch(payload, flags.json)
199
+ return
200
+ }
201
+
202
+ // "serve" is still handled by bin wrapper to keep wrangler process behavior.
203
+ if (command === "serve") {
204
+ return
205
+ }
206
+
207
+ throw new Error(`Unknown command: ${command}`)
208
+ }
209
+
210
+ main().catch((error) => {
211
+ const message = error instanceof Error ? error.message : String(error)
212
+ console.error(`sosumi: ${message}`)
213
+ process.exit(1)
214
+ })