wogiflow 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.
Files changed (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,774 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - External Context Protocol
5
+ *
6
+ * Manage external links and artifacts:
7
+ * - links.yaml for referencing external resources
8
+ * - Fetch and cache external context
9
+ * - Support for Notion, Figma, GitHub, and custom URLs
10
+ *
11
+ * Usage as module:
12
+ * const { loadLinks, fetchLink, getLinkedContext } = require('./flow-links');
13
+ * const links = loadLinks();
14
+ * const content = await fetchLink('design');
15
+ *
16
+ * Usage as CLI:
17
+ * flow links list # List all links
18
+ * flow links add <name> <url> # Add a link
19
+ * flow links fetch <name> # Fetch and cache link
20
+ * flow links show <name> # Show cached content
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const https = require('https');
26
+ const http = require('http');
27
+ const dns = require('dns');
28
+ const { getProjectRoot, colors: c } = require('./flow-utils');
29
+
30
+ const PROJECT_ROOT = getProjectRoot();
31
+ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
32
+ const LINKS_PATH = path.join(WORKFLOW_DIR, 'links.yaml');
33
+ const LINKS_JSON_PATH = path.join(WORKFLOW_DIR, 'links.json');
34
+ const CACHE_DIR = path.join(WORKFLOW_DIR, 'cache', 'links');
35
+
36
+ /**
37
+ * Link types
38
+ */
39
+ const LINK_TYPES = {
40
+ NOTION: 'notion',
41
+ FIGMA: 'figma',
42
+ GITHUB: 'github',
43
+ JIRA: 'jira',
44
+ LINEAR: 'linear',
45
+ URL: 'url',
46
+ FILE: 'file'
47
+ };
48
+
49
+ /**
50
+ * Simple YAML parser (for links.yaml)
51
+ */
52
+ function parseYaml(content) {
53
+ const result = {};
54
+ let currentSection = null;
55
+ let currentItem = null;
56
+
57
+ const lines = content.split('\n');
58
+
59
+ for (const line of lines) {
60
+ const trimmed = line.trim();
61
+
62
+ // Skip comments and empty lines
63
+ if (!trimmed || trimmed.startsWith('#')) continue;
64
+
65
+ // Top-level key (no indent)
66
+ if (!line.startsWith(' ') && !line.startsWith('\t') && trimmed.includes(':')) {
67
+ const [key, ...valueParts] = trimmed.split(':');
68
+ const value = valueParts.join(':').trim();
69
+
70
+ if (!value) {
71
+ // Section header
72
+ currentSection = key;
73
+ result[currentSection] = {};
74
+ currentItem = null;
75
+ } else {
76
+ // Simple key-value
77
+ result[key] = value;
78
+ }
79
+ }
80
+ // Nested item
81
+ else if (currentSection && trimmed.includes(':')) {
82
+ const indent = line.match(/^(\s*)/)[1].length;
83
+ const [key, ...valueParts] = trimmed.split(':');
84
+ const value = valueParts.join(':').trim();
85
+
86
+ if (indent <= 2) {
87
+ // New item in section
88
+ currentItem = key;
89
+ result[currentSection][currentItem] = value || {};
90
+ } else if (currentItem && typeof result[currentSection][currentItem] === 'object') {
91
+ // Property of current item
92
+ result[currentSection][currentItem][key] = value;
93
+ }
94
+ }
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Generate YAML from object
102
+ */
103
+ function toYaml(obj, indent = 0) {
104
+ let result = '';
105
+ const spaces = ' '.repeat(indent);
106
+
107
+ for (const [key, value] of Object.entries(obj)) {
108
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
109
+ result += `${spaces}${key}:\n`;
110
+ result += toYaml(value, indent + 1);
111
+ } else if (Array.isArray(value)) {
112
+ result += `${spaces}${key}:\n`;
113
+ for (const item of value) {
114
+ result += `${spaces} - ${item}\n`;
115
+ }
116
+ } else {
117
+ result += `${spaces}${key}: ${value}\n`;
118
+ }
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Load links from YAML or JSON
126
+ */
127
+ function loadLinks() {
128
+ // Try YAML first
129
+ if (fs.existsSync(LINKS_PATH)) {
130
+ try {
131
+ const content = fs.readFileSync(LINKS_PATH, 'utf-8');
132
+ return parseYaml(content);
133
+ } catch {
134
+ // Fall through to JSON
135
+ }
136
+ }
137
+
138
+ // Try JSON
139
+ if (fs.existsSync(LINKS_JSON_PATH)) {
140
+ try {
141
+ return JSON.parse(fs.readFileSync(LINKS_JSON_PATH, 'utf-8'));
142
+ } catch {
143
+ return {};
144
+ }
145
+ }
146
+
147
+ return {};
148
+ }
149
+
150
+ /**
151
+ * Save links to YAML
152
+ */
153
+ function saveLinks(links) {
154
+ const yaml = `# Wogi Flow - External Links
155
+ # Reference external resources (docs, designs, issues)
156
+ # Run: flow links fetch <name> to cache content
157
+
158
+ ${toYaml(links)}`;
159
+
160
+ fs.writeFileSync(LINKS_PATH, yaml);
161
+ }
162
+
163
+ /**
164
+ * Detect link type from URL
165
+ */
166
+ function detectLinkType(url) {
167
+ if (url.includes('notion.so') || url.includes('notion.site')) {
168
+ return LINK_TYPES.NOTION;
169
+ }
170
+ if (url.includes('figma.com')) {
171
+ return LINK_TYPES.FIGMA;
172
+ }
173
+ if (url.includes('github.com')) {
174
+ return LINK_TYPES.GITHUB;
175
+ }
176
+ if (url.includes('atlassian.net') || url.includes('jira.')) {
177
+ return LINK_TYPES.JIRA;
178
+ }
179
+ if (url.includes('linear.app')) {
180
+ return LINK_TYPES.LINEAR;
181
+ }
182
+ if (url.startsWith('file://') || url.startsWith('/') || url.startsWith('./')) {
183
+ return LINK_TYPES.FILE;
184
+ }
185
+ return LINK_TYPES.URL;
186
+ }
187
+
188
+ /**
189
+ * Check if an IP address is private/internal (SSRF protection)
190
+ */
191
+ function isPrivateIP(ip) {
192
+ // Handle IPv4
193
+ const parts = ip.split('.').map(Number);
194
+ if (parts.length === 4) {
195
+ return (
196
+ ip === '127.0.0.1' ||
197
+ ip.startsWith('127.') ||
198
+ parts[0] === 10 ||
199
+ (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
200
+ (parts[0] === 192 && parts[1] === 168) ||
201
+ parts[0] === 0 ||
202
+ ip === '255.255.255.255'
203
+ );
204
+ }
205
+ // Handle IPv6 loopback and link-local
206
+ return ip === '::1' || ip.startsWith('fe80:') || ip.startsWith('fc') || ip.startsWith('fd');
207
+ }
208
+
209
+ /**
210
+ * Validate URL for SSRF (resolve hostname and check IP)
211
+ */
212
+ async function validateUrlForSSRF(urlString, options = {}) {
213
+ const url = new URL(urlString);
214
+
215
+ // Block non-HTTP(S) protocols
216
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
217
+ throw new Error(`SSRF Protection: Only HTTP(S) protocols allowed, got ${url.protocol}`);
218
+ }
219
+
220
+ // Require HTTPS unless explicitly allowed
221
+ if (url.protocol !== 'https:' && !options.allowHttp) {
222
+ throw new Error('SSRF Protection: Only HTTPS URLs allowed. Use allowHttp option to override.');
223
+ }
224
+
225
+ // Block localhost and common internal hostnames
226
+ const hostname = url.hostname.toLowerCase();
227
+ if (hostname === 'localhost' || hostname.endsWith('.local') || hostname.endsWith('.internal')) {
228
+ throw new Error(`SSRF Protection: Internal hostnames not allowed: ${hostname}`);
229
+ }
230
+
231
+ // Resolve hostname and check IP addresses
232
+ try {
233
+ const addresses = await dns.promises.resolve4(hostname).catch(() => []);
234
+ const addresses6 = await dns.promises.resolve6(hostname).catch(() => []);
235
+ const allAddresses = [...addresses, ...addresses6];
236
+
237
+ if (allAddresses.length === 0) {
238
+ // Could not resolve - might be internal DNS, allow with caution
239
+ return;
240
+ }
241
+
242
+ for (const ip of allAddresses) {
243
+ if (isPrivateIP(ip)) {
244
+ throw new Error(`SSRF Protection: URL resolves to private IP address: ${ip}`);
245
+ }
246
+ }
247
+ } catch (err) {
248
+ if (err.message.startsWith('SSRF')) throw err;
249
+ // DNS resolution failed - proceed with caution (might be valid external host)
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Fetch content from URL (with SSRF protection)
255
+ */
256
+ async function fetchUrl(url, options = {}) {
257
+ // Validate URL for SSRF before making request
258
+ await validateUrlForSSRF(url, options);
259
+
260
+ return new Promise((resolve, reject) => {
261
+ const urlObj = new URL(url);
262
+ const protocol = urlObj.protocol === 'https:' ? https : http;
263
+
264
+ const reqOptions = {
265
+ method: 'GET',
266
+ hostname: urlObj.hostname,
267
+ port: urlObj.port,
268
+ path: urlObj.pathname + urlObj.search,
269
+ headers: {
270
+ 'User-Agent': 'Wogi-Flow/1.0',
271
+ ...options.headers
272
+ },
273
+ timeout: options.timeout || 30000
274
+ };
275
+
276
+ const req = protocol.request(reqOptions, (res) => {
277
+ // Handle redirects (with SSRF check)
278
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
279
+ // Validate redirect target for SSRF
280
+ validateUrlForSSRF(res.headers.location, options)
281
+ .then(() => fetchUrl(res.headers.location, options))
282
+ .then(resolve)
283
+ .catch(reject);
284
+ return;
285
+ }
286
+
287
+ if (res.statusCode !== 200) {
288
+ reject(new Error(`HTTP ${res.statusCode}`));
289
+ return;
290
+ }
291
+
292
+ let data = '';
293
+ res.on('data', chunk => data += chunk);
294
+ res.on('end', () => resolve(data));
295
+ });
296
+
297
+ req.on('error', reject);
298
+ req.on('timeout', () => reject(new Error('Request timeout')));
299
+ req.end();
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Extract text content from HTML
305
+ */
306
+ function extractTextFromHtml(html) {
307
+ // Remove script and style tags
308
+ let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
309
+ text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
310
+
311
+ // Remove HTML tags
312
+ text = text.replace(/<[^>]+>/g, ' ');
313
+
314
+ // Decode common entities
315
+ text = text.replace(/&nbsp;/g, ' ');
316
+ text = text.replace(/&amp;/g, '&');
317
+ text = text.replace(/&lt;/g, '<');
318
+ text = text.replace(/&gt;/g, '>');
319
+ text = text.replace(/&quot;/g, '"');
320
+
321
+ // Clean up whitespace
322
+ text = text.replace(/\s+/g, ' ').trim();
323
+
324
+ return text;
325
+ }
326
+
327
+ /**
328
+ * Fetch and process a link
329
+ */
330
+ async function fetchLink(name, links = null) {
331
+ links = links || loadLinks();
332
+
333
+ // Find the link
334
+ let linkData = null;
335
+ let section = null;
336
+
337
+ for (const [sec, items] of Object.entries(links)) {
338
+ if (typeof items === 'object' && items[name]) {
339
+ linkData = items[name];
340
+ section = sec;
341
+ break;
342
+ }
343
+ }
344
+
345
+ if (!linkData) {
346
+ throw new Error(`Link not found: ${name}`);
347
+ }
348
+
349
+ const url = typeof linkData === 'string' ? linkData : linkData.url;
350
+ if (!url) {
351
+ throw new Error(`No URL for link: ${name}`);
352
+ }
353
+
354
+ const type = detectLinkType(url);
355
+ let content = null;
356
+
357
+ // Fetch based on type
358
+ switch (type) {
359
+ case LINK_TYPES.FILE: {
360
+ const filePath = url.startsWith('file://') ? url.slice(7) : url;
361
+ const absPath = path.isAbsolute(filePath)
362
+ ? filePath
363
+ : path.join(PROJECT_ROOT, filePath);
364
+
365
+ if (fs.existsSync(absPath)) {
366
+ content = fs.readFileSync(absPath, 'utf-8');
367
+ } else {
368
+ throw new Error(`File not found: ${absPath}`);
369
+ }
370
+ break;
371
+ }
372
+
373
+ case LINK_TYPES.GITHUB: {
374
+ // Convert GitHub URL to raw content URL
375
+ let rawUrl = url;
376
+ if (url.includes('github.com') && url.includes('/blob/')) {
377
+ rawUrl = url
378
+ .replace('github.com', 'raw.githubusercontent.com')
379
+ .replace('/blob/', '/');
380
+ }
381
+ const html = await fetchUrl(rawUrl);
382
+ content = html.includes('<html') ? extractTextFromHtml(html) : html;
383
+ break;
384
+ }
385
+
386
+ default: {
387
+ const html = await fetchUrl(url);
388
+ content = extractTextFromHtml(html);
389
+ }
390
+ }
391
+
392
+ // Cache the content
393
+ if (content) {
394
+ if (!fs.existsSync(CACHE_DIR)) {
395
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
396
+ }
397
+
398
+ const cacheFile = path.join(CACHE_DIR, `${name}.txt`);
399
+ const metadata = {
400
+ name,
401
+ url,
402
+ type,
403
+ section,
404
+ fetchedAt: new Date().toISOString(),
405
+ contentLength: content.length
406
+ };
407
+
408
+ fs.writeFileSync(cacheFile, content);
409
+ fs.writeFileSync(
410
+ path.join(CACHE_DIR, `${name}.meta.json`),
411
+ JSON.stringify(metadata, null, 2)
412
+ );
413
+ }
414
+
415
+ return {
416
+ name,
417
+ url,
418
+ type,
419
+ content,
420
+ contentLength: content?.length || 0
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Get cached content for a link
426
+ */
427
+ function getCachedContent(name) {
428
+ const cacheFile = path.join(CACHE_DIR, `${name}.txt`);
429
+ const metaFile = path.join(CACHE_DIR, `${name}.meta.json`);
430
+
431
+ if (!fs.existsSync(cacheFile)) {
432
+ return null;
433
+ }
434
+
435
+ const content = fs.readFileSync(cacheFile, 'utf-8');
436
+ let metadata = {};
437
+
438
+ if (fs.existsSync(metaFile)) {
439
+ try {
440
+ metadata = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
441
+ } catch {
442
+ // Ignore
443
+ }
444
+ }
445
+
446
+ return {
447
+ content,
448
+ ...metadata
449
+ };
450
+ }
451
+
452
+ /**
453
+ * Add a new link
454
+ */
455
+ function addLink(name, url, section = 'links') {
456
+ const links = loadLinks();
457
+
458
+ if (!links[section]) {
459
+ links[section] = {};
460
+ }
461
+
462
+ links[section][name] = url;
463
+ saveLinks(links);
464
+
465
+ return links;
466
+ }
467
+
468
+ /**
469
+ * Remove a link
470
+ */
471
+ function removeLink(name) {
472
+ const links = loadLinks();
473
+
474
+ for (const section of Object.keys(links)) {
475
+ if (typeof links[section] === 'object' && links[section][name]) {
476
+ delete links[section][name];
477
+ break;
478
+ }
479
+ }
480
+
481
+ saveLinks(links);
482
+
483
+ // Remove cache
484
+ const cacheFile = path.join(CACHE_DIR, `${name}.txt`);
485
+ const metaFile = path.join(CACHE_DIR, `${name}.meta.json`);
486
+
487
+ if (fs.existsSync(cacheFile)) fs.unlinkSync(cacheFile);
488
+ if (fs.existsSync(metaFile)) fs.unlinkSync(metaFile);
489
+
490
+ return links;
491
+ }
492
+
493
+ /**
494
+ * Get all linked context for LLM prompts
495
+ */
496
+ function getLinkedContext(linkNames = null) {
497
+ const links = loadLinks();
498
+ let context = '';
499
+
500
+ const processLink = (name) => {
501
+ const cached = getCachedContent(name);
502
+ if (cached) {
503
+ context += `\n## ${name}\n`;
504
+ context += `Source: ${cached.url || 'unknown'}\n`;
505
+ context += `Fetched: ${cached.fetchedAt || 'unknown'}\n\n`;
506
+ context += cached.content.slice(0, 5000); // Limit content
507
+ if (cached.content.length > 5000) {
508
+ context += '\n[... truncated ...]';
509
+ }
510
+ context += '\n\n';
511
+ }
512
+ };
513
+
514
+ if (linkNames) {
515
+ for (const name of linkNames) {
516
+ processLink(name);
517
+ }
518
+ } else {
519
+ // Get all cached links
520
+ if (fs.existsSync(CACHE_DIR)) {
521
+ const files = fs.readdirSync(CACHE_DIR)
522
+ .filter(f => f.endsWith('.txt'))
523
+ .map(f => f.replace('.txt', ''));
524
+
525
+ for (const name of files) {
526
+ processLink(name);
527
+ }
528
+ }
529
+ }
530
+
531
+ return context;
532
+ }
533
+
534
+ /**
535
+ * List all links with status
536
+ */
537
+ function listLinks() {
538
+ const links = loadLinks();
539
+ const result = [];
540
+
541
+ for (const [section, items] of Object.entries(links)) {
542
+ if (typeof items !== 'object') continue;
543
+
544
+ for (const [name, data] of Object.entries(items)) {
545
+ const url = typeof data === 'string' ? data : data.url;
546
+ const type = detectLinkType(url);
547
+ const cached = getCachedContent(name);
548
+
549
+ result.push({
550
+ name,
551
+ section,
552
+ url,
553
+ type,
554
+ cached: !!cached,
555
+ cachedAt: cached?.fetchedAt
556
+ });
557
+ }
558
+ }
559
+
560
+ return result;
561
+ }
562
+
563
+ /**
564
+ * Initialize links file with template
565
+ */
566
+ function initLinks() {
567
+ if (fs.existsSync(LINKS_PATH)) {
568
+ return false;
569
+ }
570
+
571
+ const template = {
572
+ docs: {
573
+ prd: './docs/PRD.md',
574
+ api: 'https://api.example.com/docs'
575
+ },
576
+ design: {
577
+ figma: 'https://figma.com/file/...'
578
+ },
579
+ issues: {
580
+ backlog: 'https://linear.app/...'
581
+ }
582
+ };
583
+
584
+ saveLinks(template);
585
+ return true;
586
+ }
587
+
588
+ // Module exports
589
+ module.exports = {
590
+ LINK_TYPES,
591
+ loadLinks,
592
+ saveLinks,
593
+ addLink,
594
+ removeLink,
595
+ fetchLink,
596
+ getCachedContent,
597
+ getLinkedContext,
598
+ listLinks,
599
+ initLinks,
600
+ detectLinkType
601
+ };
602
+
603
+ // CLI Handler
604
+ if (require.main === module) {
605
+ const args = process.argv.slice(2);
606
+ const command = args[0];
607
+
608
+ async function main() {
609
+ switch (command) {
610
+ case 'list': {
611
+ const links = listLinks();
612
+
613
+ if (links.length === 0) {
614
+ console.log(`${c.dim}No links configured.${c.reset}`);
615
+ console.log(`${c.dim}Run "flow links init" to create template.${c.reset}`);
616
+ return;
617
+ }
618
+
619
+ console.log(`\n${c.cyan}${c.bold}External Links${c.reset}\n`);
620
+
621
+ // Group by section
622
+ const sections = {};
623
+ for (const link of links) {
624
+ if (!sections[link.section]) {
625
+ sections[link.section] = [];
626
+ }
627
+ sections[link.section].push(link);
628
+ }
629
+
630
+ for (const [section, items] of Object.entries(sections)) {
631
+ console.log(`${c.bold}${section}${c.reset}`);
632
+ for (const link of items) {
633
+ const status = link.cached
634
+ ? `${c.green}✓ cached${c.reset}`
635
+ : `${c.dim}not cached${c.reset}`;
636
+ console.log(` ${link.name}: ${c.dim}${link.url.slice(0, 50)}...${c.reset} [${status}]`);
637
+ }
638
+ console.log('');
639
+ }
640
+ break;
641
+ }
642
+
643
+ case 'add': {
644
+ const name = args[1];
645
+ const url = args[2];
646
+ const section = args[3] || 'links';
647
+
648
+ if (!name || !url) {
649
+ console.error(`${c.red}Error: Name and URL required${c.reset}`);
650
+ console.log(`${c.dim}Usage: flow links add <name> <url> [section]${c.reset}`);
651
+ process.exit(1);
652
+ }
653
+
654
+ addLink(name, url, section);
655
+ console.log(`${c.green}✅ Added link: ${name}${c.reset}`);
656
+ break;
657
+ }
658
+
659
+ case 'remove': {
660
+ const name = args[1];
661
+ if (!name) {
662
+ console.error(`${c.red}Error: Link name required${c.reset}`);
663
+ process.exit(1);
664
+ }
665
+
666
+ removeLink(name);
667
+ console.log(`${c.green}✅ Removed link: ${name}${c.reset}`);
668
+ break;
669
+ }
670
+
671
+ case 'fetch': {
672
+ const name = args[1];
673
+ if (!name) {
674
+ console.error(`${c.red}Error: Link name required${c.reset}`);
675
+ process.exit(1);
676
+ }
677
+
678
+ console.log(`${c.cyan}Fetching ${name}...${c.reset}`);
679
+ try {
680
+ const result = await fetchLink(name);
681
+ console.log(`${c.green}✅ Fetched and cached: ${result.contentLength} chars${c.reset}`);
682
+ } catch (err) {
683
+ console.error(`${c.red}Error: ${err.message}${c.reset}`);
684
+ process.exit(1);
685
+ }
686
+ break;
687
+ }
688
+
689
+ case 'show': {
690
+ const name = args[1];
691
+ if (!name) {
692
+ console.error(`${c.red}Error: Link name required${c.reset}`);
693
+ process.exit(1);
694
+ }
695
+
696
+ const cached = getCachedContent(name);
697
+ if (!cached) {
698
+ console.error(`${c.yellow}Not cached. Run: flow links fetch ${name}${c.reset}`);
699
+ process.exit(1);
700
+ }
701
+
702
+ console.log(`\n${c.cyan}${c.bold}${name}${c.reset}`);
703
+ console.log(`${c.dim}URL: ${cached.url}${c.reset}`);
704
+ console.log(`${c.dim}Fetched: ${cached.fetchedAt}${c.reset}`);
705
+ console.log(`${c.dim}Length: ${cached.contentLength} chars${c.reset}`);
706
+ console.log(`${'─'.repeat(60)}`);
707
+ console.log(cached.content.slice(0, 2000));
708
+ if (cached.content.length > 2000) {
709
+ console.log(`\n${c.dim}... (${cached.content.length - 2000} more chars)${c.reset}`);
710
+ }
711
+ break;
712
+ }
713
+
714
+ case 'init': {
715
+ const created = initLinks();
716
+ if (created) {
717
+ console.log(`${c.green}✅ Created links.yaml template${c.reset}`);
718
+ } else {
719
+ console.log(`${c.yellow}links.yaml already exists${c.reset}`);
720
+ }
721
+ break;
722
+ }
723
+
724
+ case 'context': {
725
+ const linkNames = args.slice(1);
726
+ const context = getLinkedContext(linkNames.length > 0 ? linkNames : null);
727
+
728
+ if (!context) {
729
+ console.log(`${c.dim}No cached content. Run "flow links fetch <name>" first.${c.reset}`);
730
+ } else {
731
+ console.log(context);
732
+ }
733
+ break;
734
+ }
735
+
736
+ default: {
737
+ console.log(`
738
+ ${c.cyan}Wogi Flow - External Context Protocol${c.reset}
739
+
740
+ ${c.bold}Usage:${c.reset}
741
+ flow links list List all configured links
742
+ flow links add <name> <url> Add a new link
743
+ flow links remove <name> Remove a link
744
+ flow links fetch <name> Fetch and cache link content
745
+ flow links show <name> Show cached content
746
+ flow links context [names...] Get linked context for prompts
747
+ flow links init Create template links.yaml
748
+
749
+ ${c.bold}Supported Sources:${c.reset}
750
+ - Notion pages
751
+ - Figma files
752
+ - GitHub files/repos
753
+ - Jira/Linear issues
754
+ - Any URL (HTML extracted)
755
+ - Local files
756
+
757
+ ${c.bold}Configuration:${c.reset}
758
+ Edit .workflow/links.yaml:
759
+
760
+ docs:
761
+ prd: ./docs/PRD.md
762
+ api: https://api.example.com/docs
763
+ design:
764
+ figma: https://figma.com/file/...
765
+ `);
766
+ }
767
+ }
768
+ }
769
+
770
+ main().catch(err => {
771
+ console.error(`${c.red}Error: ${err.message}${c.reset}`);
772
+ process.exit(1);
773
+ });
774
+ }