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,977 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Team Collaboration Module
5
+ *
6
+ * Manages team features including:
7
+ * - Team login/logout with invite codes
8
+ * - Setup selection from team configurations
9
+ * - Knowledge sync with AWS backend
10
+ * - Proposal management with offline queue
11
+ *
12
+ * Part of v1.8.0 Team Collaboration
13
+ *
14
+ * Note: Team features require a subscription and hosted backend.
15
+ * Without subscription, features gracefully degrade to local-only mode.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const crypto = require('crypto');
21
+ const {
22
+ getConfig,
23
+ saveConfig,
24
+ STATE_DIR,
25
+ colors,
26
+ color,
27
+ success,
28
+ warn,
29
+ error,
30
+ info,
31
+ printHeader,
32
+ fileExists,
33
+ readFile,
34
+ writeFile
35
+ } = require('./flow-utils');
36
+
37
+ // Use shared database for proposals
38
+ const memoryDb = require('./flow-memory-db');
39
+
40
+ // Decisions file path
41
+ const DECISIONS_PATH = path.join(STATE_DIR, 'decisions.md');
42
+
43
+ // ============================================================
44
+ // Constants
45
+ // ============================================================
46
+
47
+ const TEAM_STATE_FILE = path.join(STATE_DIR, 'team-state.json');
48
+ const OFFLINE_QUEUE_FILE = path.join(STATE_DIR, 'offline-queue.json');
49
+ const DEFAULT_BACKEND_URL = 'https://api.wogi-flow.com';
50
+
51
+ // Token refresh threshold (refresh 5 minutes before expiry)
52
+ const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000;
53
+
54
+ // ============================================================
55
+ // Team State Management
56
+ // ============================================================
57
+
58
+ /**
59
+ * Get current team state
60
+ */
61
+ function getTeamState() {
62
+ if (!fileExists(TEAM_STATE_FILE)) {
63
+ return {
64
+ loggedIn: false,
65
+ teamId: null,
66
+ userId: null,
67
+ teamName: null,
68
+ setupId: null,
69
+ setupName: null,
70
+ lastSync: null,
71
+ accessToken: null,
72
+ refreshToken: null,
73
+ tokenExpiresAt: null
74
+ };
75
+ }
76
+
77
+ try {
78
+ const state = JSON.parse(readFile(TEAM_STATE_FILE));
79
+ // Decrypt tokens if encrypted
80
+ if (state.accessToken && state.encrypted) {
81
+ state.accessToken = decryptToken(state.accessToken);
82
+ state.refreshToken = decryptToken(state.refreshToken);
83
+ }
84
+ return state;
85
+ } catch (err) {
86
+ return { loggedIn: false };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Save team state (with encrypted tokens)
92
+ */
93
+ function saveTeamState(state) {
94
+ const stateToSave = { ...state };
95
+
96
+ // Encrypt tokens before saving
97
+ if (stateToSave.accessToken) {
98
+ stateToSave.accessToken = encryptToken(stateToSave.accessToken);
99
+ stateToSave.refreshToken = encryptToken(stateToSave.refreshToken);
100
+ stateToSave.encrypted = true;
101
+ }
102
+
103
+ writeFile(TEAM_STATE_FILE, JSON.stringify(stateToSave, null, 2));
104
+ }
105
+
106
+ /**
107
+ * Simple token encryption (better than plain text)
108
+ */
109
+ function encryptToken(token) {
110
+ if (!token) return null;
111
+ const key = crypto.createHash('sha256').update(getMachineId()).digest();
112
+ const iv = crypto.randomBytes(16);
113
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
114
+ let encrypted = cipher.update(token, 'utf8', 'hex');
115
+ encrypted += cipher.final('hex');
116
+ return iv.toString('hex') + ':' + encrypted;
117
+ }
118
+
119
+ function decryptToken(encrypted) {
120
+ if (!encrypted) return null;
121
+ try {
122
+ const key = crypto.createHash('sha256').update(getMachineId()).digest();
123
+ const parts = encrypted.split(':');
124
+ const iv = Buffer.from(parts[0], 'hex');
125
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
126
+ let decrypted = decipher.update(parts[1], 'hex', 'utf8');
127
+ decrypted += decipher.final('utf8');
128
+ return decrypted;
129
+ } catch (err) {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ function getMachineId() {
135
+ // Use a combination of machine-specific values
136
+ return process.env.USER + process.env.HOME + __dirname;
137
+ }
138
+
139
+ /**
140
+ * Check if team features are enabled and configured
141
+ */
142
+ function isTeamEnabled() {
143
+ const config = getConfig();
144
+ return config.team?.enabled === true && config.team?.teamId;
145
+ }
146
+
147
+ /**
148
+ * Get backend URL from config or default
149
+ */
150
+ function getBackendUrl() {
151
+ const config = getConfig();
152
+ return config.team?.backendUrl || DEFAULT_BACKEND_URL;
153
+ }
154
+
155
+ // ============================================================
156
+ // Offline Queue
157
+ // ============================================================
158
+
159
+ function getOfflineQueue() {
160
+ if (!fileExists(OFFLINE_QUEUE_FILE)) return [];
161
+ try {
162
+ return JSON.parse(readFile(OFFLINE_QUEUE_FILE));
163
+ } catch (err) {
164
+ return [];
165
+ }
166
+ }
167
+
168
+ function saveOfflineQueue(queue) {
169
+ writeFile(OFFLINE_QUEUE_FILE, JSON.stringify(queue, null, 2));
170
+ }
171
+
172
+ function addToOfflineQueue(operation) {
173
+ const queue = getOfflineQueue();
174
+ queue.push({
175
+ ...operation,
176
+ queuedAt: new Date().toISOString(),
177
+ retries: 0
178
+ });
179
+ saveOfflineQueue(queue);
180
+ }
181
+
182
+ async function processOfflineQueue() {
183
+ const queue = getOfflineQueue();
184
+ if (queue.length === 0) return { processed: 0, failed: 0 };
185
+
186
+ let processed = 0;
187
+ let failed = 0;
188
+ const remaining = [];
189
+
190
+ for (const item of queue) {
191
+ try {
192
+ const result = await executeQueuedOperation(item);
193
+ if (result.success) {
194
+ processed++;
195
+ } else if (item.retries < 3) {
196
+ remaining.push({ ...item, retries: item.retries + 1 });
197
+ failed++;
198
+ }
199
+ } catch (err) {
200
+ if (item.retries < 3) {
201
+ remaining.push({ ...item, retries: item.retries + 1 });
202
+ }
203
+ failed++;
204
+ }
205
+ }
206
+
207
+ saveOfflineQueue(remaining);
208
+ return { processed, failed, remaining: remaining.length };
209
+ }
210
+
211
+ async function executeQueuedOperation(item) {
212
+ switch (item.type) {
213
+ case 'proposal':
214
+ return await apiRequest(`/teams/${item.teamId}/proposals`, {
215
+ method: 'POST',
216
+ body: JSON.stringify(item.data)
217
+ });
218
+ case 'knowledge':
219
+ return await apiRequest(`/teams/${item.teamId}/knowledge`, {
220
+ method: 'POST',
221
+ body: JSON.stringify(item.data)
222
+ });
223
+ default:
224
+ return { success: false, error: 'Unknown operation type' };
225
+ }
226
+ }
227
+
228
+ // ============================================================
229
+ // API Client (with JWT refresh and offline support)
230
+ // ============================================================
231
+
232
+ /**
233
+ * Ensure valid access token, refresh if needed
234
+ */
235
+ async function ensureValidToken() {
236
+ const state = getTeamState();
237
+
238
+ if (!state.accessToken || !state.refreshToken) {
239
+ return null;
240
+ }
241
+
242
+ // Check if token is about to expire
243
+ const expiresAt = state.tokenExpiresAt ? new Date(state.tokenExpiresAt).getTime() : 0;
244
+ const now = Date.now();
245
+
246
+ if (now > expiresAt - TOKEN_REFRESH_THRESHOLD) {
247
+ // Token expired or about to expire, refresh it
248
+ const backendUrl = getBackendUrl();
249
+
250
+ try {
251
+ const response = await fetch(`${backendUrl}/auth/refresh`, {
252
+ method: 'POST',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ refreshToken: state.refreshToken })
255
+ });
256
+
257
+ if (response.ok) {
258
+ const data = await response.json();
259
+ state.accessToken = data.accessToken;
260
+ state.tokenExpiresAt = new Date(now + (data.expiresIn || 3600) * 1000).toISOString();
261
+ saveTeamState(state);
262
+ return state.accessToken;
263
+ }
264
+ } catch (err) {
265
+ // Token refresh failed, return existing token and hope for the best
266
+ console.error('Token refresh failed:', err.message);
267
+ }
268
+ }
269
+
270
+ return state.accessToken;
271
+ }
272
+
273
+ /**
274
+ * Make authenticated request to backend
275
+ */
276
+ async function apiRequest(endpoint, options = {}) {
277
+ const backendUrl = getBackendUrl();
278
+ const url = `${backendUrl}${endpoint}`;
279
+
280
+ // Get valid token (will refresh if needed)
281
+ const token = await ensureValidToken();
282
+
283
+ const headers = {
284
+ 'Content-Type': 'application/json',
285
+ ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
286
+ ...options.headers
287
+ };
288
+
289
+ try {
290
+ const response = await fetch(url, {
291
+ ...options,
292
+ headers
293
+ });
294
+
295
+ if (response.status === 401) {
296
+ return { error: 'unauthorized', message: 'Token expired or invalid' };
297
+ }
298
+
299
+ if (response.status === 403) {
300
+ return { error: 'forbidden', message: 'Access denied' };
301
+ }
302
+
303
+ if (!response.ok) {
304
+ const data = await response.json().catch(() => ({}));
305
+ return { error: 'api_error', message: data.error || response.statusText, status: response.status };
306
+ }
307
+
308
+ return await response.json();
309
+ } catch (err) {
310
+ // Network error - add to offline queue if it's a write operation
311
+ if (options.method && ['POST', 'PUT', 'DELETE'].includes(options.method)) {
312
+ // Extract teamId from endpoint
313
+ const teamIdMatch = endpoint.match(/\/teams\/([^/]+)/);
314
+ if (teamIdMatch && options.body) {
315
+ addToOfflineQueue({
316
+ type: inferOperationType(endpoint),
317
+ teamId: teamIdMatch[1],
318
+ endpoint,
319
+ data: JSON.parse(options.body)
320
+ });
321
+ return { error: 'queued', message: 'Operation queued for sync when online' };
322
+ }
323
+ }
324
+ return { error: 'network', message: `Backend unavailable: ${err.message}` };
325
+ }
326
+ }
327
+
328
+ function inferOperationType(endpoint) {
329
+ if (endpoint.includes('/proposals')) return 'proposal';
330
+ if (endpoint.includes('/knowledge')) return 'knowledge';
331
+ return 'unknown';
332
+ }
333
+
334
+ // ============================================================
335
+ // Auto-Apply Approved Proposals (v1.8.0)
336
+ // ============================================================
337
+
338
+ /**
339
+ * Apply approved team proposals to local decisions.md
340
+ */
341
+ async function applyApprovedProposals(approvedProposals) {
342
+ if (!approvedProposals || approvedProposals.length === 0) return { applied: 0 };
343
+
344
+ // Load current decisions.md
345
+ let decisionsContent = '';
346
+ if (fileExists(DECISIONS_PATH)) {
347
+ decisionsContent = readFile(DECISIONS_PATH);
348
+ } else {
349
+ decisionsContent = '# Decisions\n\nProject coding rules and patterns.\n\n';
350
+ }
351
+
352
+ let applied = 0;
353
+ let currentContent = decisionsContent;
354
+
355
+ for (const proposal of approvedProposals) {
356
+ // Check if already in decisions.md
357
+ if (isRuleInDecisions(proposal.rule, currentContent)) {
358
+ continue;
359
+ }
360
+
361
+ // Format and add to decisions.md
362
+ const formatted = formatProposalForDecisions(proposal);
363
+ currentContent = appendToDecisions(formatted, currentContent);
364
+ applied++;
365
+
366
+ // Mark as applied in local memory
367
+ try {
368
+ await memoryDb.markFactPromoted(`proposal:${proposal.id}`, 'decisions.md');
369
+ } catch (err) {
370
+ // Ignore if fact doesn't exist locally
371
+ }
372
+ }
373
+
374
+ if (applied > 0) {
375
+ writeFile(DECISIONS_PATH, currentContent);
376
+ }
377
+
378
+ return { applied };
379
+ }
380
+
381
+ /**
382
+ * Check if rule is already in decisions.md
383
+ */
384
+ function isRuleInDecisions(rule, content) {
385
+ const keywords = rule.split(/\s+/)
386
+ .filter(w => w.length > 4)
387
+ .slice(0, 5);
388
+
389
+ let matches = 0;
390
+ for (const keyword of keywords) {
391
+ if (content.toLowerCase().includes(keyword.toLowerCase())) {
392
+ matches++;
393
+ }
394
+ }
395
+
396
+ return matches > keywords.length / 2;
397
+ }
398
+
399
+ /**
400
+ * Format proposal for decisions.md
401
+ */
402
+ function formatProposalForDecisions(proposal) {
403
+ const sectionMap = {
404
+ 'naming': 'Naming Conventions',
405
+ 'pattern': 'Coding Patterns',
406
+ 'architecture': 'Architecture Decisions',
407
+ 'styling': 'Styling Rules',
408
+ 'testing': 'Testing Conventions',
409
+ 'error-handling': 'Error Handling',
410
+ 'general': 'General Rules',
411
+ 'api': 'API Patterns',
412
+ 'component': 'Component Patterns'
413
+ };
414
+
415
+ return {
416
+ section: sectionMap[proposal.category] || 'Team-Approved Rules',
417
+ rule: `- ${proposal.rule}`,
418
+ source: '(Team-approved)'
419
+ };
420
+ }
421
+
422
+ /**
423
+ * Append formatted rule to decisions.md content
424
+ */
425
+ function appendToDecisions(formatted, content) {
426
+ const lines = content.split('\n');
427
+ let sectionIndex = -1;
428
+
429
+ // Find the section
430
+ for (let i = 0; i < lines.length; i++) {
431
+ if (lines[i].includes(formatted.section) && lines[i].startsWith('#')) {
432
+ sectionIndex = i;
433
+ break;
434
+ }
435
+ }
436
+
437
+ if (sectionIndex === -1) {
438
+ // Section doesn't exist, append at end
439
+ return content.trim() + `\n\n## ${formatted.section}\n\n${formatted.rule} ${formatted.source}\n`;
440
+ }
441
+
442
+ // Find end of section (next heading or end of file)
443
+ let insertIndex = lines.length;
444
+ for (let i = sectionIndex + 1; i < lines.length; i++) {
445
+ if (lines[i].startsWith('#')) {
446
+ insertIndex = i;
447
+ break;
448
+ }
449
+ }
450
+
451
+ // Insert before next section
452
+ lines.splice(insertIndex, 0, `${formatted.rule} ${formatted.source}`);
453
+
454
+ return lines.join('\n');
455
+ }
456
+
457
+ // ============================================================
458
+ // Team Commands
459
+ // ============================================================
460
+
461
+ /**
462
+ * Login to team with invite code or credentials
463
+ */
464
+ async function login(inviteCodeOrEmail, password) {
465
+ printHeader('Team Login');
466
+
467
+ if (!inviteCodeOrEmail) {
468
+ console.log('To join a team, you need an invite code from your team admin.');
469
+ console.log('');
470
+ console.log('Options:');
471
+ console.log(' 1. Join with invite: ./scripts/flow team login <invite-code>');
472
+ console.log(' 2. Login with email: ./scripts/flow team login <email> <password>');
473
+ console.log('');
474
+ info('Team features require a subscription at https://wogi-flow.com');
475
+ return;
476
+ }
477
+
478
+ const backendUrl = getBackendUrl();
479
+
480
+ // Determine if invite code or email login
481
+ const isEmail = inviteCodeOrEmail.includes('@');
482
+ let body;
483
+
484
+ if (isEmail) {
485
+ if (!password) {
486
+ error('Password required for email login');
487
+ return;
488
+ }
489
+ body = { email: inviteCodeOrEmail, password };
490
+ info('Logging in...');
491
+ } else {
492
+ // Prompt for email and password for invite code login
493
+ const readline = require('readline');
494
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
495
+
496
+ console.log('Setting up your account for this team...');
497
+
498
+ const email = await new Promise(resolve => rl.question('Email: ', resolve));
499
+ const pwd = await new Promise(resolve => {
500
+ process.stdout.write('Password: ');
501
+ // Note: In production, use a proper password input
502
+ rl.question('', resolve);
503
+ });
504
+ rl.close();
505
+
506
+ body = { inviteCode: inviteCodeOrEmail, email, password: pwd };
507
+ info('Validating invite and creating account...');
508
+ }
509
+
510
+ try {
511
+ const response = await fetch(`${backendUrl}/auth/login`, {
512
+ method: 'POST',
513
+ headers: { 'Content-Type': 'application/json' },
514
+ body: JSON.stringify(body)
515
+ });
516
+
517
+ if (!response.ok) {
518
+ const data = await response.json().catch(() => ({}));
519
+ if (response.status === 401) {
520
+ error(data.error || 'Invalid credentials or invite code');
521
+ } else {
522
+ error(`Login failed: ${data.error || response.statusText}`);
523
+ }
524
+ return;
525
+ }
526
+
527
+ const result = await response.json();
528
+
529
+ // Save team state with tokens
530
+ const teamState = {
531
+ loggedIn: true,
532
+ teamId: result.teamId,
533
+ userId: result.userId,
534
+ teamName: result.teamName,
535
+ accessToken: result.accessToken,
536
+ refreshToken: result.refreshToken,
537
+ tokenExpiresAt: new Date(Date.now() + (result.expiresIn || 3600) * 1000).toISOString(),
538
+ setupId: null,
539
+ setupName: null,
540
+ lastSync: null
541
+ };
542
+ saveTeamState(teamState);
543
+
544
+ // Update config
545
+ const config = getConfig();
546
+ config.team = {
547
+ ...config.team,
548
+ enabled: true,
549
+ teamId: result.teamId,
550
+ userId: result.userId,
551
+ backendUrl
552
+ };
553
+ saveConfig(config);
554
+
555
+ success(`Logged in to team: ${result.teamName || result.teamId}`);
556
+
557
+ // Trigger initial sync
558
+ console.log('');
559
+ info('Running initial sync...');
560
+ await sync({ silent: true });
561
+ success('Initial sync complete');
562
+
563
+ } catch (err) {
564
+ error(`Could not connect to team backend: ${err.message}`);
565
+ info('Check your internet connection or try again later.');
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Logout from team
571
+ */
572
+ async function logout() {
573
+ printHeader('Team Logout');
574
+
575
+ const state = getTeamState();
576
+
577
+ if (!state.loggedIn) {
578
+ warn('Not logged in to any team.');
579
+ return;
580
+ }
581
+
582
+ const teamName = state.teamName || 'team';
583
+
584
+ // Clear team state
585
+ saveTeamState({
586
+ loggedIn: false,
587
+ teamId: null,
588
+ userId: null,
589
+ teamName: null,
590
+ setupId: null,
591
+ setupName: null,
592
+ lastSync: null,
593
+ accessToken: null,
594
+ refreshToken: null,
595
+ tokenExpiresAt: null
596
+ });
597
+
598
+ // Update config
599
+ const config = getConfig();
600
+ config.team = {
601
+ ...config.team,
602
+ enabled: false
603
+ };
604
+ saveConfig(config);
605
+
606
+ success(`Logged out from ${teamName}`);
607
+ info('Local data preserved. Team features disabled.');
608
+ }
609
+
610
+ /**
611
+ * Sync with team backend
612
+ */
613
+ async function sync(options = {}) {
614
+ if (!options.silent) printHeader('Team Sync');
615
+
616
+ const state = getTeamState();
617
+
618
+ if (!state.loggedIn) {
619
+ error('Not logged in to a team.');
620
+ return { pulled: 0, pushed: 0 };
621
+ }
622
+
623
+ const { silent = false } = options;
624
+
625
+ if (!silent) info('Syncing with team...');
626
+
627
+ // 1. Process offline queue first
628
+ const queueResult = await processOfflineQueue();
629
+ if (queueResult.processed > 0 && !silent) {
630
+ info(`Processed ${queueResult.processed} queued operations`);
631
+ }
632
+
633
+ // 2. Get unsynced proposals from local database
634
+ const localProposals = await memoryDb.getUnsyncedProposals();
635
+ let pushed = 0;
636
+
637
+ // Push local proposals
638
+ for (const proposal of localProposals) {
639
+ const result = await apiRequest(`/teams/${state.teamId}/proposals`, {
640
+ method: 'POST',
641
+ body: JSON.stringify({
642
+ rule: proposal.rule,
643
+ category: proposal.category,
644
+ rationale: proposal.rationale,
645
+ sourceContext: proposal.source_context,
646
+ localId: proposal.id
647
+ })
648
+ });
649
+
650
+ if (!result.error || result.error === 'queued') {
651
+ await memoryDb.updateProposal(proposal.id, {
652
+ synced: true,
653
+ remoteId: result.id
654
+ });
655
+ pushed++;
656
+ }
657
+ }
658
+
659
+ // 3. Pull new knowledge
660
+ const pullResult = await apiRequest(`/teams/${state.teamId}/knowledge?since=${state.lastSync || ''}`);
661
+
662
+ let pulled = 0;
663
+ if (!pullResult.error && pullResult.knowledge) {
664
+ for (const item of pullResult.knowledge) {
665
+ await memoryDb.storeFact({
666
+ fact: item.fact,
667
+ category: item.category,
668
+ scope: 'team',
669
+ model: item.modelSpecific,
670
+ sourceContext: `team:${state.teamId}`
671
+ });
672
+ pulled++;
673
+ }
674
+ }
675
+
676
+ // 4. Pull approved proposals and auto-apply to decisions.md (v1.8.0)
677
+ let appliedProposals = 0;
678
+ const config = getConfig();
679
+ const autoApply = config.automaticPromotion?.autoApplyTeamApproved !== false;
680
+
681
+ if (autoApply) {
682
+ const approvedResult = await apiRequest(`/teams/${state.teamId}/proposals?status=approved&since=${state.lastSync || ''}`);
683
+
684
+ if (!approvedResult.error && approvedResult.proposals && approvedResult.proposals.length > 0) {
685
+ const applyResult = await applyApprovedProposals(approvedResult.proposals);
686
+ appliedProposals = applyResult.applied;
687
+
688
+ if (!silent && appliedProposals > 0) {
689
+ success(`Applied ${appliedProposals} team-approved rule(s) to decisions.md`);
690
+ }
691
+ }
692
+ }
693
+
694
+ // Update last sync time
695
+ state.lastSync = new Date().toISOString();
696
+ saveTeamState(state);
697
+
698
+ if (!silent) {
699
+ success(`Sync complete: ${pulled} pulled, ${pushed} pushed`);
700
+ if (appliedProposals > 0) {
701
+ info(`${appliedProposals} approved rule(s) added to decisions.md`);
702
+ }
703
+ if (queueResult.remaining > 0) {
704
+ warn(`${queueResult.remaining} operations still queued (will retry)`);
705
+ }
706
+ }
707
+
708
+ return { pulled, pushed, appliedProposals };
709
+ }
710
+
711
+ /**
712
+ * Show/vote on team proposals
713
+ */
714
+ async function proposals(action, proposalId, vote) {
715
+ printHeader('Team Proposals');
716
+
717
+ const state = getTeamState();
718
+
719
+ if (!state.loggedIn) {
720
+ error('Not logged in to a team.');
721
+ return;
722
+ }
723
+
724
+ if (action === 'vote' && proposalId && vote) {
725
+ if (!['approve', 'reject'].includes(vote)) {
726
+ error('Vote must be "approve" or "reject"');
727
+ return;
728
+ }
729
+
730
+ const result = await apiRequest(`/teams/${state.teamId}/proposals/${proposalId}/vote`, {
731
+ method: 'POST',
732
+ body: JSON.stringify({ vote, comment: '' })
733
+ });
734
+
735
+ if (result.error) {
736
+ if (result.error === 'queued') {
737
+ info('Vote queued for sync when online');
738
+ } else {
739
+ error(`Failed to vote: ${result.message}`);
740
+ }
741
+ return;
742
+ }
743
+
744
+ success(`Vote recorded: ${vote} on proposal #${proposalId}`);
745
+
746
+ if (result.status === 'approved') {
747
+ success('Proposal approved and added to team knowledge!');
748
+ } else if (result.status === 'rejected') {
749
+ info('Proposal rejected');
750
+ }
751
+ return;
752
+ }
753
+
754
+ // List pending proposals
755
+ const result = await apiRequest(`/teams/${state.teamId}/proposals?status=pending`);
756
+
757
+ if (result.error) {
758
+ error(`Failed to fetch proposals: ${result.message}`);
759
+ return;
760
+ }
761
+
762
+ if (!result.proposals || result.proposals.length === 0) {
763
+ info('No pending proposals.');
764
+ return;
765
+ }
766
+
767
+ console.log('');
768
+ console.log('Pending proposals:');
769
+ console.log('');
770
+
771
+ for (const p of result.proposals) {
772
+ const votes = p.votes || [];
773
+ const approvals = votes.filter(v => v.vote === 'approve').length;
774
+ const rejections = votes.filter(v => v.vote === 'reject').length;
775
+
776
+ console.log(` #${p.id} | ${p.category}`);
777
+ console.log(color('dim', ' ' + '─'.repeat(50)));
778
+ console.log(` Rule: "${p.rule}"`);
779
+ if (p.rationale) {
780
+ console.log(color('dim', ` Rationale: ${p.rationale}`));
781
+ }
782
+ console.log(` Votes: ${approvals} approve, ${rejections} reject`);
783
+ console.log('');
784
+ }
785
+
786
+ info('Vote with: ./scripts/flow team proposals vote <id> <approve|reject>');
787
+ }
788
+
789
+ /**
790
+ * Generate invite code (admin only)
791
+ */
792
+ async function invite(expiresInDays = 7) {
793
+ printHeader('Generate Invite');
794
+
795
+ const state = getTeamState();
796
+
797
+ if (!state.loggedIn) {
798
+ error('Not logged in to a team.');
799
+ return;
800
+ }
801
+
802
+ const result = await apiRequest(`/teams/${state.teamId}/invites`, {
803
+ method: 'POST',
804
+ body: JSON.stringify({ expiresInDays: parseInt(expiresInDays) || 7 })
805
+ });
806
+
807
+ if (result.error) {
808
+ if (result.error === 'forbidden') {
809
+ error('Only team admins can generate invite codes.');
810
+ } else {
811
+ error(`Failed to generate invite: ${result.message}`);
812
+ }
813
+ return;
814
+ }
815
+
816
+ console.log('');
817
+ console.log('Invite code generated:');
818
+ console.log('');
819
+ console.log(` ${color('green', result.code)}`);
820
+ console.log('');
821
+ console.log(`Valid for: ${expiresInDays} days`);
822
+ if (result.url) {
823
+ console.log(`Join URL: ${result.url}`);
824
+ }
825
+ console.log('');
826
+ info('Share this code with team members to join.');
827
+ }
828
+
829
+ /**
830
+ * Show team status
831
+ */
832
+ async function status() {
833
+ printHeader('Team Status');
834
+
835
+ const state = getTeamState();
836
+ const config = getConfig();
837
+
838
+ if (!state.loggedIn) {
839
+ console.log('Status: ' + color('yellow', 'Not logged in'));
840
+ console.log('');
841
+ info('Join a team: ./scripts/flow team login <invite-code>');
842
+ info('Learn more: https://wogi-flow.com/teams');
843
+ return;
844
+ }
845
+
846
+ console.log('Status: ' + color('green', 'Logged in'));
847
+ console.log('');
848
+ console.log(`Team: ${state.teamName || state.teamId}`);
849
+ console.log(`Setup: ${state.setupName || 'None selected'}`);
850
+ console.log(`Last sync: ${state.lastSync || 'Never'}`);
851
+ console.log('');
852
+
853
+ // Check token status
854
+ const expiresAt = state.tokenExpiresAt ? new Date(state.tokenExpiresAt) : null;
855
+ if (expiresAt) {
856
+ const now = new Date();
857
+ if (expiresAt < now) {
858
+ warn('Access token expired - will refresh on next request');
859
+ } else {
860
+ const hours = Math.round((expiresAt - now) / (1000 * 60 * 60));
861
+ console.log(`Token: Valid for ~${hours} hours`);
862
+ }
863
+ }
864
+ console.log('');
865
+
866
+ // Show offline queue status
867
+ const queue = getOfflineQueue();
868
+ if (queue.length > 0) {
869
+ warn(`${queue.length} operation(s) queued for sync`);
870
+ }
871
+
872
+ // Show stats from database
873
+ const stats = await memoryDb.getStats();
874
+ console.log('Local stats:');
875
+ console.log(` Facts: ${stats.facts.total}`);
876
+ console.log(` Proposals: ${stats.proposals.total} (${stats.proposals.pending} pending)`);
877
+ }
878
+
879
+ // ============================================================
880
+ // CLI
881
+ // ============================================================
882
+
883
+ function printUsage() {
884
+ console.log(`
885
+ Wogi Flow - Team Collaboration
886
+
887
+ Usage: ./scripts/flow team <command> [args]
888
+
889
+ Commands:
890
+ login <code> Join team with invite code
891
+ login <email> <pass> Login with email and password
892
+ logout Leave current team
893
+ sync Sync knowledge with team
894
+ proposals List pending proposals
895
+ proposals vote <id> <approve|reject>
896
+ Vote on a proposal
897
+ invite [days] Generate invite code (admin only)
898
+ status Show team status
899
+
900
+ Examples:
901
+ ./scripts/flow team login ABC123XY
902
+ ./scripts/flow team login user@example.com mypassword
903
+ ./scripts/flow team sync
904
+ ./scripts/flow team proposals vote prop_abc123 approve
905
+ ./scripts/flow team invite 14
906
+
907
+ Team features require a subscription at https://wogi-flow.com
908
+ `);
909
+ }
910
+
911
+ async function main() {
912
+ const args = process.argv.slice(2);
913
+ const command = args[0];
914
+
915
+ switch (command) {
916
+ case 'login':
917
+ await login(args[1], args[2]);
918
+ break;
919
+
920
+ case 'logout':
921
+ await logout();
922
+ break;
923
+
924
+ case 'sync':
925
+ await sync();
926
+ break;
927
+
928
+ case 'proposals':
929
+ await proposals(args[1], args[2], args[3]);
930
+ break;
931
+
932
+ case 'invite':
933
+ await invite(args[1]);
934
+ break;
935
+
936
+ case 'status':
937
+ await status();
938
+ break;
939
+
940
+ case '--help':
941
+ case '-h':
942
+ case 'help':
943
+ printUsage();
944
+ break;
945
+
946
+ default:
947
+ if (command) {
948
+ error(`Unknown command: ${command}`);
949
+ }
950
+ printUsage();
951
+ process.exit(command ? 1 : 0);
952
+ }
953
+ }
954
+
955
+ // ============================================================
956
+ // Exports
957
+ // ============================================================
958
+
959
+ module.exports = {
960
+ getTeamState,
961
+ saveTeamState,
962
+ isTeamEnabled,
963
+ login,
964
+ logout,
965
+ sync,
966
+ proposals,
967
+ invite,
968
+ status,
969
+ processOfflineQueue
970
+ };
971
+
972
+ if (require.main === module) {
973
+ main().catch(e => {
974
+ error(`Error: ${err.message}`);
975
+ process.exit(1);
976
+ });
977
+ }