swift-skills 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.
Files changed (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +410 -0
  3. package/build/cli/auth.d.ts +3 -0
  4. package/build/cli/auth.d.ts.map +1 -0
  5. package/build/cli/auth.js +44 -0
  6. package/build/cli/auth.js.map +1 -0
  7. package/build/cli/setup.d.ts +2 -0
  8. package/build/cli/setup.d.ts.map +1 -0
  9. package/build/cli/setup.js +150 -0
  10. package/build/cli/setup.js.map +1 -0
  11. package/build/cli/source-manager.d.ts +3 -0
  12. package/build/cli/source-manager.d.ts.map +1 -0
  13. package/build/cli/source-manager.js +117 -0
  14. package/build/cli/source-manager.js.map +1 -0
  15. package/build/config/creators.d.ts +11 -0
  16. package/build/config/creators.d.ts.map +1 -0
  17. package/build/config/creators.js +32 -0
  18. package/build/config/creators.js.map +1 -0
  19. package/build/config/sources.d.ts +91 -0
  20. package/build/config/sources.d.ts.map +1 -0
  21. package/build/config/sources.js +231 -0
  22. package/build/config/sources.js.map +1 -0
  23. package/build/config/sources.test.d.ts +2 -0
  24. package/build/config/sources.test.d.ts.map +1 -0
  25. package/build/config/sources.test.js +199 -0
  26. package/build/config/sources.test.js.map +1 -0
  27. package/build/config/swift-keywords.d.ts +29 -0
  28. package/build/config/swift-keywords.d.ts.map +1 -0
  29. package/build/config/swift-keywords.js +77 -0
  30. package/build/config/swift-keywords.js.map +1 -0
  31. package/build/index.d.ts +3 -0
  32. package/build/index.d.ts.map +1 -0
  33. package/build/index.js +181 -0
  34. package/build/index.js.map +1 -0
  35. package/build/integration/cache-behavior.test.d.ts +2 -0
  36. package/build/integration/cache-behavior.test.d.ts.map +1 -0
  37. package/build/integration/cache-behavior.test.js +521 -0
  38. package/build/integration/cache-behavior.test.js.map +1 -0
  39. package/build/integration/mcp-client.test.d.ts +2 -0
  40. package/build/integration/mcp-client.test.d.ts.map +1 -0
  41. package/build/integration/mcp-client.test.js +82 -0
  42. package/build/integration/mcp-client.test.js.map +1 -0
  43. package/build/integration/response-quality.test.d.ts +2 -0
  44. package/build/integration/response-quality.test.d.ts.map +1 -0
  45. package/build/integration/response-quality.test.js +230 -0
  46. package/build/integration/response-quality.test.js.map +1 -0
  47. package/build/integration/test-client.d.ts +25 -0
  48. package/build/integration/test-client.d.ts.map +1 -0
  49. package/build/integration/test-client.js +93 -0
  50. package/build/integration/test-client.js.map +1 -0
  51. package/build/sources/free/nilcoalescing.d.ts +8 -0
  52. package/build/sources/free/nilcoalescing.d.ts.map +1 -0
  53. package/build/sources/free/nilcoalescing.js +26 -0
  54. package/build/sources/free/nilcoalescing.js.map +1 -0
  55. package/build/sources/free/nilcoalescing.test.d.ts +2 -0
  56. package/build/sources/free/nilcoalescing.test.d.ts.map +1 -0
  57. package/build/sources/free/nilcoalescing.test.js +63 -0
  58. package/build/sources/free/nilcoalescing.test.js.map +1 -0
  59. package/build/sources/free/pointfree.d.ts +15 -0
  60. package/build/sources/free/pointfree.d.ts.map +1 -0
  61. package/build/sources/free/pointfree.js +175 -0
  62. package/build/sources/free/pointfree.js.map +1 -0
  63. package/build/sources/free/pointfree.test.d.ts +2 -0
  64. package/build/sources/free/pointfree.test.d.ts.map +1 -0
  65. package/build/sources/free/pointfree.test.js +86 -0
  66. package/build/sources/free/pointfree.test.js.map +1 -0
  67. package/build/sources/free/rssPatternSource.d.ts +42 -0
  68. package/build/sources/free/rssPatternSource.d.ts.map +1 -0
  69. package/build/sources/free/rssPatternSource.js +109 -0
  70. package/build/sources/free/rssPatternSource.js.map +1 -0
  71. package/build/sources/free/rssPatternSource.test.d.ts +2 -0
  72. package/build/sources/free/rssPatternSource.test.d.ts.map +1 -0
  73. package/build/sources/free/rssPatternSource.test.js +89 -0
  74. package/build/sources/free/rssPatternSource.test.js.map +1 -0
  75. package/build/sources/free/sundell.d.ts +8 -0
  76. package/build/sources/free/sundell.d.ts.map +1 -0
  77. package/build/sources/free/sundell.js +17 -0
  78. package/build/sources/free/sundell.js.map +1 -0
  79. package/build/sources/free/sundell.test.d.ts +2 -0
  80. package/build/sources/free/sundell.test.d.ts.map +1 -0
  81. package/build/sources/free/sundell.test.js +63 -0
  82. package/build/sources/free/sundell.test.js.map +1 -0
  83. package/build/sources/free/vanderlee.d.ts +8 -0
  84. package/build/sources/free/vanderlee.d.ts.map +1 -0
  85. package/build/sources/free/vanderlee.js +63 -0
  86. package/build/sources/free/vanderlee.js.map +1 -0
  87. package/build/sources/free/vanderlee.test.d.ts +2 -0
  88. package/build/sources/free/vanderlee.test.d.ts.map +1 -0
  89. package/build/sources/free/vanderlee.test.js +77 -0
  90. package/build/sources/free/vanderlee.test.js.map +1 -0
  91. package/build/sources/premium/patreon-dl.d.ts +45 -0
  92. package/build/sources/premium/patreon-dl.d.ts.map +1 -0
  93. package/build/sources/premium/patreon-dl.js +189 -0
  94. package/build/sources/premium/patreon-dl.js.map +1 -0
  95. package/build/sources/premium/patreon-fetch.d.ts +3 -0
  96. package/build/sources/premium/patreon-fetch.d.ts.map +1 -0
  97. package/build/sources/premium/patreon-fetch.js +18 -0
  98. package/build/sources/premium/patreon-fetch.js.map +1 -0
  99. package/build/sources/premium/patreon-oauth.d.ts +24 -0
  100. package/build/sources/premium/patreon-oauth.d.ts.map +1 -0
  101. package/build/sources/premium/patreon-oauth.js +208 -0
  102. package/build/sources/premium/patreon-oauth.js.map +1 -0
  103. package/build/sources/premium/patreon-zip.d.ts +17 -0
  104. package/build/sources/premium/patreon-zip.d.ts.map +1 -0
  105. package/build/sources/premium/patreon-zip.js +127 -0
  106. package/build/sources/premium/patreon-zip.js.map +1 -0
  107. package/build/sources/premium/patreon.d.ts +48 -0
  108. package/build/sources/premium/patreon.d.ts.map +1 -0
  109. package/build/sources/premium/patreon.js +221 -0
  110. package/build/sources/premium/patreon.js.map +1 -0
  111. package/build/sources/premium/youtube.d.ts +14 -0
  112. package/build/sources/premium/youtube.d.ts.map +1 -0
  113. package/build/sources/premium/youtube.js +92 -0
  114. package/build/sources/premium/youtube.js.map +1 -0
  115. package/build/tools/extract-cookie.d.ts +2 -0
  116. package/build/tools/extract-cookie.d.ts.map +1 -0
  117. package/build/tools/extract-cookie.js +40 -0
  118. package/build/tools/extract-cookie.js.map +1 -0
  119. package/build/tools/handlers/enableSource.d.ts +3 -0
  120. package/build/tools/handlers/enableSource.d.ts.map +1 -0
  121. package/build/tools/handlers/enableSource.js +25 -0
  122. package/build/tools/handlers/enableSource.js.map +1 -0
  123. package/build/tools/handlers/getPatreonPatterns.d.ts +3 -0
  124. package/build/tools/handlers/getPatreonPatterns.d.ts.map +1 -0
  125. package/build/tools/handlers/getPatreonPatterns.js +43 -0
  126. package/build/tools/handlers/getPatreonPatterns.js.map +1 -0
  127. package/build/tools/handlers/getSwiftPattern.d.ts +3 -0
  128. package/build/tools/handlers/getSwiftPattern.d.ts.map +1 -0
  129. package/build/tools/handlers/getSwiftPattern.js +72 -0
  130. package/build/tools/handlers/getSwiftPattern.js.map +1 -0
  131. package/build/tools/handlers/handlers.test.d.ts +2 -0
  132. package/build/tools/handlers/handlers.test.d.ts.map +1 -0
  133. package/build/tools/handlers/handlers.test.js +359 -0
  134. package/build/tools/handlers/handlers.test.js.map +1 -0
  135. package/build/tools/handlers/listContentSources.d.ts +3 -0
  136. package/build/tools/handlers/listContentSources.d.ts.map +1 -0
  137. package/build/tools/handlers/listContentSources.js +34 -0
  138. package/build/tools/handlers/listContentSources.js.map +1 -0
  139. package/build/tools/handlers/searchSwiftContent.d.ts +3 -0
  140. package/build/tools/handlers/searchSwiftContent.d.ts.map +1 -0
  141. package/build/tools/handlers/searchSwiftContent.js +121 -0
  142. package/build/tools/handlers/searchSwiftContent.js.map +1 -0
  143. package/build/tools/handlers/setupPatreon.d.ts +3 -0
  144. package/build/tools/handlers/setupPatreon.d.ts.map +1 -0
  145. package/build/tools/handlers/setupPatreon.js +40 -0
  146. package/build/tools/handlers/setupPatreon.js.map +1 -0
  147. package/build/tools/index.d.ts +3 -0
  148. package/build/tools/index.d.ts.map +1 -0
  149. package/build/tools/index.js +18 -0
  150. package/build/tools/index.js.map +1 -0
  151. package/build/tools/registry.d.ts +14 -0
  152. package/build/tools/registry.d.ts.map +1 -0
  153. package/build/tools/registry.js +21 -0
  154. package/build/tools/registry.js.map +1 -0
  155. package/build/tools/registry.test.d.ts +2 -0
  156. package/build/tools/registry.test.d.ts.map +1 -0
  157. package/build/tools/registry.test.js +54 -0
  158. package/build/tools/registry.test.js.map +1 -0
  159. package/build/tools/types.d.ts +67 -0
  160. package/build/tools/types.d.ts.map +1 -0
  161. package/build/tools/types.js +3 -0
  162. package/build/tools/types.js.map +1 -0
  163. package/build/utils/cache.d.ts +20 -0
  164. package/build/utils/cache.d.ts.map +1 -0
  165. package/build/utils/cache.js +186 -0
  166. package/build/utils/cache.js.map +1 -0
  167. package/build/utils/concurrency.d.ts +13 -0
  168. package/build/utils/concurrency.d.ts.map +1 -0
  169. package/build/utils/concurrency.js +33 -0
  170. package/build/utils/concurrency.js.map +1 -0
  171. package/build/utils/errors.d.ts +19 -0
  172. package/build/utils/errors.d.ts.map +1 -0
  173. package/build/utils/errors.js +35 -0
  174. package/build/utils/errors.js.map +1 -0
  175. package/build/utils/fetch.d.ts +6 -0
  176. package/build/utils/fetch.d.ts.map +1 -0
  177. package/build/utils/fetch.js +6 -0
  178. package/build/utils/fetch.js.map +1 -0
  179. package/build/utils/http.d.ts +21 -0
  180. package/build/utils/http.d.ts.map +1 -0
  181. package/build/utils/http.js +53 -0
  182. package/build/utils/http.js.map +1 -0
  183. package/build/utils/intent-cache.d.ts +94 -0
  184. package/build/utils/intent-cache.d.ts.map +1 -0
  185. package/build/utils/intent-cache.js +164 -0
  186. package/build/utils/intent-cache.js.map +1 -0
  187. package/build/utils/intent-cache.test.d.ts +2 -0
  188. package/build/utils/intent-cache.test.d.ts.map +1 -0
  189. package/build/utils/intent-cache.test.js +290 -0
  190. package/build/utils/intent-cache.test.js.map +1 -0
  191. package/build/utils/logger.d.ts +4 -0
  192. package/build/utils/logger.d.ts.map +1 -0
  193. package/build/utils/logger.js +9 -0
  194. package/build/utils/logger.js.map +1 -0
  195. package/build/utils/paths.d.ts +27 -0
  196. package/build/utils/paths.d.ts.map +1 -0
  197. package/build/utils/paths.js +43 -0
  198. package/build/utils/paths.js.map +1 -0
  199. package/build/utils/pattern-formatter.d.ts +40 -0
  200. package/build/utils/pattern-formatter.d.ts.map +1 -0
  201. package/build/utils/pattern-formatter.js +124 -0
  202. package/build/utils/pattern-formatter.js.map +1 -0
  203. package/build/utils/response-helpers.d.ts +17 -0
  204. package/build/utils/response-helpers.d.ts.map +1 -0
  205. package/build/utils/response-helpers.js +34 -0
  206. package/build/utils/response-helpers.js.map +1 -0
  207. package/build/utils/search-terms.d.ts +17 -0
  208. package/build/utils/search-terms.d.ts.map +1 -0
  209. package/build/utils/search-terms.js +71 -0
  210. package/build/utils/search-terms.js.map +1 -0
  211. package/build/utils/search-terms.test.d.ts +2 -0
  212. package/build/utils/search-terms.test.d.ts.map +1 -0
  213. package/build/utils/search-terms.test.js +107 -0
  214. package/build/utils/search-terms.test.js.map +1 -0
  215. package/build/utils/search.d.ts +48 -0
  216. package/build/utils/search.d.ts.map +1 -0
  217. package/build/utils/search.js +158 -0
  218. package/build/utils/search.js.map +1 -0
  219. package/build/utils/search.test.d.ts +2 -0
  220. package/build/utils/search.test.d.ts.map +1 -0
  221. package/build/utils/search.test.js +199 -0
  222. package/build/utils/search.test.js.map +1 -0
  223. package/build/utils/semantic-recall.d.ts +38 -0
  224. package/build/utils/semantic-recall.d.ts.map +1 -0
  225. package/build/utils/semantic-recall.js +134 -0
  226. package/build/utils/semantic-recall.js.map +1 -0
  227. package/build/utils/semantic-recall.test.d.ts +2 -0
  228. package/build/utils/semantic-recall.test.d.ts.map +1 -0
  229. package/build/utils/semantic-recall.test.js +326 -0
  230. package/build/utils/semantic-recall.test.js.map +1 -0
  231. package/build/utils/source-registry.d.ts +45 -0
  232. package/build/utils/source-registry.d.ts.map +1 -0
  233. package/build/utils/source-registry.js +113 -0
  234. package/build/utils/source-registry.js.map +1 -0
  235. package/build/utils/source-registry.test.d.ts +2 -0
  236. package/build/utils/source-registry.test.d.ts.map +1 -0
  237. package/build/utils/source-registry.test.js +206 -0
  238. package/build/utils/source-registry.test.js.map +1 -0
  239. package/build/utils/swift-analysis.d.ts +61 -0
  240. package/build/utils/swift-analysis.d.ts.map +1 -0
  241. package/build/utils/swift-analysis.js +339 -0
  242. package/build/utils/swift-analysis.js.map +1 -0
  243. package/build/utils/swift-analysis.test.d.ts +2 -0
  244. package/build/utils/swift-analysis.test.d.ts.map +1 -0
  245. package/build/utils/swift-analysis.test.js +473 -0
  246. package/build/utils/swift-analysis.test.js.map +1 -0
  247. package/package.json +85 -0
@@ -0,0 +1,208 @@
1
+ // src/sources/premium/patreon-oauth.ts
2
+ import http from 'http';
3
+ import { exec } from 'child_process';
4
+ import { URL } from 'url';
5
+ import { join } from 'path';
6
+ import { homedir } from 'os';
7
+ import fs from 'fs/promises';
8
+ import keytar from 'keytar';
9
+ import { fetch } from '../../utils/fetch.js';
10
+ const SERVICE_NAME = 'swift-patterns-mcp';
11
+ const ACCOUNT_NAME = 'patreon-tokens';
12
+ const CALLBACK_PORT = 9876;
13
+ const PATREON_AUTH_URL = 'https://www.patreon.com/oauth2/authorize';
14
+ const PATREON_TOKEN_URL = 'https://www.patreon.com/api/oauth2/token';
15
+ // Scopes explained:
16
+ // - identity: Basic user info
17
+ // - identity.memberships: REQUIRED - access to patron memberships via /identity endpoint
18
+ // - campaigns: Access campaigns you manage (creator-only, optional)
19
+ // - campaigns.members: Access members of your own campaign (creator-only, optional)
20
+ export const PATREON_SCOPES = [
21
+ 'identity',
22
+ 'identity.memberships', // REQUIRED: patron memberships live here
23
+ 'campaigns',
24
+ 'campaigns.members',
25
+ ].join(' ');
26
+ export async function saveTokens(tokens) {
27
+ const encrypted = JSON.stringify(tokens);
28
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, encrypted);
29
+ }
30
+ export async function loadTokens() {
31
+ try {
32
+ const encrypted = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
33
+ if (!encrypted)
34
+ return null;
35
+ return JSON.parse(encrypted);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export async function clearTokens() {
42
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
43
+ }
44
+ export function isTokenExpired(tokens) {
45
+ // Refresh 5 minutes before actual expiry
46
+ return Date.now() >= (tokens.expires_at - 5 * 60 * 1000);
47
+ }
48
+ export async function refreshAccessToken(clientId, clientSecret, refreshToken) {
49
+ const response = await fetch(PATREON_TOKEN_URL, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
52
+ body: new URLSearchParams({
53
+ grant_type: 'refresh_token',
54
+ refresh_token: refreshToken,
55
+ client_id: clientId,
56
+ client_secret: clientSecret,
57
+ }),
58
+ });
59
+ if (!response.ok) {
60
+ throw new Error(`Token refresh failed: ${response.status}`);
61
+ }
62
+ const data = await response.json();
63
+ const tokens = {
64
+ access_token: data.access_token,
65
+ refresh_token: data.refresh_token,
66
+ expires_at: Date.now() + data.expires_in * 1000,
67
+ scope: data.scope,
68
+ };
69
+ await saveTokens(tokens);
70
+ return tokens;
71
+ }
72
+ export async function startOAuthFlow(clientId, clientSecret) {
73
+ return new Promise((resolve) => {
74
+ const redirectUri = `http://localhost:${CALLBACK_PORT}/callback`;
75
+ const authUrl = new URL(PATREON_AUTH_URL);
76
+ authUrl.searchParams.set('client_id', clientId);
77
+ authUrl.searchParams.set('redirect_uri', redirectUri);
78
+ authUrl.searchParams.set('response_type', 'code');
79
+ authUrl.searchParams.set('scope', PATREON_SCOPES);
80
+ let serverClosed = false;
81
+ const server = http.createServer(async (req, res) => {
82
+ if (!req.url?.startsWith('/callback')) {
83
+ res.writeHead(404);
84
+ res.end('Not found');
85
+ return;
86
+ }
87
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
88
+ const code = url.searchParams.get('code');
89
+ const error = url.searchParams.get('error');
90
+ if (error) {
91
+ res.writeHead(200, { 'Content-Type': 'text/html' });
92
+ res.end('<h1>Authorization Denied</h1><p>You can close this window.</p>');
93
+ if (!serverClosed) {
94
+ serverClosed = true;
95
+ server.close();
96
+ resolve({ success: false, error });
97
+ }
98
+ return;
99
+ }
100
+ if (!code) {
101
+ res.writeHead(400, { 'Content-Type': 'text/html' });
102
+ res.end('<h1>Error</h1><p>No authorization code received.</p>');
103
+ return;
104
+ }
105
+ try {
106
+ // Exchange code for tokens
107
+ const tokenResponse = await fetch(PATREON_TOKEN_URL, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
110
+ body: new URLSearchParams({
111
+ code,
112
+ grant_type: 'authorization_code',
113
+ client_id: clientId,
114
+ client_secret: clientSecret,
115
+ redirect_uri: redirectUri,
116
+ }),
117
+ });
118
+ if (!tokenResponse.ok) {
119
+ throw new Error(`Token exchange failed: ${tokenResponse.status}`);
120
+ }
121
+ const data = await tokenResponse.json();
122
+ const tokens = {
123
+ access_token: data.access_token,
124
+ refresh_token: data.refresh_token,
125
+ expires_at: Date.now() + data.expires_in * 1000,
126
+ scope: data.scope,
127
+ };
128
+ await saveTokens(tokens);
129
+ res.writeHead(200, { 'Content-Type': 'text/html' });
130
+ res.end(`
131
+ <html>
132
+ <body style="font-family: system-ui; text-align: center; padding: 50px;">
133
+ <h1>Authorization Successful!</h1>
134
+ <p>You can close this window and return to the terminal.</p>
135
+ </body>
136
+ </html>
137
+ `);
138
+ if (!serverClosed) {
139
+ serverClosed = true;
140
+ server.close();
141
+ resolve({ success: true, tokens });
142
+ }
143
+ }
144
+ catch (err) {
145
+ res.writeHead(500, { 'Content-Type': 'text/html' });
146
+ res.end('<h1>Error</h1><p>Failed to complete authorization.</p>');
147
+ if (!serverClosed) {
148
+ serverClosed = true;
149
+ server.close();
150
+ resolve({ success: false, error: String(err) });
151
+ }
152
+ }
153
+ });
154
+ server.listen(CALLBACK_PORT, '127.0.0.1', () => {
155
+ console.log(`\nOpening browser for Patreon authorization...`);
156
+ console.log(`If browser doesn't open, visit: ${authUrl.toString()}\n`);
157
+ // Open browser
158
+ const cmd = process.platform === 'darwin' ? 'open' :
159
+ process.platform === 'win32' ? 'start' : 'xdg-open';
160
+ exec(`${cmd} "${authUrl.toString()}"`);
161
+ });
162
+ // Timeout after 60 seconds
163
+ setTimeout(() => {
164
+ if (!serverClosed) {
165
+ serverClosed = true;
166
+ server.close();
167
+ resolve({ success: false, error: 'Authorization timed out after 60 seconds' });
168
+ }
169
+ }, 60000);
170
+ });
171
+ }
172
+ export async function getValidAccessToken(clientId, clientSecret) {
173
+ const tokens = await loadTokens();
174
+ if (!tokens)
175
+ return null;
176
+ if (isTokenExpired(tokens)) {
177
+ try {
178
+ const refreshed = await refreshAccessToken(clientId, clientSecret, tokens.refresh_token);
179
+ return refreshed.access_token;
180
+ }
181
+ catch {
182
+ // Refresh failed, need to re-authenticate
183
+ await clearTokens();
184
+ return null;
185
+ }
186
+ }
187
+ return tokens.access_token;
188
+ }
189
+ // =============================================================================
190
+ // Cleanup & Reset
191
+ // =============================================================================
192
+ /**
193
+ * Clear all Patreon authentication data (keytar + legacy tokens.json)
194
+ */
195
+ export async function clearPatreonAuth() {
196
+ // Clear from keytar
197
+ await keytar.deletePassword(SERVICE_NAME, "patreon");
198
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
199
+ // Clear legacy tokens.json file
200
+ const tokensPath = join(homedir(), ".swift-patterns-mcp", "tokens.json");
201
+ try {
202
+ await fs.rm(tokensPath, { force: true });
203
+ }
204
+ catch {
205
+ // Ignore if file doesn't exist
206
+ }
207
+ }
208
+ //# sourceMappingURL=patreon-oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patreon-oauth.js","sourceRoot":"","sources":["../../../src/sources/premium/patreon-oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export interface ExtractedPattern {
2
+ filename: string;
3
+ content: string;
4
+ type: 'swift' | 'markdown' | 'playground' | 'other';
5
+ hasCode: boolean;
6
+ topics: string[];
7
+ }
8
+ export interface ZipExtractionResult {
9
+ success: boolean;
10
+ patterns: ExtractedPattern[];
11
+ warnings: string[];
12
+ }
13
+ export declare function downloadZip(url: string, postId: string, accessToken: string): Promise<string | null>;
14
+ export declare function extractZip(zipPath: string, postId: string): ZipExtractionResult;
15
+ export declare function extractFromAttachment(attachmentUrl: string, postId: string, accessToken: string): Promise<ZipExtractionResult>;
16
+ export declare function clearZipCache(): void;
17
+ //# sourceMappingURL=patreon-zip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patreon-zip.d.ts","sourceRoot":"","sources":["../../../src/sources/premium/patreon-zip.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CAAC;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAuBD,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmCxB;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAwD/E;AAED,wBAAsB,qBAAqB,CACzC,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAY9B;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC"}
@@ -0,0 +1,127 @@
1
+ // src/sources/premium/patreon-zip.ts
2
+ import AdmZip from 'adm-zip';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { getCacheDir } from '../../utils/paths.js';
6
+ import { detectTopics, hasCodeContent } from '../../utils/swift-analysis.js';
7
+ import { createSourceConfig } from '../../config/swift-keywords.js';
8
+ import { logError } from '../../utils/errors.js';
9
+ import { fetch } from '../../utils/fetch.js';
10
+ const MAX_ZIP_SIZE = 50 * 1024 * 1024; // 50MB
11
+ const MAX_FILES = 100;
12
+ const { topicKeywords: zipTopicKeywords } = createSourceConfig({
13
+ 'concurrency': ['sendable'],
14
+ 'networking': ['request'],
15
+ 'testing': ['stub'],
16
+ 'architecture': ['repository', 'usecase'],
17
+ 'uikit': ['uitableview', 'uicollectionview'],
18
+ }, {});
19
+ function detectFileType(filename) {
20
+ const ext = path.extname(filename).toLowerCase();
21
+ switch (ext) {
22
+ case '.swift': return 'swift';
23
+ case '.md':
24
+ case '.markdown': return 'markdown';
25
+ case '.playground': return 'playground';
26
+ default: return 'other';
27
+ }
28
+ }
29
+ export async function downloadZip(url, postId, accessToken) {
30
+ const cacheDir = getCacheDir('zips');
31
+ const zipPath = path.join(cacheDir, `${postId}.zip`);
32
+ // Check if already cached
33
+ if (fs.existsSync(zipPath)) {
34
+ return zipPath;
35
+ }
36
+ try {
37
+ const response = await fetch(url, {
38
+ headers: { 'Authorization': `Bearer ${accessToken}` },
39
+ });
40
+ if (!response.ok) {
41
+ logError('Patreon Zip', `Download failed: ${response.status}`, { postId });
42
+ return null;
43
+ }
44
+ const contentLength = response.headers.get('content-length');
45
+ if (contentLength && parseInt(contentLength, 10) > MAX_ZIP_SIZE) {
46
+ return null;
47
+ }
48
+ // Ensure cache directory exists
49
+ fs.mkdirSync(cacheDir, { recursive: true });
50
+ const buffer = Buffer.from(await response.arrayBuffer());
51
+ fs.writeFileSync(zipPath, buffer);
52
+ return zipPath;
53
+ }
54
+ catch (error) {
55
+ logError('Patreon Zip', error, { postId });
56
+ return null;
57
+ }
58
+ }
59
+ export function extractZip(zipPath, postId) {
60
+ const warnings = [];
61
+ const patterns = [];
62
+ const destDir = path.join(getCacheDir('zips'), postId);
63
+ try {
64
+ const zip = new AdmZip(zipPath);
65
+ const entries = zip.getEntries();
66
+ if (entries.length > MAX_FILES) {
67
+ warnings.push(`Zip contains ${entries.length} files, extracting first ${MAX_FILES}`);
68
+ }
69
+ // Ensure destination exists
70
+ fs.mkdirSync(destDir, { recursive: true });
71
+ let count = 0;
72
+ for (const entry of entries) {
73
+ if (count >= MAX_FILES)
74
+ break;
75
+ if (entry.isDirectory)
76
+ continue;
77
+ const filename = entry.entryName;
78
+ const type = detectFileType(filename);
79
+ // Skip non-relevant files
80
+ if (type === 'other')
81
+ continue;
82
+ try {
83
+ const content = entry.getData().toString('utf8');
84
+ const text = `${filename} ${content}`;
85
+ const topics = detectTopics(text, zipTopicKeywords);
86
+ const hasCode = hasCodeContent(content);
87
+ patterns.push({
88
+ filename,
89
+ content,
90
+ type,
91
+ hasCode,
92
+ topics,
93
+ });
94
+ count++;
95
+ }
96
+ catch (err) {
97
+ warnings.push(`Failed to read ${filename}: ${err}`);
98
+ }
99
+ }
100
+ return { success: true, patterns, warnings };
101
+ }
102
+ catch (error) {
103
+ return {
104
+ success: false,
105
+ patterns: [],
106
+ warnings: [`Extraction failed: ${error}`],
107
+ };
108
+ }
109
+ }
110
+ export async function extractFromAttachment(attachmentUrl, postId, accessToken) {
111
+ const zipPath = await downloadZip(attachmentUrl, postId, accessToken);
112
+ if (!zipPath) {
113
+ return {
114
+ success: false,
115
+ patterns: [],
116
+ warnings: ['Failed to download zip attachment'],
117
+ };
118
+ }
119
+ return extractZip(zipPath, postId);
120
+ }
121
+ export function clearZipCache() {
122
+ const cacheDir = getCacheDir('zips');
123
+ if (fs.existsSync(cacheDir)) {
124
+ fs.rmSync(cacheDir, { recursive: true });
125
+ }
126
+ }
127
+ //# sourceMappingURL=patreon-zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patreon-zip.js","sourceRoot":"","sources":["../../../src/sources/premium/patreon-zip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ export interface PatreonPattern {
2
+ id: string;
3
+ title: string;
4
+ url: string;
5
+ publishDate: string;
6
+ excerpt: string;
7
+ content: string;
8
+ creator: string;
9
+ topics: string[];
10
+ relevanceScore: number;
11
+ hasCode: boolean;
12
+ }
13
+ export interface CreatorInfo {
14
+ id: string;
15
+ name: string;
16
+ url: string;
17
+ isSwiftRelated: boolean;
18
+ }
19
+ export declare class PatreonSource {
20
+ private clientId;
21
+ private clientSecret;
22
+ private enabledCreators;
23
+ constructor();
24
+ private loadEnabledCreators;
25
+ saveEnabledCreators(creatorIds: string[]): void;
26
+ isConfigured(): Promise<boolean>;
27
+ /**
28
+ * Returns creators the user PAYS (patron memberships).
29
+ *
30
+ * IMPORTANT: This uses the identity endpoint with memberships.campaign include,
31
+ * NOT the /campaigns endpoint. The /campaigns endpoint only returns campaigns
32
+ * you OWN as a creator, not campaigns you subscribe to as a patron.
33
+ *
34
+ * Correct API: GET /identity?include=memberships.campaign
35
+ */
36
+ getSubscribedCreators(): Promise<CreatorInfo[]>;
37
+ detectSwiftCreators(): Promise<CreatorInfo[]>;
38
+ fetchPatterns(creatorId?: string): Promise<PatreonPattern[]>;
39
+ /**
40
+ * Convert downloaded post to patterns
41
+ */
42
+ private downloadedPostToPatterns;
43
+ private videoToPattern;
44
+ searchPatterns(query: string): Promise<PatreonPattern[]>;
45
+ isAvailable(): boolean;
46
+ }
47
+ export default PatreonSource;
48
+ //# sourceMappingURL=patreon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patreon.d.ts","sourceRoot":"","sources":["../../../src/sources/premium/patreon.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,OAAO,CAAC;CACzB;AA+CD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAAgB;;IAQvC,OAAO,CAAC,mBAAmB;IAY3B,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI;IAOzC,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC;IAMtC;;;;;;;;OAQG;IACG,qBAAqB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IA0D/C,mBAAmB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAK7C,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAoDlE;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA8BhC,OAAO,CAAC,cAAc;IAoBhB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAqB9D,WAAW,IAAI,OAAO;CAGvB;AAED,eAAe,aAAa,CAAC"}
@@ -0,0 +1,221 @@
1
+ // src/sources/premium/patreon.ts
2
+ import { loadTokens, getValidAccessToken } from './patreon-oauth.js';
3
+ import { getChannelVideos, searchVideos } from './youtube.js';
4
+ import { scanDownloadedContent } from './patreon-dl.js';
5
+ import { getByPatreonId } from '../../config/creators.js';
6
+ import { getPatreonCreatorsPath } from '../../utils/paths.js';
7
+ import { detectTopics, hasCodeContent, calculateRelevance } from '../../utils/swift-analysis.js';
8
+ import { createSourceConfig } from '../../config/swift-keywords.js';
9
+ import { logError } from '../../utils/errors.js';
10
+ import { fetch } from '../../utils/fetch.js';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ const PATREON_API = 'https://www.patreon.com/api/oauth2/v2';
14
+ const { topicKeywords: patreonTopicKeywords, qualitySignals: patreonQualitySignals } = createSourceConfig({ 'swiftui': ['@observable'], 'architecture': ['clean architecture'] }, { 'swift': 10, 'ios': 8, 'pattern': 6, 'best practice': 8 });
15
+ // Patreon-specific scoring constants
16
+ const PATREON_CODE_BONUS = 15; // Higher bonus for code-heavy Patreon content
17
+ const PATREON_BASE_SCORE = 0; // Start at 0 for Patreon to rely on quality signals
18
+ function isSwiftRelated(name, summary) {
19
+ const text = `${name} ${summary || ''}`.toLowerCase();
20
+ const keywords = ['swift', 'swiftui', 'ios', 'apple', 'xcode', 'uikit', 'iphone', 'ipad'];
21
+ return keywords.some(k => text.includes(k));
22
+ }
23
+ export class PatreonSource {
24
+ clientId;
25
+ clientSecret;
26
+ enabledCreators = [];
27
+ constructor() {
28
+ this.clientId = process.env.PATREON_CLIENT_ID || '';
29
+ this.clientSecret = process.env.PATREON_CLIENT_SECRET || '';
30
+ this.loadEnabledCreators();
31
+ }
32
+ loadEnabledCreators() {
33
+ try {
34
+ const configPath = getPatreonCreatorsPath();
35
+ if (fs.existsSync(configPath)) {
36
+ const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
37
+ this.enabledCreators = data.enabledCreators || [];
38
+ }
39
+ }
40
+ catch {
41
+ this.enabledCreators = [];
42
+ }
43
+ }
44
+ saveEnabledCreators(creatorIds) {
45
+ this.enabledCreators = creatorIds;
46
+ const configPath = getPatreonCreatorsPath();
47
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
48
+ fs.writeFileSync(configPath, JSON.stringify({ enabledCreators: creatorIds }, null, 2));
49
+ }
50
+ async isConfigured() {
51
+ if (!this.clientId || !this.clientSecret)
52
+ return false;
53
+ const tokens = await loadTokens();
54
+ return tokens !== null;
55
+ }
56
+ /**
57
+ * Returns creators the user PAYS (patron memberships).
58
+ *
59
+ * IMPORTANT: This uses the identity endpoint with memberships.campaign include,
60
+ * NOT the /campaigns endpoint. The /campaigns endpoint only returns campaigns
61
+ * you OWN as a creator, not campaigns you subscribe to as a patron.
62
+ *
63
+ * Correct API: GET /identity?include=memberships.campaign
64
+ */
65
+ async getSubscribedCreators() {
66
+ const accessToken = await getValidAccessToken(this.clientId, this.clientSecret);
67
+ if (!accessToken)
68
+ return [];
69
+ try {
70
+ const url = `${PATREON_API}/identity?include=memberships.campaign&fields[user]=full_name,email&fields[member]=patron_status&fields[campaign]=creation_name,url,summary`;
71
+ const response = await fetch(url, {
72
+ headers: { 'Authorization': `Bearer ${accessToken}` }
73
+ });
74
+ if (!response.ok) {
75
+ logError('Patreon', `Failed to fetch memberships: ${response.status}`);
76
+ return [];
77
+ }
78
+ const data = await response.json();
79
+ if (!data.included) {
80
+ return [];
81
+ }
82
+ // Extract active memberships and their campaigns
83
+ const members = data.included.filter((item) => item.type === 'member' &&
84
+ item.attributes?.patron_status === 'active_patron');
85
+ const campaigns = data.included.filter((item) => item.type === 'campaign');
86
+ // Get campaign IDs from active memberships
87
+ const activeCampaignIds = new Set(members
88
+ .map(m => m.relationships?.campaign?.data?.id)
89
+ .filter((id) => !!id));
90
+ // Return campaigns user is actively subscribed to
91
+ return campaigns
92
+ .filter(c => activeCampaignIds.has(c.id))
93
+ .map(campaign => ({
94
+ id: campaign.id,
95
+ name: campaign.attributes.creation_name,
96
+ url: campaign.attributes.url,
97
+ isSwiftRelated: isSwiftRelated(campaign.attributes.creation_name, campaign.attributes.summary),
98
+ }));
99
+ }
100
+ catch (error) {
101
+ logError('Patreon', error);
102
+ return [];
103
+ }
104
+ }
105
+ async detectSwiftCreators() {
106
+ const creators = await this.getSubscribedCreators();
107
+ return creators.filter(c => c.isSwiftRelated);
108
+ }
109
+ async fetchPatterns(creatorId) {
110
+ const patterns = [];
111
+ const creatorsToFetch = creatorId
112
+ ? [creatorId]
113
+ : this.enabledCreators;
114
+ // 1. Scan locally downloaded content (from patreon-dl)
115
+ const downloadedPosts = scanDownloadedContent();
116
+ for (const post of downloadedPosts) {
117
+ // Filter by creator if specified
118
+ const creator = creatorsToFetch.length > 0
119
+ ? getByPatreonId(creatorsToFetch.find(id => {
120
+ const c = getByPatreonId(id);
121
+ return c?.name === post.creator;
122
+ }) || '')
123
+ : null;
124
+ if (creatorsToFetch.length > 0 && !creator)
125
+ continue;
126
+ patterns.push(...this.downloadedPostToPatterns(post));
127
+ }
128
+ // 2. Fetch YouTube videos for additional metadata
129
+ for (const patreonId of creatorsToFetch) {
130
+ const creator = getByPatreonId(patreonId);
131
+ if (!creator?.youtubeChannelId)
132
+ continue;
133
+ try {
134
+ const videos = await getChannelVideos(creator.youtubeChannelId, 50);
135
+ for (const video of videos) {
136
+ // Add video as pattern (skip if we already have downloaded content for this)
137
+ const hasDownloaded = patterns.some(p => p.url === video.patreonLink || p.title.includes(video.title));
138
+ if (!hasDownloaded) {
139
+ patterns.push(this.videoToPattern(video, creator.name));
140
+ }
141
+ }
142
+ }
143
+ catch (error) {
144
+ logError('Patreon', error, { creator: creator.name });
145
+ }
146
+ }
147
+ // Sort by date (newest first)
148
+ patterns.sort((a, b) => new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime());
149
+ return patterns;
150
+ }
151
+ /**
152
+ * Convert downloaded post to patterns
153
+ */
154
+ downloadedPostToPatterns(post) {
155
+ const patterns = [];
156
+ for (const file of post.files) {
157
+ if (file.type === 'other')
158
+ continue;
159
+ const content = file.content || '';
160
+ const text = `${file.filename} ${content}`;
161
+ const topics = detectTopics(text, patreonTopicKeywords);
162
+ const hasCode = file.type === 'swift' || hasCodeContent(content);
163
+ const relevanceScore = calculateRelevance(text, hasCode, patreonQualitySignals, PATREON_BASE_SCORE, PATREON_CODE_BONUS);
164
+ patterns.push({
165
+ id: `dl-${post.postId}-${file.filename}`,
166
+ title: `${post.title} - ${file.filename}`,
167
+ url: `file://${file.filepath}`,
168
+ publishDate: post.publishDate || new Date().toISOString(),
169
+ excerpt: content.substring(0, 300),
170
+ content,
171
+ creator: post.creator,
172
+ topics,
173
+ relevanceScore,
174
+ hasCode,
175
+ });
176
+ }
177
+ return patterns;
178
+ }
179
+ videoToPattern(video, creatorName) {
180
+ const text = `${video.title} ${video.description}`;
181
+ const topics = detectTopics(text, patreonTopicKeywords);
182
+ const hasCode = hasCodeContent(video.description) || (video.codeLinks?.length ?? 0) > 0;
183
+ const relevanceScore = calculateRelevance(text, hasCode, patreonQualitySignals, PATREON_BASE_SCORE, PATREON_CODE_BONUS);
184
+ return {
185
+ id: `yt-${video.id}`,
186
+ title: video.title,
187
+ url: video.patreonLink || `https://youtube.com/watch?v=${video.id}`,
188
+ publishDate: video.publishedAt,
189
+ excerpt: video.description.substring(0, 300),
190
+ content: video.description,
191
+ creator: creatorName,
192
+ topics,
193
+ relevanceScore,
194
+ hasCode,
195
+ };
196
+ }
197
+ async searchPatterns(query) {
198
+ const patterns = [];
199
+ // Search YouTube for each enabled creator
200
+ for (const patreonId of this.enabledCreators) {
201
+ const creator = getByPatreonId(patreonId);
202
+ if (!creator?.youtubeChannelId)
203
+ continue;
204
+ try {
205
+ const videos = await searchVideos(query, creator.youtubeChannelId, 25);
206
+ for (const video of videos) {
207
+ patterns.push(this.videoToPattern(video, creator.name));
208
+ }
209
+ }
210
+ catch (error) {
211
+ logError('Patreon', error, { creator: creator.name, query });
212
+ }
213
+ }
214
+ return patterns;
215
+ }
216
+ isAvailable() {
217
+ return !!(this.clientId && this.clientSecret);
218
+ }
219
+ }
220
+ export default PatreonSource;
221
+ //# sourceMappingURL=patreon.js.map