@nshipster/sosumi 1.0.0 → 1.0.4

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/public/llms.txt CHANGED
@@ -9,29 +9,34 @@ If they try to fetch it, all they see is:
9
9
  > This page requires JavaScript.
10
10
  > Please turn on JavaScript in your browser and refresh the page to view its content.
11
11
 
12
- This service translates Apple Developer documentation pages into AI-friendly Markdown.
12
+ This service translates Apple Developer documentation,
13
+ Human Interface Guidelines, WWDC sessions,
14
+ and external Swift-DocC sites into AI-friendly Markdown.
15
+ Access it in your browser, over MCP, from the command line, as an AI skill,
16
+ or with an unofficial [Chrome extension](https://chromewebstore.google.com/detail/donffakeimppgoehccpfhlchmbfdmfpj).
13
17
 
14
18
  ## HTTP Usage
15
19
 
16
20
  Replace `developer.apple.com` with `sosumi.ai`:
17
21
 
18
22
  **Original:**
19
- https://developer.apple.com/documentation/swift/array
23
+ <https://developer.apple.com/documentation/swift/array>
20
24
 
21
25
  **AI-readable:**
22
- https://sosumi.ai/documentation/swift/array
26
+ <https://sosumi.ai/documentation/swift/array>
23
27
 
24
28
  ### Examples
25
29
 
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
30
+ - Swift: <https://sosumi.ai/documentation/swift>
31
+ - SwiftUI: <https://sosumi.ai/documentation/swiftui>
32
+ - Human Interface Guidelines: <https://sosumi.ai/design/human-interface-guidelines>
33
+ - WWDC21 Protect mutable state with Swift actors: <https://sosumi.ai/videos/play/wwdc2021/10133>
34
+ - Swift Argument Parser (GitHub Pages): <https://sosumi.ai/external/https://apple.github.io/swift-argument-parser/documentation/argumentparser/>
35
+ - The Composable Architecture (Swift Package Index): <https://sosumi.ai/external/https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/1.23.1/documentation/composablearchitecture>
31
36
 
32
37
  ## MCP Usage
33
38
 
34
- Connect your MCP client to `https://sosumi.ai/mcp`:
39
+ Connect your MCP client to `https://sosumi.ai/mcp`.
35
40
 
36
41
  ### GitHub Copilot for Xcode
37
42
 
@@ -89,7 +94,7 @@ Run the following command in your terminal:
89
94
  claude mcp add --transport http sosumi https://sosumi.ai/mcp
90
95
  ```
91
96
 
92
- ### Other
97
+ ### Other MCP Clients
93
98
 
94
99
  Sosumi's MCP server supports Streamable HTTP and Server-Sent Events (SSE) transport.
95
100
  **If your client supports either of these,
@@ -113,25 +118,21 @@ you can run this command to proxy over stdio:
113
118
 
114
119
  - `searchAppleDocumentation` - Searches Apple Developer documentation
115
120
  - Parameters: `query` (string)
116
- - Returns structured results with titles,
117
- URLs,
118
- descriptions,
119
- breadcrumbs,
120
- and tags
121
+ - Returns structured results with titles, URLs, descriptions, breadcrumbs, and tags
121
122
 
122
123
  - `fetchAppleDocumentation` - Fetches Apple Developer documentation and Human Interface Guidelines by path
123
124
  - Parameters: `path` (string) - Documentation path (e.g., '/documentation/swift', 'design/human-interface-guidelines/foundations/color')
124
125
  - Returns content as Markdown
125
126
 
126
- - `fetchAppleVideoTranscript` - Fetches video transcripts, including WWDC sessions
127
- - Parameters: `path` (string) - video path (e.g., `/videos/play/wwdc2021/10133`)
127
+ - `fetchAppleVideoTranscript` - Fetches video transcripts, including WWDC sessions, by video path
128
+ - Parameters: `path` (string) - video path (e.g., '/videos/play/wwdc2021/10133', '/videos/play/meet-with-apple/208')
128
129
  - Returns content as Markdown
129
130
 
130
131
  - `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
+ - Parameters: `url` (string) - External URL (e.g., '<https://apple.github.io/swift-argument-parser/documentation/argumentparser>')
132
133
  - Returns content as Markdown
133
134
 
134
- ## Troubleshooting
135
+ ### Troubleshooting
135
136
 
136
137
  If you're experiencing connection timeouts or network issues with the MCP server,
137
138
  you may need to configure a proxy.
@@ -159,14 +160,163 @@ Replace `proxy.example.com:8080` with your actual proxy server details.
159
160
  For authenticated proxies, use the format:
160
161
  `http://username:password@proxy.example.com:8080`
161
162
 
162
- ## About
163
+ ## CLI Usage
164
+
165
+ Sosumi also provides a CLI that complements MCP.
166
+
167
+ ### Node.js
168
+
169
+ Run directly with [`npx`](https://docs.npmjs.com/cli/commands/npx)
170
+ (included with [Node.js](https://nodejs.org/)):
171
+
172
+ ```shell
173
+ npx @nshipster/sosumi fetch https://developer.apple.com/documentation/swift/array
174
+ ```
175
+
176
+ If you use it regularly, install it once:
177
+
178
+ ```shell
179
+ npm i -g @nshipster/sosumi
180
+ ```
181
+
182
+ ### Bun
183
+
184
+ Run directly with [`bunx`](https://bun.sh/docs/cli/bunx)
185
+ (included with [Bun](https://bun.sh/)):
186
+
187
+ ```shell
188
+ bunx @nshipster/sosumi fetch https://developer.apple.com/documentation/swift/array
189
+ ```
190
+
191
+ If you use it regularly, install it once:
192
+
193
+ ```shell
194
+ bun i -g @nshipster/sosumi
195
+ ```
196
+
197
+ ### Deno
198
+
199
+ Run directly with [`deno run`](https://docs.deno.com/runtime/reference/cli/run/):
200
+
201
+ ```shell
202
+ deno run -A npm:@nshipster/sosumi fetch https://developer.apple.com/documentation/swift/array
203
+ ```
204
+
205
+ If you use it regularly, install it once:
206
+
207
+ ```shell
208
+ deno install -g -A npm:@nshipster/sosumi
209
+ ```
210
+
211
+ ### Fetching Documentation
212
+
213
+ Fetch any content type supported by the MCP tools —
214
+ API docs, Human Interface Guidelines, WWDC sessions, and external Swift-DocC pages:
215
+
216
+ ```shell
217
+ sosumi fetch /documentation/swift/array
218
+ sosumi fetch /design/human-interface-guidelines/color
219
+ sosumi fetch /videos/play/wwdc2021/10133
220
+ sosumi fetch https://apple.github.io/swift-argument-parser/documentation/argumentparser
221
+ ```
222
+
223
+ ### Searching
224
+
225
+ Search Apple Developer documentation:
226
+
227
+ ```shell
228
+ sosumi search "SwiftData"
229
+ ```
230
+
231
+ ### JSON Output
163
232
 
164
- This is an unofficial,
165
- independent project and is not affiliated with or endorsed by Apple Inc.
233
+ By default, output is plain text / Markdown.
234
+ Add `--json` for scripts:
235
+
236
+ ```shell
237
+ sosumi fetch /documentation/swift/array --json
238
+ sosumi search "SwiftData" --json
239
+ ```
240
+
241
+ ### Local Server
242
+
243
+ Run a local instance of the server from the published package:
244
+
245
+ ```shell
246
+ sosumi serve
247
+ sosumi serve --port 8787
248
+ ```
249
+
250
+ ## AI Agent Skill
251
+
252
+ Teach your coding assistant to use Sosumi consistently with:
253
+ `https://sosumi.ai/SKILL.md`
254
+
255
+ Spec-compliant clients can also discover and install it from this domain:
256
+
257
+ ```bash
258
+ npx skills add https://sosumi.ai
259
+ ```
260
+
261
+ ### Claude Code
262
+
263
+ Install as a reusable skill (personal or project-level):
264
+
265
+ ```bash
266
+ # Personal skill (all your projects)
267
+ mkdir -p ~/.claude/skills/sosumi
268
+ curl -o ~/.claude/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
269
+
270
+ # Project skill (just this project)
271
+ mkdir -p .claude/skills/sosumi
272
+ curl -o .claude/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
273
+ ```
274
+
275
+ ### Codex
276
+
277
+ Install globally:
278
+
279
+ ```bash
280
+ # Global instructions (all your projects)
281
+ mkdir -p ~/.agents/skills/sosumi
282
+ curl -o ~/.agents/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
283
+ ```
284
+
285
+ ### Cursor
286
+
287
+ Install as a Cursor skill (global or project-level):
288
+
289
+ ```bash
290
+ # Personal skill (all your projects)
291
+ mkdir -p ~/.cursor/skills/sosumi
292
+ curl -o ~/.cursor/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
293
+
294
+ # Project skill (just this project)
295
+ mkdir -p .cursor/skills/sosumi
296
+ curl -o .cursor/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
297
+ ```
298
+
299
+ ### Other
300
+
301
+ For AGENTS-compatible tools, install as a reusable skill:
302
+
303
+ ```bash
304
+ mkdir -p ~/.agents/skills/sosumi
305
+ curl -o ~/.agents/skills/sosumi/SKILL.md https://sosumi.ai/SKILL.md
306
+ ```
307
+
308
+ Use this together with Sosumi MCP at `https://sosumi.ai/mcp`.
309
+
310
+ ## Legal
311
+
312
+ This is an unofficial, independent project
313
+ and is not affiliated with or endorsed by Apple Inc.
166
314
  "Apple", "Xcode", and related marks are trademarks of Apple Inc.
167
315
 
168
- This service is an accessibility-first,
169
- on‑demand renderer.
316
+ This project is open source and available on
317
+ [GitHub](https://github.com/NSHipster/sosumi.ai).
318
+
319
+ This service is an accessibility-first, on-demand renderer.
170
320
  It converts a single Apple Developer page to Markdown only when requested by a user.
171
321
  It does not crawl, spider, or bulk download;
172
322
  it does not attempt to bypass authentication or security;
@@ -181,4 +331,8 @@ Your use of this service must comply with Apple's Terms of Use and applicable la
181
331
  You are solely responsible for how you access and use Apple's content through this tool.
182
332
  Do not use this service to circumvent technical measures or for redistribution.
183
333
 
184
- **Contact:** info@sosumi.ai
334
+ Fetches are made with the user agent `sosumi-ai/1.0 (+https://sosumi.ai/#bot)`.
335
+ For external Swift-DocC hosts, sosumi honors `robots.txt` rules
336
+ and opt-out response directives such as `X-Robots-Tag: noai`.
337
+
338
+ **Contact:** <info@sosumi.ai>
@@ -0,0 +1,8 @@
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /documentation/
4
+ Disallow: /external/
5
+ Disallow: /design/human-interface-guidelines
6
+ Disallow: /videos/play/
7
+
8
+ Sitemap: https://sosumi.ai/sitemap.xml
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url>
4
+ <loc>https://sosumi.ai/</loc>
5
+ </url>
6
+ </urlset>
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { StreamableHTTPTransport } from "@hono/mcp"
2
2
  import { Hono } from "hono"
3
+ import { accepts } from "hono/accepts"
3
4
  import { cache } from "hono/cache"
4
5
  import { cors } from "hono/cors"
5
6
  import { HTTPException } from "hono/http-exception"
@@ -21,6 +22,14 @@ import {
21
22
  import { createMcpServer } from "./lib/mcp"
22
23
  import { fetchJSONData, renderFromJSON } from "./lib/reference"
23
24
  import { searchAppleDeveloperDocs } from "./lib/search"
25
+ import {
26
+ createSkillIndex,
27
+ loadSkill,
28
+ SKILL_NAME,
29
+ skillExists,
30
+ skillHeaders,
31
+ skillIndexHeaders,
32
+ } from "./lib/skill"
24
33
  import { generateAppleDocUrl, isValidAppleDocUrl, normalizeDocumentationPath } from "./lib/url"
25
34
  import { fetchVideoTranscriptMarkdown, TranscriptNotFoundError } from "./lib/video"
26
35
 
@@ -32,24 +41,6 @@ interface Env {
32
41
  }
33
42
 
34
43
  const app = new Hono<{ Bindings: Env }>()
35
- const mcpServerCache = new Map<string, ReturnType<typeof createMcpServer>>()
36
-
37
- function getMcpServer(env: Env) {
38
- const allowlist = env.EXTERNAL_DOC_HOST_ALLOWLIST ?? ""
39
- const blocklist = env.EXTERNAL_DOC_HOST_BLOCKLIST ?? ""
40
- const cacheKey = `${allowlist}\n---\n${blocklist}`
41
- const cached = mcpServerCache.get(cacheKey)
42
- if (cached) {
43
- return cached
44
- }
45
-
46
- const server = createMcpServer({
47
- EXTERNAL_DOC_HOST_ALLOWLIST: env.EXTERNAL_DOC_HOST_ALLOWLIST,
48
- EXTERNAL_DOC_HOST_BLOCKLIST: env.EXTERNAL_DOC_HOST_BLOCKLIST,
49
- })
50
- mcpServerCache.set(cacheKey, server)
51
- return server
52
- }
53
44
 
54
45
  app.use("*", async (c, next) => {
55
46
  await next()
@@ -84,15 +75,89 @@ app.use("*", async (c, next) => {
84
75
  await next()
85
76
  })
86
77
 
78
+ app.get("/", async (c) => {
79
+ const accepted = accepts(c, {
80
+ header: "Accept",
81
+ supports: ["text/markdown", "text/html"],
82
+ default: "text/html",
83
+ })
84
+
85
+ if (accepted === "text/markdown") {
86
+ const llmsUrl = new URL("/llms.txt", c.req.url)
87
+ const llmsResponse = await c.env.ASSETS.fetch(new Request(llmsUrl.toString()))
88
+
89
+ if (!llmsResponse.ok) {
90
+ throw new HTTPException(500, {
91
+ message: "Failed to load llms.txt",
92
+ })
93
+ }
94
+
95
+ const markdown = await llmsResponse.text()
96
+ return c.text(markdown, 200, {
97
+ "Content-Type": "text/markdown; charset=utf-8",
98
+ "Cache-Control": "public, max-age=300, s-maxage=600",
99
+ })
100
+ }
101
+
102
+ return c.env.ASSETS.fetch(c.req.raw)
103
+ })
104
+
105
+ app.all("/.well-known/agent-skills/index.json", async (c) => {
106
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") {
107
+ return c.text("Method Not Allowed", 405, { Allow: "GET, HEAD" })
108
+ }
109
+
110
+ if (c.req.method === "HEAD") {
111
+ if (!(await skillExists(c.env.ASSETS, c.req.url))) {
112
+ throw new HTTPException(500, { message: "Failed to load SKILL.md" })
113
+ }
114
+ return new Response(null, {
115
+ status: 200,
116
+ headers: skillIndexHeaders,
117
+ })
118
+ }
119
+
120
+ const skill = await loadSkill(c.env.ASSETS, c.req.url)
121
+ const index = await createSkillIndex(skill)
122
+
123
+ return c.json(index, 200, skillIndexHeaders)
124
+ })
125
+
126
+ app.all(`/.well-known/agent-skills/${SKILL_NAME}/SKILL.md`, async (c) => {
127
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") {
128
+ return c.text("Method Not Allowed", 405, { Allow: "GET, HEAD" })
129
+ }
130
+
131
+ if (c.req.method === "HEAD") {
132
+ if (!(await skillExists(c.env.ASSETS, c.req.url))) {
133
+ throw new HTTPException(500, { message: "Failed to load SKILL.md" })
134
+ }
135
+ return new Response(null, {
136
+ status: 200,
137
+ headers: skillHeaders,
138
+ })
139
+ }
140
+
141
+ const skill = await loadSkill(c.env.ASSETS, c.req.url)
142
+
143
+ return new Response(skill.bytes, {
144
+ status: 200,
145
+ headers: skillHeaders,
146
+ })
147
+ })
148
+
149
+ app.get("/bot", (c) => c.redirect("/#bot", 302))
150
+
87
151
  app.all("/mcp", async (c) => {
88
- const mcpServer = getMcpServer(c.env)
152
+ const mcpServer = createMcpServer({
153
+ EXTERNAL_DOC_HOST_ALLOWLIST: c.env.EXTERNAL_DOC_HOST_ALLOWLIST,
154
+ EXTERNAL_DOC_HOST_BLOCKLIST: c.env.EXTERNAL_DOC_HOST_BLOCKLIST,
155
+ })
89
156
  const transport = new StreamableHTTPTransport()
90
157
  await mcpServer.connect(transport)
91
158
  return transport.handleRequest(c)
92
159
  })
93
160
 
94
- app.get("/bot", (c) => c.redirect("/#bot", 302))
95
-
96
161
  app.get("/search", async (c) => {
97
162
  const query = c.req.query("q")?.trim() ?? ""
98
163
  if (!query) {
@@ -183,6 +248,7 @@ This service only works with Apple Developer documentation URLs:
183
248
  "Content-Location": appleUrl,
184
249
  "Cache-Control": "public, max-age=3600, s-maxage=86400",
185
250
  ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
251
+ "X-Robots-Tag": "noindex, nofollow, noarchive",
186
252
  }
187
253
 
188
254
  if (c.req.header("Accept")?.includes("application/json")) {
@@ -225,6 +291,7 @@ app.get("/external/*", async (c) => {
225
291
  "Content-Location": targetUrl.toString(),
226
292
  "Cache-Control": "public, max-age=3600, s-maxage=86400",
227
293
  ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
294
+ "X-Robots-Tag": "noindex, nofollow, noarchive",
228
295
  }
229
296
 
230
297
  if (c.req.header("Accept")?.includes("application/json")) {
@@ -263,6 +330,7 @@ app.get("/design/human-interface-guidelines", async (c) => {
263
330
  "Content-Location": sourceUrl,
264
331
  "Cache-Control": "public, max-age=3600, s-maxage=86400",
265
332
  ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
333
+ "X-Robots-Tag": "noindex, nofollow, noarchive",
266
334
  }
267
335
 
268
336
  if (c.req.header("Accept")?.includes("application/json")) {
@@ -305,6 +373,7 @@ app.get("/design/human-interface-guidelines/:path{.+}", async (c) => {
305
373
  "Content-Location": sourceUrl,
306
374
  "Cache-Control": "public, max-age=3600, s-maxage=86400",
307
375
  ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
376
+ "X-Robots-Tag": "noindex, nofollow, noarchive",
308
377
  }
309
378
 
310
379
  if (c.req.header("Accept")?.includes("application/json")) {
@@ -358,6 +427,7 @@ app.get("/videos/play/:collection/:id", async (c) => {
358
427
  "Content-Location": sourceUrl,
359
428
  "Cache-Control": "public, max-age=3600, s-maxage=86400",
360
429
  ETag: `"${Buffer.from(markdown).toString("base64").slice(0, 16)}"`,
430
+ "X-Robots-Tag": "noindex, nofollow, noarchive",
361
431
  }
362
432
 
363
433
  if (c.req.header("Accept")?.includes("application/json")) {
@@ -496,7 +496,10 @@ function renderHIGTocItems(items: HIGTocItem[], headingLevel: number): string {
496
496
 
497
497
  for (const item of items) {
498
498
  if (item.type === "module" || item.type === "symbol") {
499
- // Main sections get headings
499
+ // Ensure blank line before heading when preceding content was a list
500
+ if (markdown && !markdown.endsWith("\n\n")) {
501
+ markdown += "\n"
502
+ }
500
503
  const hashes = "#".repeat(Math.min(headingLevel, 6))
501
504
  markdown += `${hashes} ${item.title}\n\n`
502
505
 
@@ -6,6 +6,7 @@ import type {
6
6
  AppleDocJSON,
7
7
  ContentItem,
8
8
  IndexContentItem,
9
+ PossibleValueItem,
9
10
  PropertyItem,
10
11
  TopicSection,
11
12
  Variant,
@@ -94,6 +95,18 @@ export async function renderFromJSON(
94
95
  )
95
96
  }
96
97
 
98
+ // Add possible values (used by enum/string type pages)
99
+ const possibleValuesSection = jsonData.primaryContentSections.find(
100
+ (s) => s.kind === "possibleValues",
101
+ )
102
+ if (possibleValuesSection?.values) {
103
+ markdown += renderPossibleValues(
104
+ possibleValuesSection.values,
105
+ jsonData.references,
106
+ options.externalOrigin,
107
+ )
108
+ }
109
+
97
110
  // Add content sections
98
111
  const contentSections = jsonData.primaryContentSections.filter((s) => s.kind === "content")
99
112
  for (const section of contentSections) {
@@ -294,6 +307,34 @@ function renderProperties(
294
307
  return markdown
295
308
  }
296
309
 
310
+ /**
311
+ * Render possible values section for enum/string type pages.
312
+ */
313
+ function renderPossibleValues(
314
+ values: PossibleValueItem[],
315
+ references?: Record<string, ContentItem>,
316
+ externalOrigin?: string,
317
+ ): string {
318
+ if (values.length === 0) return ""
319
+
320
+ let markdown = "## Possible Values\n\n"
321
+
322
+ for (const value of values) {
323
+ if (!value.name) continue
324
+
325
+ markdown += `### \`${value.name}\`\n\n`
326
+
327
+ if (value.content && Array.isArray(value.content)) {
328
+ const text = renderContentArray(value.content, references, 0, externalOrigin).trim()
329
+ if (text) {
330
+ markdown += `${text}\n\n`
331
+ }
332
+ }
333
+ }
334
+
335
+ return markdown
336
+ }
337
+
297
338
  function renderPropertyType(
298
339
  type: Array<{ text?: string; kind?: string; identifier?: string }> | undefined,
299
340
  references?: Record<string, ContentItem>,
@@ -397,6 +438,16 @@ function renderContentArray(
397
438
  markdown += `> [!${calloutType}]\n> ${cleanContent}\n\n`
398
439
  } else if (item.type === "table") {
399
440
  markdown += renderTable(item, references, depth, externalOrigin)
441
+ } else if (item.type === "tabNavigator" && item.tabs?.length) {
442
+ for (const tab of item.tabs) {
443
+ const label = tab.title?.trim()
444
+ if (label) {
445
+ markdown += `**${label}**\n\n`
446
+ }
447
+ if (tab.content?.length) {
448
+ markdown += renderContentArray(tab.content, references, depth + 1, externalOrigin)
449
+ }
450
+ }
400
451
  }
401
452
  }
402
453
 
@@ -20,6 +20,7 @@ export type {
20
20
  LanguageVariant,
21
21
  Parameter,
22
22
  Platform,
23
+ PossibleValueItem,
23
24
  PrimaryContentSection,
24
25
  PropertyItem,
25
26
  SeeAlsoSection,