@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.
- package/LICENSE.md +19 -0
- package/README.md +304 -0
- package/bin/sosumi.mjs +76 -0
- package/package.json +53 -0
- package/public/_headers +2 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +7 -0
- package/public/icons/square.and.pencil.svg +15 -0
- package/public/index.html +898 -0
- package/public/llms.txt +184 -0
- package/public/sosumi.m4a +0 -0
- package/src/cli.ts +214 -0
- package/src/index.ts +507 -0
- package/src/lib/cli-endpoints.ts +106 -0
- package/src/lib/external/fetch.ts +133 -0
- package/src/lib/external/index.ts +8 -0
- package/src/lib/external/policy.ts +308 -0
- package/src/lib/external/types.ts +10 -0
- package/src/lib/fetch.ts +43 -0
- package/src/lib/hig/fetch.ts +186 -0
- package/src/lib/hig/index.ts +9 -0
- package/src/lib/hig/render.ts +514 -0
- package/src/lib/hig/types.ts +206 -0
- package/src/lib/hig/util.ts +30 -0
- package/src/lib/mcp.ts +315 -0
- package/src/lib/reference/fetch.ts +53 -0
- package/src/lib/reference/index.ts +8 -0
- package/src/lib/reference/render.ts +739 -0
- package/src/lib/reference/types.ts +31 -0
- package/src/lib/search.ts +221 -0
- package/src/lib/types.ts +334 -0
- package/src/lib/url.ts +55 -0
- package/src/lib/video/index.ts +179 -0
- package/wrangler.jsonc +27 -0
package/public/llms.txt
ADDED
|
@@ -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
|
+
})
|