decisionnode 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/ai/gemini.d.ts +15 -0
- package/dist/ai/gemini.js +56 -0
- package/dist/ai/rag.d.ts +79 -0
- package/dist/ai/rag.js +268 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1724 -0
- package/dist/cloud.d.ts +177 -0
- package/dist/cloud.js +631 -0
- package/dist/env.d.ts +47 -0
- package/dist/env.js +139 -0
- package/dist/history.d.ts +34 -0
- package/dist/history.js +159 -0
- package/dist/maintenance.d.ts +7 -0
- package/dist/maintenance.js +49 -0
- package/dist/marketplace.d.ts +46 -0
- package/dist/marketplace.js +300 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +621 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +132 -0
- package/dist/store.d.ts +126 -0
- package/dist/store.js +555 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +9 -0
- package/package.json +57 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1724 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { listDecisions, getDecisionById, addDecision, updateDecision, deleteDecision, deleteScope, getNextDecisionId, renumberDecisions, importDecisions, getAvailableScopes, listProjects, saveDecisions, listGlobalDecisions, getGlobalDecisionById, addGlobalDecision, updateGlobalDecision, deleteGlobalDecision, getNextGlobalDecisionId, getGlobalScopes } from './store.js';
|
|
3
|
+
import { getHistory, getSnapshot, getDecisionsFromSnapshot, logBatchAction, logAction } from './history.js';
|
|
4
|
+
import { getSearchSensitivity, setSearchSensitivity, isGlobalId } from './env.js';
|
|
5
|
+
import { loginToCloud, logoutFromCloud, getCloudStatus, syncDecisionsToCloud, getCloudSyncStatus, pullDecisionsFromCloud, detectConflicts, resolveConflict, updateSyncMetadata, saveIncomingChanges, removeIncomingChanges } from './cloud.js';
|
|
6
|
+
import { getProjectRoot } from './env.js';
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args[0];
|
|
12
|
+
async function main() {
|
|
13
|
+
try {
|
|
14
|
+
switch (command) {
|
|
15
|
+
case 'list':
|
|
16
|
+
await handleList();
|
|
17
|
+
break;
|
|
18
|
+
case 'get':
|
|
19
|
+
await handleGet();
|
|
20
|
+
break;
|
|
21
|
+
case 'search':
|
|
22
|
+
await handleSearch();
|
|
23
|
+
break;
|
|
24
|
+
case 'add':
|
|
25
|
+
case 'add-decision':
|
|
26
|
+
await handleAddDecision();
|
|
27
|
+
break;
|
|
28
|
+
case 'edit':
|
|
29
|
+
await handleEdit();
|
|
30
|
+
break;
|
|
31
|
+
case 'delete':
|
|
32
|
+
await handleDelete();
|
|
33
|
+
break;
|
|
34
|
+
case 'deprecate':
|
|
35
|
+
await handleDeprecate();
|
|
36
|
+
break;
|
|
37
|
+
case 'activate':
|
|
38
|
+
await handleActivate();
|
|
39
|
+
break;
|
|
40
|
+
case 'import':
|
|
41
|
+
await handleImport();
|
|
42
|
+
break;
|
|
43
|
+
case 'history':
|
|
44
|
+
await handleHistory();
|
|
45
|
+
break;
|
|
46
|
+
case 'init':
|
|
47
|
+
await handleInit();
|
|
48
|
+
break;
|
|
49
|
+
case 'setup':
|
|
50
|
+
await handleSetup();
|
|
51
|
+
break;
|
|
52
|
+
case 'embed':
|
|
53
|
+
await handleEmbed();
|
|
54
|
+
break;
|
|
55
|
+
case 'check':
|
|
56
|
+
await handleCheck();
|
|
57
|
+
break;
|
|
58
|
+
case 'clean':
|
|
59
|
+
await handleClean();
|
|
60
|
+
break;
|
|
61
|
+
case 'export':
|
|
62
|
+
await handleExport();
|
|
63
|
+
break;
|
|
64
|
+
// case 'marketplace':
|
|
65
|
+
// case 'market':
|
|
66
|
+
// await handleMarketplace();
|
|
67
|
+
// break;
|
|
68
|
+
case 'projects':
|
|
69
|
+
await handleProjects();
|
|
70
|
+
break;
|
|
71
|
+
case 'config':
|
|
72
|
+
await handleConfig();
|
|
73
|
+
break;
|
|
74
|
+
// case 'login':
|
|
75
|
+
// await handleLogin();
|
|
76
|
+
// break;
|
|
77
|
+
// case 'logout':
|
|
78
|
+
// await handleLogout();
|
|
79
|
+
// break;
|
|
80
|
+
// case 'status':
|
|
81
|
+
// await handleStatus();
|
|
82
|
+
// break;
|
|
83
|
+
// case 'sync':
|
|
84
|
+
// await handleSync();
|
|
85
|
+
// break;
|
|
86
|
+
// case 'cloud':
|
|
87
|
+
// await handleCloud();
|
|
88
|
+
// break;
|
|
89
|
+
// case 'conflicts':
|
|
90
|
+
// await handleConflicts();
|
|
91
|
+
// break;
|
|
92
|
+
// case 'fetch':
|
|
93
|
+
// await handleFetch();
|
|
94
|
+
// break;
|
|
95
|
+
// case 'pull':
|
|
96
|
+
// await handlePull();
|
|
97
|
+
// break;
|
|
98
|
+
case 'delete-scope':
|
|
99
|
+
await handleDeleteScope();
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
printUsage();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('ā Error:', error.message);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function handleList() {
|
|
112
|
+
const scopeFlag = args.indexOf('--scope');
|
|
113
|
+
const scope = scopeFlag > -1 ? args[scopeFlag + 1] : undefined;
|
|
114
|
+
const globalOnly = args.includes('--global');
|
|
115
|
+
// Get project decisions (unless --global flag is set)
|
|
116
|
+
const projectDecisions = globalOnly ? [] : await listDecisions(scope);
|
|
117
|
+
// Get global decisions (always, unless --scope is filtering a project scope)
|
|
118
|
+
const globalDecisions = globalOnly
|
|
119
|
+
? await listGlobalDecisions(scope)
|
|
120
|
+
: await listGlobalDecisions();
|
|
121
|
+
const allDecisions = [...projectDecisions, ...globalDecisions];
|
|
122
|
+
if (allDecisions.length === 0) {
|
|
123
|
+
console.log('š No decisions found.');
|
|
124
|
+
console.log('\nRun: decide add');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const label = globalOnly ? 'Global Decisions' : `Decisions${scope ? ` (${scope})` : ''}`;
|
|
128
|
+
console.log(`\nš ${label}: \n`);
|
|
129
|
+
// Show global decisions first, then project decisions
|
|
130
|
+
if (globalDecisions.length > 0) {
|
|
131
|
+
const globalGrouped = {};
|
|
132
|
+
for (const d of globalDecisions) {
|
|
133
|
+
if (!globalGrouped[d.scope])
|
|
134
|
+
globalGrouped[d.scope] = [];
|
|
135
|
+
globalGrouped[d.scope].push(d);
|
|
136
|
+
}
|
|
137
|
+
console.log('š Global');
|
|
138
|
+
for (const [scopeName, scopeDecisions] of Object.entries(globalGrouped)) {
|
|
139
|
+
console.log(` š ${scopeName} `);
|
|
140
|
+
for (const d of scopeDecisions) {
|
|
141
|
+
const statusIcon = d.status === 'active' ? 'ā
' :
|
|
142
|
+
'ā ļø';
|
|
143
|
+
console.log(` ${statusIcon} [${d.id}] ${d.decision} `);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.log('');
|
|
147
|
+
}
|
|
148
|
+
if (projectDecisions.length > 0) {
|
|
149
|
+
const grouped = {};
|
|
150
|
+
for (const d of projectDecisions) {
|
|
151
|
+
if (!grouped[d.scope])
|
|
152
|
+
grouped[d.scope] = [];
|
|
153
|
+
grouped[d.scope].push(d);
|
|
154
|
+
}
|
|
155
|
+
for (const [scopeName, scopeDecisions] of Object.entries(grouped)) {
|
|
156
|
+
console.log(`š ${scopeName} `);
|
|
157
|
+
for (const d of scopeDecisions) {
|
|
158
|
+
const statusIcon = d.status === 'active' ? 'ā
' :
|
|
159
|
+
'ā ļø';
|
|
160
|
+
console.log(` ${statusIcon} [${d.id}] ${d.decision} `);
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const parts = [];
|
|
166
|
+
if (projectDecisions.length > 0)
|
|
167
|
+
parts.push(`${projectDecisions.length} project`);
|
|
168
|
+
if (globalDecisions.length > 0)
|
|
169
|
+
parts.push(`${globalDecisions.length} global`);
|
|
170
|
+
console.log(`Total: ${parts.join(' + ')} decisions`);
|
|
171
|
+
}
|
|
172
|
+
async function handleGet() {
|
|
173
|
+
const id = args[1];
|
|
174
|
+
if (!id) {
|
|
175
|
+
console.log('Usage: decide get <decision-id>');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const decision = isGlobalId(id)
|
|
179
|
+
? await getGlobalDecisionById(id)
|
|
180
|
+
: await getDecisionById(id);
|
|
181
|
+
if (!decision) {
|
|
182
|
+
console.log(`ā Decision "${id}" not found`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
console.log('\n' + 'ā'.repeat(60));
|
|
186
|
+
console.log(`š ${decision.id} `);
|
|
187
|
+
console.log('ā'.repeat(60));
|
|
188
|
+
console.log(`\nš Decision: ${decision.decision} `);
|
|
189
|
+
if (decision.rationale) {
|
|
190
|
+
console.log(`\nš” Rationale: ${decision.rationale} `);
|
|
191
|
+
}
|
|
192
|
+
console.log(`\nš Scope: ${decision.scope} `);
|
|
193
|
+
console.log(`š Status: ${decision.status} `);
|
|
194
|
+
if (decision.constraints?.length) {
|
|
195
|
+
console.log(`ā ļø Constraints: ${decision.constraints.join(', ')} `);
|
|
196
|
+
}
|
|
197
|
+
console.log('');
|
|
198
|
+
}
|
|
199
|
+
async function handleSearch() {
|
|
200
|
+
const query = args.slice(1).join(' ');
|
|
201
|
+
if (!query) {
|
|
202
|
+
console.log('Usage: decide search "<your question>"');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const { findRelevantDecisions } = await import('./ai/rag.js');
|
|
207
|
+
const results = await findRelevantDecisions(query, 5);
|
|
208
|
+
if (results.length === 0) {
|
|
209
|
+
console.log('š No relevant decisions found.');
|
|
210
|
+
console.log(' (Have you added decisions yet?)');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.log(`\nš Results for: "${query}"\n`);
|
|
214
|
+
for (const result of results) {
|
|
215
|
+
const score = (result.score * 100).toFixed(0);
|
|
216
|
+
console.log(`[${score}%] ${result.decision.id}: ${result.decision.decision} `);
|
|
217
|
+
}
|
|
218
|
+
console.log('');
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
console.log('ā Semantic search requires a Gemini API key.');
|
|
222
|
+
console.log(' Run: decide setup');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function prompt(question, defaultValue) {
|
|
226
|
+
const rl = readline.createInterface({
|
|
227
|
+
input: process.stdin,
|
|
228
|
+
output: process.stdout
|
|
229
|
+
});
|
|
230
|
+
return new Promise(resolve => {
|
|
231
|
+
rl.question(question, (answer) => {
|
|
232
|
+
rl.close();
|
|
233
|
+
resolve(answer);
|
|
234
|
+
});
|
|
235
|
+
if (defaultValue) {
|
|
236
|
+
rl.write(defaultValue);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function getFlag(flag) {
|
|
241
|
+
const idx = args.indexOf(flag);
|
|
242
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
243
|
+
return undefined;
|
|
244
|
+
return args[idx + 1];
|
|
245
|
+
}
|
|
246
|
+
async function handleAddDecision() {
|
|
247
|
+
const isGlobal = args.includes('--global');
|
|
248
|
+
// Check for inline mode: decide add --scope UI --decision "Use Tailwind" ...
|
|
249
|
+
const inlineScope = getFlag('--scope') || getFlag('-s');
|
|
250
|
+
const inlineDecision = getFlag('--decision') || getFlag('-d');
|
|
251
|
+
let scope;
|
|
252
|
+
let decisionText;
|
|
253
|
+
let rationale;
|
|
254
|
+
let constraintsInput;
|
|
255
|
+
if (inlineScope && inlineDecision) {
|
|
256
|
+
// Inline mode ā no prompts
|
|
257
|
+
scope = inlineScope;
|
|
258
|
+
decisionText = inlineDecision;
|
|
259
|
+
rationale = getFlag('--rationale') || getFlag('-r') || '';
|
|
260
|
+
constraintsInput = getFlag('--constraints') || getFlag('-c') || '';
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Interactive mode
|
|
264
|
+
console.log(`\nā Add New ${isGlobal ? 'Global ' : ''}Decision\n`);
|
|
265
|
+
// Show existing scopes for consistency
|
|
266
|
+
const existingScopes = isGlobal ? await getGlobalScopes() : await getAvailableScopes();
|
|
267
|
+
if (existingScopes.length > 0) {
|
|
268
|
+
console.log(`Existing scopes: ${existingScopes.join(', ')} \n`);
|
|
269
|
+
}
|
|
270
|
+
const scopeExamples = existingScopes.length > 0
|
|
271
|
+
? existingScopes.slice(0, 3).join(', ')
|
|
272
|
+
: 'UI, Backend, API';
|
|
273
|
+
scope = await prompt(`Scope (e.g., ${scopeExamples} - capitalization doesn't matter): `);
|
|
274
|
+
if (!scope.trim()) {
|
|
275
|
+
console.log('ā Scope is required');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
decisionText = await prompt('Decision: ');
|
|
279
|
+
if (!decisionText.trim()) {
|
|
280
|
+
console.log('ā Decision text is required');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Check for potential conflicts with existing decisions
|
|
284
|
+
try {
|
|
285
|
+
const { findPotentialConflicts } = await import('./ai/rag.js');
|
|
286
|
+
const conflicts = await findPotentialConflicts(`${scope}: ${decisionText}`, 0.75);
|
|
287
|
+
if (conflicts.length > 0) {
|
|
288
|
+
console.log('\nā ļø Similar decisions found:\n');
|
|
289
|
+
for (const { decision, score } of conflicts) {
|
|
290
|
+
const similarity = Math.round(score * 100);
|
|
291
|
+
console.log(` ${decision.id}: ${decision.decision.substring(0, 50)}... (${similarity}% similar)`);
|
|
292
|
+
}
|
|
293
|
+
console.log('');
|
|
294
|
+
const proceed = await prompt('Continue anyway? (y/N): ');
|
|
295
|
+
if (proceed.toLowerCase() !== 'y') {
|
|
296
|
+
console.log('Cancelled.');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// Conflict check failed (API key not set) - continue anyway
|
|
303
|
+
}
|
|
304
|
+
rationale = await prompt('Rationale (optional): ');
|
|
305
|
+
constraintsInput = await prompt('Constraints (comma-separated, optional): ');
|
|
306
|
+
}
|
|
307
|
+
if (isGlobal) {
|
|
308
|
+
const rawId = await getNextGlobalDecisionId(scope.trim());
|
|
309
|
+
const newDecision = {
|
|
310
|
+
id: rawId,
|
|
311
|
+
scope: scope.trim(),
|
|
312
|
+
decision: decisionText.trim(),
|
|
313
|
+
rationale: rationale.trim() || undefined,
|
|
314
|
+
constraints: constraintsInput.trim() ? constraintsInput.split(',').map(s => s.trim()) : undefined,
|
|
315
|
+
status: 'active',
|
|
316
|
+
createdAt: new Date().toISOString()
|
|
317
|
+
};
|
|
318
|
+
const { embedded } = await addGlobalDecision(newDecision);
|
|
319
|
+
console.log(`\nā
Created global:${rawId}`);
|
|
320
|
+
console.log(` This decision applies to all projects`);
|
|
321
|
+
if (embedded) {
|
|
322
|
+
console.log(` Auto-embedded for semantic search`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.log(`\nā ļø Not embedded ā semantic search won't find this decision.`);
|
|
326
|
+
console.log(` Run: decide setup (to set your Gemini API key)`);
|
|
327
|
+
console.log(` Then: decide embed (to embed all unembedded decisions)`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const id = await getNextDecisionId(scope.trim());
|
|
332
|
+
const newDecision = {
|
|
333
|
+
id,
|
|
334
|
+
scope: scope.trim(),
|
|
335
|
+
decision: decisionText.trim(),
|
|
336
|
+
rationale: rationale.trim() || undefined,
|
|
337
|
+
constraints: constraintsInput.trim() ? constraintsInput.split(',').map(s => s.trim()) : undefined,
|
|
338
|
+
status: 'active',
|
|
339
|
+
createdAt: new Date().toISOString()
|
|
340
|
+
};
|
|
341
|
+
const { embedded } = await addDecision(newDecision);
|
|
342
|
+
console.log(`\nā
Created ${id}`);
|
|
343
|
+
if (embedded) {
|
|
344
|
+
console.log(` Auto-embedded for semantic search`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
console.log(`\nā ļø Not embedded ā semantic search won't find this decision.`);
|
|
348
|
+
console.log(` Run: decide setup (to set your Gemini API key)`);
|
|
349
|
+
console.log(` Then: decide embed (to embed all unembedded decisions)`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function handleEdit() {
|
|
354
|
+
const id = args[1];
|
|
355
|
+
if (!id) {
|
|
356
|
+
console.log('Usage: decide edit <decision-id>');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const global = isGlobalId(id);
|
|
360
|
+
const decision = global
|
|
361
|
+
? await getGlobalDecisionById(id)
|
|
362
|
+
: await getDecisionById(id);
|
|
363
|
+
if (!decision) {
|
|
364
|
+
console.log(`ā Decision ${id} not found`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (global) {
|
|
368
|
+
console.log(`\nā ļø This is a global decision that affects ALL projects.`);
|
|
369
|
+
const confirm = await prompt('Continue editing? (y/N): ');
|
|
370
|
+
if (confirm.trim().toLowerCase() !== 'y') {
|
|
371
|
+
console.log('Cancelled.');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
console.log(`\nāļø Editing ${id}`);
|
|
376
|
+
console.log('Press Enter to keep current value.\n');
|
|
377
|
+
const newDecision = await prompt('Decision: ', decision.decision);
|
|
378
|
+
const newRationale = await prompt('Rationale: ', decision.rationale || '');
|
|
379
|
+
const newConstraints = await prompt('Constraints: ', (decision.constraints || []).join(', '));
|
|
380
|
+
const updates = {};
|
|
381
|
+
if (newDecision.trim())
|
|
382
|
+
updates.decision = newDecision.trim();
|
|
383
|
+
if (newRationale.trim())
|
|
384
|
+
updates.rationale = newRationale.trim();
|
|
385
|
+
if (newConstraints.trim())
|
|
386
|
+
updates.constraints = newConstraints.split(',').map(s => s.trim());
|
|
387
|
+
if (Object.keys(updates).length === 0) {
|
|
388
|
+
console.log('\nNo changes made.');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (global) {
|
|
392
|
+
await updateGlobalDecision(id, updates);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
await updateDecision(id, updates);
|
|
396
|
+
}
|
|
397
|
+
console.log(`\nā
Updated ${id}`);
|
|
398
|
+
console.log(` Auto-embedded for semantic search`);
|
|
399
|
+
}
|
|
400
|
+
async function handleDelete() {
|
|
401
|
+
const id = args[1];
|
|
402
|
+
if (!id) {
|
|
403
|
+
console.log('Usage: decide delete <decision-id>');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const global = isGlobalId(id);
|
|
407
|
+
const decision = global
|
|
408
|
+
? await getGlobalDecisionById(id)
|
|
409
|
+
: await getDecisionById(id);
|
|
410
|
+
if (!decision) {
|
|
411
|
+
console.log(`ā Decision ${id} not found`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
console.log(`\nšļø Delete: ${id}`);
|
|
415
|
+
console.log(` "${decision.decision}"\n`);
|
|
416
|
+
if (global) {
|
|
417
|
+
console.log(`ā ļø This is a global decision that affects ALL projects.`);
|
|
418
|
+
}
|
|
419
|
+
const confirm = await prompt('Type "yes" to confirm: ');
|
|
420
|
+
if (confirm.trim().toLowerCase() !== 'yes') {
|
|
421
|
+
console.log('Cancelled.');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (global) {
|
|
425
|
+
await deleteGlobalDecision(id);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
await deleteDecision(id);
|
|
429
|
+
// Auto-clean orphaned data (reviews, etc.)
|
|
430
|
+
try {
|
|
431
|
+
const { cleanOrphanedData } = await import('./maintenance.js');
|
|
432
|
+
await cleanOrphanedData();
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// Ignore clean errors during delete flow
|
|
436
|
+
}
|
|
437
|
+
const renumber = await prompt('Renumber remaining decisions? (y/n): ');
|
|
438
|
+
if (renumber.trim().toLowerCase() === 'y') {
|
|
439
|
+
const renames = await renumberDecisions(decision.scope);
|
|
440
|
+
if (renames.length > 0) {
|
|
441
|
+
console.log('\nRenumbered:');
|
|
442
|
+
renames.forEach(r => console.log(` ${r}`));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
console.log(`\nā
Deleted ${id}`);
|
|
447
|
+
}
|
|
448
|
+
async function handleDeprecate() {
|
|
449
|
+
const id = args[1];
|
|
450
|
+
if (!id) {
|
|
451
|
+
console.log('Usage: decide deprecate <decision-id>');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const global = isGlobalId(id);
|
|
455
|
+
const decision = global
|
|
456
|
+
? await getGlobalDecisionById(id)
|
|
457
|
+
: await getDecisionById(id);
|
|
458
|
+
if (!decision) {
|
|
459
|
+
console.log(`ā Decision ${id} not found`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (decision.status === 'deprecated') {
|
|
463
|
+
console.log(`ā ļø ${id} is already deprecated.`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
console.log(`\nš ${id}: ${decision.decision}`);
|
|
467
|
+
if (global) {
|
|
468
|
+
await updateGlobalDecision(id, { status: 'deprecated' });
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
await updateDecision(id, { status: 'deprecated' });
|
|
472
|
+
}
|
|
473
|
+
console.log(`\nā
Deprecated ${id}`);
|
|
474
|
+
console.log(` This decision will no longer appear in search results.`);
|
|
475
|
+
}
|
|
476
|
+
async function handleActivate() {
|
|
477
|
+
const id = args[1];
|
|
478
|
+
if (!id) {
|
|
479
|
+
console.log('Usage: decide activate <decision-id>');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const global = isGlobalId(id);
|
|
483
|
+
const decision = global
|
|
484
|
+
? await getGlobalDecisionById(id)
|
|
485
|
+
: await getDecisionById(id);
|
|
486
|
+
if (!decision) {
|
|
487
|
+
console.log(`ā Decision ${id} not found`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (decision.status === 'active') {
|
|
491
|
+
console.log(`ā ļø ${id} is already active.`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
console.log(`\nš ${id}: ${decision.decision}`);
|
|
495
|
+
if (global) {
|
|
496
|
+
await updateGlobalDecision(id, { status: 'active' });
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
await updateDecision(id, { status: 'active' });
|
|
500
|
+
}
|
|
501
|
+
console.log(`\nā
Activated ${id}`);
|
|
502
|
+
console.log(` This decision will now appear in search results.`);
|
|
503
|
+
}
|
|
504
|
+
async function handleDeleteScope() {
|
|
505
|
+
const scopeArg = args[1];
|
|
506
|
+
if (!scopeArg) {
|
|
507
|
+
console.log('Usage: decide delete-scope <scope>');
|
|
508
|
+
console.log('\nDeletes all decisions in a scope.');
|
|
509
|
+
console.log('Example: decide delete-scope UI');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// Show what will be deleted
|
|
513
|
+
const scopes = await getAvailableScopes();
|
|
514
|
+
const normalizedInput = scopeArg.charAt(0).toUpperCase() + scopeArg.slice(1).toLowerCase();
|
|
515
|
+
if (!scopes.some(s => s.toLowerCase() === scopeArg.toLowerCase())) {
|
|
516
|
+
console.log(`ā Scope "${scopeArg}" not found.`);
|
|
517
|
+
console.log(`Available scopes: ${scopes.join(', ')}`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const decisions = await listDecisions(normalizedInput);
|
|
521
|
+
console.log(`\nā ļø This will delete the "${normalizedInput}" scope and ALL ${decisions.length} decision(s) in it:`);
|
|
522
|
+
decisions.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}...`));
|
|
523
|
+
console.log('\nā ļø This action cannot be undone!');
|
|
524
|
+
const confirm = await prompt('Type the scope name to confirm deletion: ');
|
|
525
|
+
if (confirm.toLowerCase() !== scopeArg.toLowerCase() && confirm.toLowerCase() !== normalizedInput.toLowerCase()) {
|
|
526
|
+
console.log('ā Deletion cancelled.');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const result = await deleteScope(scopeArg);
|
|
530
|
+
console.log(`\nā
Deleted scope "${normalizedInput}" (${result.deleted} decisions removed)`);
|
|
531
|
+
}
|
|
532
|
+
async function handleImport() {
|
|
533
|
+
const globalFlag = args.includes('--global');
|
|
534
|
+
const filePath = args.find(a => a !== 'import' && a !== '--global' && a !== '--overwrite' && !a.startsWith('-'));
|
|
535
|
+
if (!filePath) {
|
|
536
|
+
console.log('Usage: decide import <file.json> [--global] [--overwrite]');
|
|
537
|
+
console.log('\nExample JSON format:');
|
|
538
|
+
console.log(`[
|
|
539
|
+
{ "id": "ui-001", "scope": "UI", "decision": "...", "status": "active" },
|
|
540
|
+
{ "id": "ui-002", "scope": "UI", "decision": "...", "status": "active" }
|
|
541
|
+
]`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
console.log(`\nš„ Importing from ${filePath}${globalFlag ? ' (global)' : ''}...`);
|
|
545
|
+
try {
|
|
546
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
547
|
+
const data = JSON.parse(content);
|
|
548
|
+
// Support both array and {decisions: [...]} format
|
|
549
|
+
const decisions = Array.isArray(data) ? data : data.decisions;
|
|
550
|
+
if (!decisions || decisions.length === 0) {
|
|
551
|
+
console.log('ā No decisions found in file');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const overwriteFlag = args.includes('--overwrite');
|
|
555
|
+
if (globalFlag) {
|
|
556
|
+
// Import into global store
|
|
557
|
+
let added = 0;
|
|
558
|
+
let skipped = 0;
|
|
559
|
+
for (const decision of decisions) {
|
|
560
|
+
try {
|
|
561
|
+
await addGlobalDecision(decision);
|
|
562
|
+
added++;
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
skipped++;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
console.log(`\nā
Import complete (global)`);
|
|
569
|
+
console.log(` Added: ${added}`);
|
|
570
|
+
console.log(` Skipped: ${skipped}`);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
const result = await importDecisions(decisions, { overwrite: overwriteFlag });
|
|
574
|
+
console.log(`\nā
Import complete`);
|
|
575
|
+
console.log(` Added: ${result.added}`);
|
|
576
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
577
|
+
console.log(` Embedded: ${result.embedded}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
console.log(`ā Import failed: ${error.message}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function handleHistory() {
|
|
585
|
+
const entryId = args[1];
|
|
586
|
+
if (entryId && !entryId.startsWith('-')) {
|
|
587
|
+
// View specific snapshot
|
|
588
|
+
const entry = await getSnapshot(entryId);
|
|
589
|
+
if (!entry) {
|
|
590
|
+
console.log(`ā Entry ${entryId} not found`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
console.log(`\nš Snapshot: ${entry.id}`);
|
|
594
|
+
console.log(` Action: ${entry.action}`);
|
|
595
|
+
console.log(` Time: ${new Date(entry.timestamp).toLocaleString()}`);
|
|
596
|
+
console.log(` ${entry.description}\n`);
|
|
597
|
+
const decisions = getDecisionsFromSnapshot(entry.snapshot);
|
|
598
|
+
console.log(`Decisions at this point (${decisions.length}):\n`);
|
|
599
|
+
for (const d of decisions) {
|
|
600
|
+
console.log(`\nāāā ${d.id} āāā`);
|
|
601
|
+
console.log(` Decision: ${d.decision}`);
|
|
602
|
+
if (d.rationale)
|
|
603
|
+
console.log(` Rationale: ${d.rationale}`);
|
|
604
|
+
if (d.constraints?.length)
|
|
605
|
+
console.log(` Constraints: ${d.constraints.join(', ')}`);
|
|
606
|
+
console.log(` Status: ${d.status}`);
|
|
607
|
+
}
|
|
608
|
+
console.log('');
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// List recent history
|
|
612
|
+
const filterIndex = args.indexOf('--filter');
|
|
613
|
+
const filter = filterIndex > -1 ? args[filterIndex + 1] : undefined;
|
|
614
|
+
const history = await getHistory(50); // Get last 50 entries
|
|
615
|
+
let displayedHistory = history;
|
|
616
|
+
if (filter) {
|
|
617
|
+
const validFilters = ['cloud', 'cli', 'mcp', 'marketplace'];
|
|
618
|
+
if (!validFilters.includes(filter)) {
|
|
619
|
+
console.log(`ā Invalid filter: ${filter}`);
|
|
620
|
+
console.log(` Valid options: ${validFilters.join(', ')}`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
displayedHistory = history.filter(h => h.source === filter);
|
|
624
|
+
}
|
|
625
|
+
if (displayedHistory.length === 0) {
|
|
626
|
+
console.log(`š No history found${filter ? ` for filter "${filter}"` : ''}.`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
console.log(`\nš Activity History${filter ? ` (Filter: ${filter.toUpperCase()})` : ''}\n`);
|
|
630
|
+
console.log('ā'.repeat(60));
|
|
631
|
+
displayedHistory.forEach(entry => {
|
|
632
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
633
|
+
const icon = getActionIcon(entry.action);
|
|
634
|
+
const source = entry.source ? `[${entry.source.toUpperCase()}]` : '[CLI]';
|
|
635
|
+
console.log(`${icon} ${date} ${source.padEnd(13)} ${entry.description}`);
|
|
636
|
+
});
|
|
637
|
+
console.log('');
|
|
638
|
+
}
|
|
639
|
+
function getActionIcon(action) {
|
|
640
|
+
switch (action) {
|
|
641
|
+
case 'added': return 'ā
';
|
|
642
|
+
case 'updated': return 'āļø ';
|
|
643
|
+
case 'deleted': return 'šļø '; // Windows terminal handles this best
|
|
644
|
+
case 'imported': return 'š„';
|
|
645
|
+
case 'installed': return 'š¦';
|
|
646
|
+
case 'cloud_push': return 'ā¬ļø ';
|
|
647
|
+
case 'cloud_pull': return 'ā¬ļø ';
|
|
648
|
+
case 'conflict_resolved': return 'š¤';
|
|
649
|
+
default: return 'š¹';
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function getTimeAgo(date) {
|
|
653
|
+
const now = new Date();
|
|
654
|
+
const diff = now.getTime() - date.getTime();
|
|
655
|
+
const minutes = Math.floor(diff / 60000);
|
|
656
|
+
if (minutes < 1)
|
|
657
|
+
return 'just now';
|
|
658
|
+
if (minutes < 60)
|
|
659
|
+
return `${minutes}m ago`;
|
|
660
|
+
const hours = Math.floor(minutes / 60);
|
|
661
|
+
if (hours < 24)
|
|
662
|
+
return `${hours}h ago`;
|
|
663
|
+
const days = Math.floor(hours / 24);
|
|
664
|
+
return `${days}d ago`;
|
|
665
|
+
}
|
|
666
|
+
async function handleInit() {
|
|
667
|
+
const cwd = process.cwd();
|
|
668
|
+
const projectName = path.basename(cwd);
|
|
669
|
+
console.log('\nš Initializing DecisionNode\n');
|
|
670
|
+
console.log(` Project: ${projectName}`);
|
|
671
|
+
console.log(` Location: ${cwd}\n`);
|
|
672
|
+
// Check if already initialized by looking for existing decisions
|
|
673
|
+
const existingScopes = await getAvailableScopes();
|
|
674
|
+
if (existingScopes.length > 0) {
|
|
675
|
+
console.log(`ā
Already initialized with ${existingScopes.length} scope(s): ${existingScopes.join(', ')}`);
|
|
676
|
+
console.log('\n Run: decide list');
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
// Create the .decisions directory
|
|
680
|
+
const { getProjectRoot } = await import('./env.js');
|
|
681
|
+
const projectRoot = getProjectRoot();
|
|
682
|
+
await fs.mkdir(projectRoot, { recursive: true });
|
|
683
|
+
// Create .mcp.json for AI client integration (Claude Code, Cursor, etc.)
|
|
684
|
+
const mcpConfigPath = path.join(cwd, '.mcp.json');
|
|
685
|
+
try {
|
|
686
|
+
await fs.access(mcpConfigPath);
|
|
687
|
+
// Already exists ā don't overwrite
|
|
688
|
+
console.log(' .mcp.json already exists (skipped)');
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
const mcpConfig = {
|
|
692
|
+
mcpServers: {
|
|
693
|
+
decisionnode: {
|
|
694
|
+
command: 'decide-mcp',
|
|
695
|
+
args: []
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf-8');
|
|
700
|
+
console.log(' Created .mcp.json (connects AI clients to DecisionNode)');
|
|
701
|
+
}
|
|
702
|
+
console.log('\nā
DecisionNode initialized!\n');
|
|
703
|
+
console.log('Next steps:');
|
|
704
|
+
console.log(' 1. Configure your API key: decide setup');
|
|
705
|
+
console.log(' 2. Add your first decision: decide add\n');
|
|
706
|
+
}
|
|
707
|
+
async function handleSetup() {
|
|
708
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
709
|
+
const envPath = path.join(homeDir, '.decisionnode', '.env');
|
|
710
|
+
const envDir = path.dirname(envPath);
|
|
711
|
+
console.log('\nāļø DecisionNode Setup\n');
|
|
712
|
+
console.log('Semantic search requires a Gemini API key (free tier available).');
|
|
713
|
+
console.log('Get one at: https://aistudio.google.com/\n');
|
|
714
|
+
// Check if key already exists
|
|
715
|
+
let existingKey = process.env.GEMINI_API_KEY || '';
|
|
716
|
+
if (!existingKey) {
|
|
717
|
+
try {
|
|
718
|
+
const content = await fs.readFile(envPath, 'utf-8');
|
|
719
|
+
const match = content.match(/GEMINI_API_KEY=(.+)/);
|
|
720
|
+
if (match)
|
|
721
|
+
existingKey = match[1].trim();
|
|
722
|
+
}
|
|
723
|
+
catch { /* no existing file */ }
|
|
724
|
+
}
|
|
725
|
+
if (existingKey) {
|
|
726
|
+
const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
|
|
727
|
+
console.log(`Current key: ${masked}`);
|
|
728
|
+
console.log('');
|
|
729
|
+
}
|
|
730
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
731
|
+
const question = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
732
|
+
const key = await question(existingKey ? 'New Gemini API key (enter to keep current): ' : 'Gemini API key: ');
|
|
733
|
+
rl.close();
|
|
734
|
+
if (!key && existingKey) {
|
|
735
|
+
console.log('\nā
Keeping existing key.');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (!key) {
|
|
739
|
+
console.log('\nā ļø No key provided. You can run decide setup again later.');
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// Write the .env file
|
|
743
|
+
await fs.mkdir(envDir, { recursive: true });
|
|
744
|
+
let envContent = '';
|
|
745
|
+
try {
|
|
746
|
+
envContent = await fs.readFile(envPath, 'utf-8');
|
|
747
|
+
}
|
|
748
|
+
catch { /* file doesn't exist yet */ }
|
|
749
|
+
if (envContent.includes('GEMINI_API_KEY=')) {
|
|
750
|
+
envContent = envContent.replace(/GEMINI_API_KEY=.+/, `GEMINI_API_KEY=${key}`);
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
envContent = envContent ? envContent.trimEnd() + '\n' + `GEMINI_API_KEY=${key}\n` : `GEMINI_API_KEY=${key}\n`;
|
|
754
|
+
}
|
|
755
|
+
await fs.writeFile(envPath, envContent, 'utf-8');
|
|
756
|
+
process.env.GEMINI_API_KEY = key;
|
|
757
|
+
console.log(`\nā
API key saved to ${envPath}`);
|
|
758
|
+
console.log('\nYou can now use:');
|
|
759
|
+
console.log(' decide search "your query"');
|
|
760
|
+
console.log(' decide embed');
|
|
761
|
+
console.log('');
|
|
762
|
+
}
|
|
763
|
+
async function handleEmbed() {
|
|
764
|
+
console.log('\nā” Embedding decisions...\n');
|
|
765
|
+
try {
|
|
766
|
+
const { getUnembeddedDecisions, embedAllDecisions } = await import('./ai/rag.js');
|
|
767
|
+
const unembedded = await getUnembeddedDecisions();
|
|
768
|
+
if (unembedded.length === 0) {
|
|
769
|
+
console.log('ā
All decisions are embedded!');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
console.log(`Found ${unembedded.length} unembedded decisions:`);
|
|
773
|
+
unembedded.forEach(d => console.log(` ā ļø ${d.id}`));
|
|
774
|
+
console.log('');
|
|
775
|
+
console.log('Generating embeddings...');
|
|
776
|
+
const result = await embedAllDecisions();
|
|
777
|
+
if (result.embedded.length > 0) {
|
|
778
|
+
console.log(`\nā
Embedded: ${result.embedded.join(', ')}`);
|
|
779
|
+
}
|
|
780
|
+
if (result.failed.length > 0) {
|
|
781
|
+
console.log(`ā Failed: ${result.failed.join(', ')}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
console.log('ā Embedding requires a Gemini API key.');
|
|
786
|
+
console.log(' Run: decide setup');
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async function handleCheck() {
|
|
791
|
+
console.log('\nš Decision Health Check\n');
|
|
792
|
+
const { loadVectorCache, loadGlobalVectorCache } = await import('./ai/rag.js');
|
|
793
|
+
// Project decisions
|
|
794
|
+
const projectDecisions = await listDecisions();
|
|
795
|
+
const projectCache = await loadVectorCache();
|
|
796
|
+
const projectMissing = projectDecisions.filter(d => !projectCache[d.id]);
|
|
797
|
+
console.log(`š¦ Project: ${projectDecisions.length} decisions`);
|
|
798
|
+
console.log(` ā
Embedded: ${projectDecisions.length - projectMissing.length}`);
|
|
799
|
+
if (projectMissing.length > 0) {
|
|
800
|
+
console.log(` ā ļø Missing vectors: ${projectMissing.length}`);
|
|
801
|
+
projectMissing.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}`));
|
|
802
|
+
}
|
|
803
|
+
// Global decisions
|
|
804
|
+
const globalDecs = await listGlobalDecisions();
|
|
805
|
+
let globalMissingCount = 0;
|
|
806
|
+
if (globalDecs.length > 0) {
|
|
807
|
+
const globalCache = await loadGlobalVectorCache();
|
|
808
|
+
const globalMissing = globalDecs.filter(d => {
|
|
809
|
+
const rawId = d.id.replace(/^global:/, '');
|
|
810
|
+
return !globalCache[rawId];
|
|
811
|
+
});
|
|
812
|
+
globalMissingCount = globalMissing.length;
|
|
813
|
+
console.log(`\nš Global: ${globalDecs.length} decisions`);
|
|
814
|
+
console.log(` ā
Embedded: ${globalDecs.length - globalMissing.length}`);
|
|
815
|
+
if (globalMissing.length > 0) {
|
|
816
|
+
console.log(` ā ļø Missing vectors: ${globalMissing.length}`);
|
|
817
|
+
globalMissing.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}`));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const totalMissing = projectMissing.length + globalMissingCount;
|
|
821
|
+
if (totalMissing > 0) {
|
|
822
|
+
console.log(`\n${totalMissing} decision(s) not searchable. Run: decide embed`);
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
console.log(`\nā
All decisions are embedded and searchable!`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function handleClean() {
|
|
829
|
+
console.log('\nš§¹ Cleaning orphaned data...\n');
|
|
830
|
+
try {
|
|
831
|
+
const { cleanOrphanedData } = await import('./maintenance.js');
|
|
832
|
+
const result = await cleanOrphanedData();
|
|
833
|
+
if (result.vectorsRemoved === 0 && result.reviewsRemoved === 0) {
|
|
834
|
+
console.log('ā
Nothing to clean. Your data is tidy!');
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
if (result.vectorsRemoved > 0) {
|
|
838
|
+
console.log(`ā
Removed ${result.vectorsRemoved} orphaned vectors.`);
|
|
839
|
+
}
|
|
840
|
+
if (result.reviewsRemoved > 0) {
|
|
841
|
+
console.log(`ā
Removed ${result.reviewsRemoved} orphaned reviews.`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
console.error('ā Error cleaning data:', error.message);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function handleExport() {
|
|
851
|
+
const globalOnly = args.includes('--global');
|
|
852
|
+
const formatArg = args.find(a => a !== '--global' && a !== 'export');
|
|
853
|
+
const format = formatArg?.toLowerCase() || 'md';
|
|
854
|
+
const decisions = globalOnly ? await listGlobalDecisions() : await listDecisions();
|
|
855
|
+
if (decisions.length === 0) {
|
|
856
|
+
console.error('No decisions to export.');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
let output;
|
|
860
|
+
switch (format) {
|
|
861
|
+
case 'json':
|
|
862
|
+
output = JSON.stringify(decisions, null, 2);
|
|
863
|
+
break;
|
|
864
|
+
case 'csv':
|
|
865
|
+
output = exportToCSV(decisions);
|
|
866
|
+
break;
|
|
867
|
+
case 'md':
|
|
868
|
+
case 'markdown':
|
|
869
|
+
default:
|
|
870
|
+
output = exportToMarkdown(decisions);
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
console.log(output);
|
|
874
|
+
}
|
|
875
|
+
function exportToMarkdown(decisions) {
|
|
876
|
+
const grouped = new Map();
|
|
877
|
+
for (const d of decisions) {
|
|
878
|
+
const scope = d.scope.charAt(0).toUpperCase() + d.scope.slice(1);
|
|
879
|
+
if (!grouped.has(scope))
|
|
880
|
+
grouped.set(scope, []);
|
|
881
|
+
grouped.get(scope).push(d);
|
|
882
|
+
}
|
|
883
|
+
let md = '# Project Decisions\n\n';
|
|
884
|
+
md += `> Generated by DecisionNode on ${new Date().toLocaleDateString()}\n\n`;
|
|
885
|
+
for (const [scope, items] of grouped) {
|
|
886
|
+
md += `## ${scope}\n\n`;
|
|
887
|
+
for (const d of items) {
|
|
888
|
+
md += `### ${d.id}\n\n`;
|
|
889
|
+
md += `**Decision:** ${d.decision}\n\n`;
|
|
890
|
+
if (d.rationale)
|
|
891
|
+
md += `**Rationale:** ${d.rationale}\n\n`;
|
|
892
|
+
if (d.constraints?.length) {
|
|
893
|
+
md += '**Constraints:**\n';
|
|
894
|
+
d.constraints.forEach(c => md += `- ${c}\n`);
|
|
895
|
+
md += '\n';
|
|
896
|
+
}
|
|
897
|
+
md += `---\n\n`;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return md;
|
|
901
|
+
}
|
|
902
|
+
function exportToCSV(decisions) {
|
|
903
|
+
const headers = ['id', 'scope', 'decision', 'rationale', 'constraints', 'status', 'createdAt'];
|
|
904
|
+
const rows = decisions.map(d => [
|
|
905
|
+
d.id,
|
|
906
|
+
d.scope,
|
|
907
|
+
`"${d.decision.replace(/"/g, '""')}"`,
|
|
908
|
+
d.rationale ? `"${d.rationale.replace(/"/g, '""')}"` : '',
|
|
909
|
+
(d.constraints || []).join('; '),
|
|
910
|
+
d.status,
|
|
911
|
+
d.createdAt
|
|
912
|
+
]);
|
|
913
|
+
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
|
914
|
+
}
|
|
915
|
+
async function handleMarketplace() {
|
|
916
|
+
const subCommand = args[1];
|
|
917
|
+
const { getMarketplaceIndex, searchMarketplace, installPack } = await import('./marketplace.js');
|
|
918
|
+
switch (subCommand) {
|
|
919
|
+
case 'browse':
|
|
920
|
+
case undefined: {
|
|
921
|
+
console.log('\nš Decision Pack Marketplace\n');
|
|
922
|
+
const packs = await getMarketplaceIndex();
|
|
923
|
+
for (const pack of packs) {
|
|
924
|
+
console.log(`š¦ ${pack.name} (${pack.id})`);
|
|
925
|
+
console.log(` ${pack.description}`);
|
|
926
|
+
console.log(` Scope: ${pack.scope} | ${pack.decisionCount} decisions | ⬠${pack.downloads}`);
|
|
927
|
+
console.log('');
|
|
928
|
+
}
|
|
929
|
+
console.log('Install: decide marketplace install <pack-id>');
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
case 'search': {
|
|
933
|
+
const query = args[2];
|
|
934
|
+
if (!query) {
|
|
935
|
+
console.log('Usage: decide marketplace search <query>');
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
console.log(`\nš Searching for "${query}"...\n`);
|
|
939
|
+
const results = await searchMarketplace(query);
|
|
940
|
+
if (results.length === 0) {
|
|
941
|
+
console.log('No packs found.');
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
for (const pack of results) {
|
|
945
|
+
console.log(`š¦ ${pack.name} (${pack.id})`);
|
|
946
|
+
console.log(` ${pack.description}`);
|
|
947
|
+
console.log('');
|
|
948
|
+
}
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
case 'install': {
|
|
952
|
+
const packId = args[2];
|
|
953
|
+
if (!packId) {
|
|
954
|
+
console.log('Usage: decide marketplace install <pack-id>');
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
console.log(`\nš„ Installing ${packId}...\n`);
|
|
958
|
+
try {
|
|
959
|
+
const result = await installPack(packId);
|
|
960
|
+
console.log(`ā
Installed ${result.installed} decisions`);
|
|
961
|
+
if (result.skipped > 0) {
|
|
962
|
+
console.log(`ā ļø Skipped ${result.skipped} duplicates`);
|
|
963
|
+
}
|
|
964
|
+
console.log('\nPre-embedded vectors included - no sync needed!');
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
console.log(`ā ${error.message}`);
|
|
968
|
+
}
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
default:
|
|
972
|
+
console.log('Usage: decide marketplace [browse|search|install]');
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function handleProjects() {
|
|
976
|
+
console.log('\nš Available Projects\n');
|
|
977
|
+
// Show global decisions first
|
|
978
|
+
const globalDecisions = await listGlobalDecisions();
|
|
979
|
+
if (globalDecisions.length > 0) {
|
|
980
|
+
const globalScopes = await getGlobalScopes();
|
|
981
|
+
console.log(`š Global (shared across all projects)`);
|
|
982
|
+
console.log(` ${globalDecisions.length} decisions [${globalScopes.join(', ')}]`);
|
|
983
|
+
console.log('');
|
|
984
|
+
}
|
|
985
|
+
const projects = await listProjects();
|
|
986
|
+
if (projects.length === 0 && globalDecisions.length === 0) {
|
|
987
|
+
console.log('No projects found.');
|
|
988
|
+
console.log('\nCreate decisions with: decide add');
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
for (const project of projects) {
|
|
992
|
+
const scopeStr = project.scopes.length > 0 ? `[${project.scopes.join(', ')}]` : '';
|
|
993
|
+
console.log(`š¦ ${project.name}`);
|
|
994
|
+
console.log(` ${project.decisionCount} decisions ${scopeStr}`);
|
|
995
|
+
console.log('');
|
|
996
|
+
}
|
|
997
|
+
console.log(`Total: ${projects.length} projects${globalDecisions.length > 0 ? ` + global (${globalDecisions.length} decisions)` : ''}`);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Handle login command - authenticate with DecisionNode
|
|
1001
|
+
*/
|
|
1002
|
+
async function handleLogin() {
|
|
1003
|
+
const status = await getCloudStatus();
|
|
1004
|
+
if (status.authenticated) {
|
|
1005
|
+
console.log(`\nā
Already logged in as ${status.username || status.email || 'Unknown'}`);
|
|
1006
|
+
console.log(` Subscription: ${status.isPro ? 'ā Pro' : 'Free'}`);
|
|
1007
|
+
console.log('\n Run "decide logout" to sign out first.\n');
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
await loginToCloud();
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Handle logout command - sign out from DecisionNode
|
|
1014
|
+
*/
|
|
1015
|
+
async function handleLogout() {
|
|
1016
|
+
await logoutFromCloud();
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Handle status command - show account and subscription status
|
|
1020
|
+
*/
|
|
1021
|
+
async function handleStatus() {
|
|
1022
|
+
const status = await getCloudStatus();
|
|
1023
|
+
console.log('\nš DecisionNode Status\n');
|
|
1024
|
+
console.log('ā'.repeat(40));
|
|
1025
|
+
if (!status.authenticated) {
|
|
1026
|
+
console.log('\n ā Not logged in');
|
|
1027
|
+
console.log('\n Run "decide login" to authenticate.\n');
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
console.log(`\n š¤ Account: ${status.username || status.email || 'Unknown'}`);
|
|
1031
|
+
console.log(` š User ID: ${status.userId || 'Unknown'}`);
|
|
1032
|
+
if (status.isPro) {
|
|
1033
|
+
console.log(` ā Subscription: Pro`);
|
|
1034
|
+
if (status.expiresAt) {
|
|
1035
|
+
const expiresDate = new Date(status.expiresAt);
|
|
1036
|
+
const daysLeft = Math.ceil((expiresDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
1037
|
+
console.log(` š
Expires: ${expiresDate.toLocaleDateString()} (${daysLeft} days)`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
console.log(` š¦ Subscription: Free`);
|
|
1042
|
+
console.log(`\n š” Upgrade to Pro for cloud sync and embedding:`);
|
|
1043
|
+
console.log(` https://decisionnode.dev/pricing`);
|
|
1044
|
+
}
|
|
1045
|
+
if (status.lastSync) {
|
|
1046
|
+
console.log(`\n āļø Last sync: ${new Date(status.lastSync).toLocaleString()}`);
|
|
1047
|
+
}
|
|
1048
|
+
console.log('');
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Handle sync command - intelligent two-way sync (Pro only)
|
|
1052
|
+
*/
|
|
1053
|
+
async function handleSync() {
|
|
1054
|
+
const status = await getCloudStatus();
|
|
1055
|
+
if (!status.authenticated) {
|
|
1056
|
+
console.log('\nā Not logged in. Run "decide login" first.\n');
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (!status.isPro) {
|
|
1060
|
+
console.log('\nā Cloud sync requires a Pro subscription.');
|
|
1061
|
+
console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const pushOnly = args.includes('--push-only');
|
|
1065
|
+
const pullOnly = args.includes('--pull-only');
|
|
1066
|
+
const cwd = process.cwd();
|
|
1067
|
+
const projectName = path.basename(cwd);
|
|
1068
|
+
const projectRoot = getProjectRoot();
|
|
1069
|
+
console.log(`\nāļø Syncing decisions...`);
|
|
1070
|
+
console.log(` Project: ${projectName}\n`);
|
|
1071
|
+
// Get local and cloud decisions
|
|
1072
|
+
const localDecisions = await listDecisions();
|
|
1073
|
+
const cloudDecisions = await pullDecisionsFromCloud(projectName);
|
|
1074
|
+
if (!cloudDecisions && !pushOnly) {
|
|
1075
|
+
console.log('ā Failed to fetch cloud decisions.\n');
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
// Detect what needs to be done
|
|
1079
|
+
const { toPush, toPull, conflicts } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions || []);
|
|
1080
|
+
// Report conflicts
|
|
1081
|
+
if (conflicts.length > 0) {
|
|
1082
|
+
console.log(`ā ļø ${conflicts.length} conflict(s) detected!`);
|
|
1083
|
+
console.log(' Run "decide conflicts" to resolve them.\n');
|
|
1084
|
+
}
|
|
1085
|
+
// Push local changes
|
|
1086
|
+
if (!pullOnly && toPush.length > 0) {
|
|
1087
|
+
console.log(`ā¬ļø Pushing ${toPush.length} decision(s)...`);
|
|
1088
|
+
// Attach embeddings if available
|
|
1089
|
+
const { loadVectorCache } = await import('./ai/rag.js');
|
|
1090
|
+
const vectorCache = await loadVectorCache();
|
|
1091
|
+
const decisionsToSync = localDecisions
|
|
1092
|
+
.filter(d => toPush.includes(d.id))
|
|
1093
|
+
.map(d => {
|
|
1094
|
+
const entry = vectorCache[d.id];
|
|
1095
|
+
const vector = entry ? (Array.isArray(entry) ? entry : entry.vector) : undefined;
|
|
1096
|
+
return {
|
|
1097
|
+
...d,
|
|
1098
|
+
embedding: vector
|
|
1099
|
+
};
|
|
1100
|
+
});
|
|
1101
|
+
const result = await syncDecisionsToCloud(projectName, decisionsToSync);
|
|
1102
|
+
if (result) {
|
|
1103
|
+
console.log(` ā
Pushed: ${result.synced.length}`);
|
|
1104
|
+
console.log(` š Embedded: ${result.embedded}`);
|
|
1105
|
+
if (result.failed.length > 0) {
|
|
1106
|
+
console.log(` ā Failed: ${result.failed.join(', ')}`);
|
|
1107
|
+
}
|
|
1108
|
+
// Update sync metadata
|
|
1109
|
+
await updateSyncMetadata(projectRoot, result.synced, cloudDecisions || []);
|
|
1110
|
+
// Log push to history
|
|
1111
|
+
if (result.synced.length > 0) {
|
|
1112
|
+
await logBatchAction('cloud_push', result.synced, 'cloud');
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
else if (!pullOnly) {
|
|
1117
|
+
console.log('ā¬ļø No local changes to push.');
|
|
1118
|
+
}
|
|
1119
|
+
// Pull cloud changes
|
|
1120
|
+
if (!pushOnly && toPull.length > 0) {
|
|
1121
|
+
console.log(`\nā¬ļø Pulling ${toPull.length} decision(s) from cloud...`);
|
|
1122
|
+
// Group by scope and save
|
|
1123
|
+
const updatesByScope = {};
|
|
1124
|
+
for (const cloud of toPull) {
|
|
1125
|
+
// Skip metadata scope
|
|
1126
|
+
if (cloud.scope === 'Sync-metadata')
|
|
1127
|
+
continue;
|
|
1128
|
+
const node = {
|
|
1129
|
+
id: cloud.decision_id,
|
|
1130
|
+
scope: cloud.scope,
|
|
1131
|
+
decision: cloud.decision,
|
|
1132
|
+
rationale: cloud.rationale || undefined,
|
|
1133
|
+
constraints: cloud.constraints || undefined,
|
|
1134
|
+
status: (cloud.status === 'active' || cloud.status === 'deprecated') ? cloud.status : 'active',
|
|
1135
|
+
tags: cloud.tags || undefined,
|
|
1136
|
+
createdAt: cloud.created_at || new Date().toISOString(),
|
|
1137
|
+
updatedAt: cloud.synced_at || undefined,
|
|
1138
|
+
};
|
|
1139
|
+
if (!updatesByScope[cloud.scope])
|
|
1140
|
+
updatesByScope[cloud.scope] = [];
|
|
1141
|
+
updatesByScope[cloud.scope].push(node);
|
|
1142
|
+
}
|
|
1143
|
+
// Safe Merge Logic
|
|
1144
|
+
for (const [scope, newNodes] of Object.entries(updatesByScope)) {
|
|
1145
|
+
// Get CURRENT local items for this scope
|
|
1146
|
+
const currentScopeItems = localDecisions.filter(d => d.scope === scope);
|
|
1147
|
+
// Merge: Start with existing, map updates over them
|
|
1148
|
+
const mergedItems = [...currentScopeItems];
|
|
1149
|
+
for (const newNode of newNodes) {
|
|
1150
|
+
const index = mergedItems.findIndex(m => m.id === newNode.id);
|
|
1151
|
+
if (index >= 0) {
|
|
1152
|
+
// Update existing
|
|
1153
|
+
mergedItems[index] = newNode;
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
// Add new
|
|
1157
|
+
mergedItems.push(newNode);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
// Save matched scope
|
|
1161
|
+
await saveDecisions({ scope, decisions: mergedItems });
|
|
1162
|
+
// Save vectors from cloud directly - NO local generation fallback
|
|
1163
|
+
const { loadVectorCache, saveVectorCache } = await import('./ai/rag.js');
|
|
1164
|
+
const vectorCache = await loadVectorCache();
|
|
1165
|
+
let vectorsUpdated = false;
|
|
1166
|
+
for (const newNode of newNodes) {
|
|
1167
|
+
const cloudMatch = toPull.find(c => c.decision_id === newNode.id || c.id === newNode.id);
|
|
1168
|
+
if (cloudMatch && cloudMatch.embedding) {
|
|
1169
|
+
// Option A: Use Cloud Vector
|
|
1170
|
+
let vector = null;
|
|
1171
|
+
if (typeof cloudMatch.embedding === 'string') {
|
|
1172
|
+
try {
|
|
1173
|
+
vector = JSON.parse(cloudMatch.embedding);
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
vector = null;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
else if (Array.isArray(cloudMatch.embedding)) {
|
|
1180
|
+
vector = cloudMatch.embedding;
|
|
1181
|
+
}
|
|
1182
|
+
if (vector && Array.isArray(vector)) {
|
|
1183
|
+
vectorCache[newNode.id] = {
|
|
1184
|
+
vector: vector,
|
|
1185
|
+
embeddedAt: new Date().toISOString()
|
|
1186
|
+
};
|
|
1187
|
+
vectorsUpdated = true;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (vectorsUpdated) {
|
|
1192
|
+
await saveVectorCache(vectorCache);
|
|
1193
|
+
}
|
|
1194
|
+
console.log(` ā
Pulled: ${scope} (${newNodes.length} updates)`);
|
|
1195
|
+
}
|
|
1196
|
+
// Update sync metadata for pulled decisions
|
|
1197
|
+
const pulledIds = toPull.map(d => d.decision_id);
|
|
1198
|
+
await updateSyncMetadata(projectRoot, pulledIds, toPull);
|
|
1199
|
+
// Clear pulled items from incoming changes
|
|
1200
|
+
await removeIncomingChanges(projectRoot, pulledIds);
|
|
1201
|
+
// Log pull to history
|
|
1202
|
+
if (pulledIds.length > 0) {
|
|
1203
|
+
await logBatchAction('cloud_pull', pulledIds, 'cloud');
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
else if (!pushOnly) {
|
|
1207
|
+
console.log('\nā¬ļø No cloud-only changes to pull.');
|
|
1208
|
+
}
|
|
1209
|
+
// Self-Heal: If no changes were made but decisions exist on both sides,
|
|
1210
|
+
// ensure metadata is up to date (fixes missing sync-metadata.json case).
|
|
1211
|
+
if (toPush.length === 0 && toPull.length === 0 && conflicts.length === 0) {
|
|
1212
|
+
if (localDecisions.length > 0 && cloudDecisions && cloudDecisions.length > 0) {
|
|
1213
|
+
// Find intersection
|
|
1214
|
+
const cloudIds = new Set(cloudDecisions.map(d => d.decision_id));
|
|
1215
|
+
const inSyncIds = localDecisions
|
|
1216
|
+
.filter(d => cloudIds.has(d.id))
|
|
1217
|
+
.map(d => d.id);
|
|
1218
|
+
if (inSyncIds.length > 0) {
|
|
1219
|
+
// Determine if metadata file exists or needs update
|
|
1220
|
+
// For simplicity, we just update it. It's cheap.
|
|
1221
|
+
await updateSyncMetadata(projectRoot, inSyncIds, cloudDecisions);
|
|
1222
|
+
// console.log(' š Verified sync metadata.'); // Optional logging
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
console.log('\nš Sync complete!\n');
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Handle cloud command - show cloud sync status
|
|
1230
|
+
*/
|
|
1231
|
+
async function handleCloud() {
|
|
1232
|
+
const subCommand = args[1];
|
|
1233
|
+
if (subCommand === 'status') {
|
|
1234
|
+
await handleCloudSyncStatus();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
// Default: show help
|
|
1238
|
+
console.log(`
|
|
1239
|
+
āļø DecisionNode Cloud
|
|
1240
|
+
|
|
1241
|
+
Commands:
|
|
1242
|
+
decide cloud status Show sync status for current project
|
|
1243
|
+
|
|
1244
|
+
Related commands:
|
|
1245
|
+
decide login Log in to your DecisionNode account
|
|
1246
|
+
decide logout Log out
|
|
1247
|
+
decide status Show account status
|
|
1248
|
+
decide sync Sync decisions to cloud (Pro only)
|
|
1249
|
+
`);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Handle cloud status - show which decisions are synced
|
|
1253
|
+
*/
|
|
1254
|
+
async function handleCloudSyncStatus() {
|
|
1255
|
+
const status = await getCloudStatus();
|
|
1256
|
+
if (!status.authenticated) {
|
|
1257
|
+
console.log('\nā Not logged in. Run "decide login" first.\n');
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (!status.isPro) {
|
|
1261
|
+
console.log('\nā Cloud sync requires a Pro subscription.');
|
|
1262
|
+
console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const cwd = process.cwd();
|
|
1266
|
+
const projectName = path.basename(cwd);
|
|
1267
|
+
console.log(`\nāļø Cloud Sync Status: ${projectName}\n`);
|
|
1268
|
+
console.log('ā'.repeat(40));
|
|
1269
|
+
// Get local decisions
|
|
1270
|
+
const localDecisions = await listDecisions();
|
|
1271
|
+
// Get synced decisions from cloud
|
|
1272
|
+
const cloudStatus = await getCloudSyncStatus(projectName);
|
|
1273
|
+
if (!cloudStatus) {
|
|
1274
|
+
console.log('\n Unable to fetch cloud status.\n');
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const syncedIds = new Set(cloudStatus.synced);
|
|
1278
|
+
const notSynced = [];
|
|
1279
|
+
for (const decision of localDecisions) {
|
|
1280
|
+
if (!syncedIds.has(decision.id)) {
|
|
1281
|
+
notSynced.push(decision.id);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
console.log(`\n š Local decisions: ${localDecisions.length}`);
|
|
1285
|
+
console.log(` āļø In cloud: ${cloudStatus.total_in_cloud}`);
|
|
1286
|
+
if (notSynced.length === 0) {
|
|
1287
|
+
console.log(`\n ā
All decisions are synced!\n`);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
console.log(`\n ā ļø Not synced (${notSynced.length}):`);
|
|
1291
|
+
for (const id of notSynced.slice(0, 10)) {
|
|
1292
|
+
console.log(` - ${id}`);
|
|
1293
|
+
}
|
|
1294
|
+
if (notSynced.length > 10) {
|
|
1295
|
+
console.log(` ... and ${notSynced.length - 10} more`);
|
|
1296
|
+
}
|
|
1297
|
+
console.log(`\n Run "decide sync" to sync all decisions.\n`);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Handle pull command - pull decisions from cloud
|
|
1302
|
+
*/
|
|
1303
|
+
async function handlePull() {
|
|
1304
|
+
console.log('\nāļø Pulling decisions from cloud...\n');
|
|
1305
|
+
const cwd = process.cwd();
|
|
1306
|
+
const projectName = path.basename(cwd);
|
|
1307
|
+
const projectRoot = getProjectRoot();
|
|
1308
|
+
// 1. Get local decisions
|
|
1309
|
+
const localDecisions = await listDecisions();
|
|
1310
|
+
// 2. Get cloud decisions
|
|
1311
|
+
const cloudDecisions = await pullDecisionsFromCloud(projectName);
|
|
1312
|
+
if (!cloudDecisions) {
|
|
1313
|
+
console.log('ā Failed to pull decisions.');
|
|
1314
|
+
console.log(' Check if you are logged in (decide login) and have a Pro subscription.');
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (cloudDecisions.length === 0) {
|
|
1318
|
+
console.log('ā
No decisions found in cloud for this project.');
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
console.log(`Cloud has ${cloudDecisions.length} decisions.`);
|
|
1322
|
+
console.log('š Checking for conflicts...');
|
|
1323
|
+
// 3. Detect conflicts
|
|
1324
|
+
const { conflicts, toPull } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions);
|
|
1325
|
+
// 4. Handle Conflicts
|
|
1326
|
+
if (conflicts.length > 0) {
|
|
1327
|
+
console.log(`\nā Aborting pull: ${conflicts.length} conflict(s) detected.`);
|
|
1328
|
+
console.log(' These decisions have changed both locally and in the cloud:');
|
|
1329
|
+
conflicts.slice(0, 3).forEach(c => console.log(` - ${c.decisionId} (${c.scope})`));
|
|
1330
|
+
if (conflicts.length > 3)
|
|
1331
|
+
console.log(` ...and ${conflicts.length - 3} more.`);
|
|
1332
|
+
console.log('\nš Run "decide conflicts" to examine and resolve them safely.');
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
// 5. Check if anything to pull
|
|
1336
|
+
if (toPull.length === 0) {
|
|
1337
|
+
console.log('\nā
Local decisions are already up to date with cloud.');
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
console.log(`\nā¬ļø Pulling ${toPull.length} new/updated decision(s) from cloud...`);
|
|
1341
|
+
// 6. Merge & Save (Safe Update)
|
|
1342
|
+
const updatesByScope = {};
|
|
1343
|
+
// Group toPull items by scope
|
|
1344
|
+
for (const d of toPull) {
|
|
1345
|
+
// Skip metadata scope if it accidentally got synced
|
|
1346
|
+
if (d.scope === 'Sync-metadata')
|
|
1347
|
+
continue;
|
|
1348
|
+
const node = {
|
|
1349
|
+
id: d.decision_id || d.id,
|
|
1350
|
+
scope: d.scope,
|
|
1351
|
+
decision: d.decision,
|
|
1352
|
+
rationale: d.rationale || undefined,
|
|
1353
|
+
constraints: d.constraints || undefined,
|
|
1354
|
+
status: (d.status === 'active' || d.status === 'deprecated') ? d.status : 'active',
|
|
1355
|
+
tags: d.tags || undefined,
|
|
1356
|
+
createdAt: d.created_at || new Date().toISOString(),
|
|
1357
|
+
updatedAt: d.synced_at || undefined,
|
|
1358
|
+
};
|
|
1359
|
+
if (!updatesByScope[node.scope]) {
|
|
1360
|
+
updatesByScope[node.scope] = [];
|
|
1361
|
+
}
|
|
1362
|
+
updatesByScope[node.scope].push(node);
|
|
1363
|
+
}
|
|
1364
|
+
let savedCount = 0;
|
|
1365
|
+
// Apply updates scope by scope
|
|
1366
|
+
for (const [scope, newNodes] of Object.entries(updatesByScope)) {
|
|
1367
|
+
// Get CURRENT local items for this scope
|
|
1368
|
+
const currentScopeItems = localDecisions.filter(d => d.scope === scope);
|
|
1369
|
+
// Merge: Start with existing, map updates over them
|
|
1370
|
+
const mergedItems = [...currentScopeItems];
|
|
1371
|
+
for (const newNode of newNodes) {
|
|
1372
|
+
const index = mergedItems.findIndex(m => m.id === newNode.id);
|
|
1373
|
+
if (index >= 0) {
|
|
1374
|
+
// Update existing
|
|
1375
|
+
mergedItems[index] = newNode;
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
// Add new
|
|
1379
|
+
mergedItems.push(newNode);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// Save matched scope
|
|
1383
|
+
await saveDecisions({
|
|
1384
|
+
scope: scope,
|
|
1385
|
+
decisions: mergedItems
|
|
1386
|
+
});
|
|
1387
|
+
// Save vectors from cloud directly, OR fallback to local embedding
|
|
1388
|
+
const { loadVectorCache, saveVectorCache, embedDecisions } = await import('./ai/rag.js');
|
|
1389
|
+
const vectorCache = await loadVectorCache();
|
|
1390
|
+
let vectorsUpdated = false;
|
|
1391
|
+
const nodesToAutoEmbed = [];
|
|
1392
|
+
for (const newNode of newNodes) {
|
|
1393
|
+
const cloudMatch = toPull.find(c => c.decision_id === newNode.id || c.id === newNode.id);
|
|
1394
|
+
if (cloudMatch && cloudMatch.embedding) {
|
|
1395
|
+
// Option A: Use Cloud Vector
|
|
1396
|
+
let vector = null;
|
|
1397
|
+
if (typeof cloudMatch.embedding === 'string') {
|
|
1398
|
+
try {
|
|
1399
|
+
vector = JSON.parse(cloudMatch.embedding);
|
|
1400
|
+
}
|
|
1401
|
+
catch {
|
|
1402
|
+
vector = null;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
else if (Array.isArray(cloudMatch.embedding)) {
|
|
1406
|
+
vector = cloudMatch.embedding;
|
|
1407
|
+
}
|
|
1408
|
+
if (vector && Array.isArray(vector)) {
|
|
1409
|
+
vectorCache[newNode.id] = {
|
|
1410
|
+
vector: vector,
|
|
1411
|
+
embeddedAt: new Date().toISOString()
|
|
1412
|
+
};
|
|
1413
|
+
vectorsUpdated = true;
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
// Invalid vector format - fallback
|
|
1417
|
+
nodesToAutoEmbed.push(newNode);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
// Option B: Cloud has no vector? Fallback to local gen
|
|
1422
|
+
nodesToAutoEmbed.push(newNode);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (vectorsUpdated) {
|
|
1426
|
+
await saveVectorCache(vectorCache);
|
|
1427
|
+
}
|
|
1428
|
+
// Fallback: Embed any nodes that didn't have cloud vectors
|
|
1429
|
+
if (nodesToAutoEmbed.length > 0) {
|
|
1430
|
+
console.log(` ā ļø ${nodesToAutoEmbed.length} decisions missing cloud vectors. Generating locally...`);
|
|
1431
|
+
await embedDecisions(nodesToAutoEmbed);
|
|
1432
|
+
}
|
|
1433
|
+
savedCount += newNodes.length;
|
|
1434
|
+
console.log(` ā
Updated ${scope} (+${newNodes.length} changes)`);
|
|
1435
|
+
}
|
|
1436
|
+
// Also handle scopes that are NEW (no local file yet)
|
|
1437
|
+
// The previous loop only handles updates where 'updatesByScope' has keys.
|
|
1438
|
+
// 7. Update Sync Metadata
|
|
1439
|
+
// This ensures VS Code and CLI know these files are now in sync
|
|
1440
|
+
await updateSyncMetadata(projectRoot, toPull.map(d => d.decision_id), cloudDecisions);
|
|
1441
|
+
// 8. Clear pulled items from incoming changes (fetch state)
|
|
1442
|
+
const { removeIncomingChanges } = await import('./cloud.js');
|
|
1443
|
+
await removeIncomingChanges(projectRoot, toPull.map(d => d.decision_id));
|
|
1444
|
+
console.log(`\nš Pull complete. Updated ${savedCount} decisions.`);
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Handle conflicts command - list and resolve sync conflicts
|
|
1448
|
+
* Usage: decide conflicts [resolve <id> <local|cloud>]
|
|
1449
|
+
*/
|
|
1450
|
+
async function handleConflicts() {
|
|
1451
|
+
const subCommand = args[1];
|
|
1452
|
+
const projectRoot = getProjectRoot();
|
|
1453
|
+
// Handle non-interactive 'resolve' subcommand (for VS Code integration)
|
|
1454
|
+
if (subCommand === 'resolve') {
|
|
1455
|
+
const decisionId = args[2];
|
|
1456
|
+
const resolution = args[3];
|
|
1457
|
+
if (!decisionId || !resolution) {
|
|
1458
|
+
console.error('ā Usage: decide conflicts resolve <decision-id> <local|cloud>');
|
|
1459
|
+
process.exit(1);
|
|
1460
|
+
}
|
|
1461
|
+
if (resolution !== 'local' && resolution !== 'cloud') {
|
|
1462
|
+
console.error('ā Resolution must be "local" or "cloud"');
|
|
1463
|
+
process.exit(1);
|
|
1464
|
+
}
|
|
1465
|
+
// Load incoming.json to get conflicts
|
|
1466
|
+
const incomingPath = path.join(projectRoot, 'incoming.json');
|
|
1467
|
+
let data = { conflicts: [] };
|
|
1468
|
+
try {
|
|
1469
|
+
const content = await fs.readFile(incomingPath, 'utf-8');
|
|
1470
|
+
data = JSON.parse(content);
|
|
1471
|
+
}
|
|
1472
|
+
catch {
|
|
1473
|
+
// No incoming file
|
|
1474
|
+
}
|
|
1475
|
+
const conflicts = data.conflicts || [];
|
|
1476
|
+
const conflict = conflicts.find(c => c.decisionId === decisionId);
|
|
1477
|
+
if (!conflict) {
|
|
1478
|
+
console.error(`ā No conflict found for decision: ${decisionId}`);
|
|
1479
|
+
process.exit(1);
|
|
1480
|
+
}
|
|
1481
|
+
// If cloud wins, update local decision
|
|
1482
|
+
if (resolution === 'cloud') {
|
|
1483
|
+
const updated = await updateDecision(decisionId, {
|
|
1484
|
+
decision: conflict.cloudDecision,
|
|
1485
|
+
updatedAt: new Date().toISOString()
|
|
1486
|
+
});
|
|
1487
|
+
if (!updated) {
|
|
1488
|
+
console.error(`ā Failed to update local decision: ${decisionId}`);
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
// Resolve conflict in metadata
|
|
1493
|
+
await resolveConflict(projectRoot, decisionId, resolution, resolution === 'cloud' ? {
|
|
1494
|
+
id: '',
|
|
1495
|
+
user_id: '',
|
|
1496
|
+
project_name: '',
|
|
1497
|
+
decision_id: decisionId,
|
|
1498
|
+
scope: conflict.scope,
|
|
1499
|
+
decision: conflict.cloudDecision,
|
|
1500
|
+
rationale: null,
|
|
1501
|
+
constraints: null,
|
|
1502
|
+
status: 'active',
|
|
1503
|
+
synced_at: new Date().toISOString(),
|
|
1504
|
+
updated_at: conflict.cloudUpdatedAt
|
|
1505
|
+
} : undefined);
|
|
1506
|
+
// Remove conflict from incoming.json
|
|
1507
|
+
await removeIncomingChanges(projectRoot, [decisionId]);
|
|
1508
|
+
// If local wins, push to cloud to make it the source of truth
|
|
1509
|
+
if (resolution === 'local') {
|
|
1510
|
+
const decision = await getDecisionById(decisionId);
|
|
1511
|
+
if (decision) {
|
|
1512
|
+
const projectName = path.basename(projectRoot);
|
|
1513
|
+
console.log(' ā¬ļø Pushing local version to cloud...');
|
|
1514
|
+
await syncDecisionsToCloud(projectName, [decision]);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
console.log(`\nā
Conflict resolved: ${decisionId}`);
|
|
1518
|
+
console.log(` Accepted: ${resolution} version\n`);
|
|
1519
|
+
// Log conflict resolution to history
|
|
1520
|
+
await logAction('conflict_resolved', decisionId, `Conflict resolved: accepted ${resolution} (${decisionId})`, 'cloud');
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
// Default: Interactive conflict resolution
|
|
1524
|
+
const status = await getCloudStatus();
|
|
1525
|
+
if (!status.authenticated) {
|
|
1526
|
+
console.log('\nā Not logged in. Run "decide login" first.\n');
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (!status.isPro) {
|
|
1530
|
+
console.log('\nā Cloud sync requires a Pro subscription.');
|
|
1531
|
+
console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const cwd = process.cwd();
|
|
1535
|
+
const projectName = path.basename(cwd);
|
|
1536
|
+
console.log('\nā ļø Checking for conflicts...\n');
|
|
1537
|
+
// Get local and cloud decisions
|
|
1538
|
+
const localDecisions = await listDecisions();
|
|
1539
|
+
const cloudDecisions = await pullDecisionsFromCloud(projectName);
|
|
1540
|
+
if (!cloudDecisions) {
|
|
1541
|
+
console.log('ā Failed to fetch cloud decisions.');
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
// Detect conflicts
|
|
1545
|
+
const { conflicts } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions);
|
|
1546
|
+
if (conflicts.length === 0) {
|
|
1547
|
+
console.log('ā
No conflicts found! Everything is in sync.\n');
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
console.log(`Found ${conflicts.length} conflict(s):\n`);
|
|
1551
|
+
console.log('ā'.repeat(60));
|
|
1552
|
+
for (let i = 0; i < conflicts.length; i++) {
|
|
1553
|
+
const conflict = conflicts[i];
|
|
1554
|
+
const cloudDecision = cloudDecisions.find(d => d.decision_id === conflict.decisionId);
|
|
1555
|
+
console.log(`\n${i + 1}. ${conflict.decisionId} (${conflict.scope})`);
|
|
1556
|
+
console.log('ā'.repeat(40));
|
|
1557
|
+
console.log(` š Local: "${conflict.localDecision.substring(0, 50)}${conflict.localDecision.length > 50 ? '...' : ''}"`);
|
|
1558
|
+
console.log(` Updated: ${conflict.localUpdatedAt ? new Date(conflict.localUpdatedAt).toLocaleString() : 'Unknown'}`);
|
|
1559
|
+
console.log(` āļø Cloud: "${conflict.cloudDecision.substring(0, 50)}${conflict.cloudDecision.length > 50 ? '...' : ''}"`);
|
|
1560
|
+
console.log(` Updated: ${new Date(conflict.cloudUpdatedAt).toLocaleString()}`);
|
|
1561
|
+
const choice = await prompt('\n [L]ocal or [C]loud? ');
|
|
1562
|
+
const resolution = choice.toLowerCase().startsWith('l') ? 'local' : 'cloud';
|
|
1563
|
+
if (resolution === 'local') {
|
|
1564
|
+
// Keep local version - mark for push
|
|
1565
|
+
await resolveConflict(projectRoot, conflict.decisionId, 'local');
|
|
1566
|
+
console.log(' ā
Keeping local version (will push on next sync)');
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
// Use cloud version - update local
|
|
1570
|
+
if (cloudDecision) {
|
|
1571
|
+
await resolveConflict(projectRoot, conflict.decisionId, 'cloud', cloudDecision);
|
|
1572
|
+
// Update local decision with cloud data
|
|
1573
|
+
await updateDecision(conflict.decisionId, {
|
|
1574
|
+
decision: cloudDecision.decision,
|
|
1575
|
+
rationale: cloudDecision.rationale || undefined,
|
|
1576
|
+
constraints: cloudDecision.constraints || undefined,
|
|
1577
|
+
status: cloudDecision.status,
|
|
1578
|
+
});
|
|
1579
|
+
console.log(' ā
Updated to cloud version');
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
console.log('\nš All conflicts resolved!\n');
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Handle fetch command - check for cloud updates without applying them
|
|
1587
|
+
*/
|
|
1588
|
+
async function handleFetch() {
|
|
1589
|
+
const status = await getCloudStatus();
|
|
1590
|
+
if (!status.authenticated) {
|
|
1591
|
+
console.log('\nā Not logged in. Run "decide login" first.\n');
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (!status.isPro) {
|
|
1595
|
+
console.log('\nā Cloud sync requires a Pro subscription.');
|
|
1596
|
+
console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
console.log('\nš Fetching updates from cloud... (no changes will be applied)\n');
|
|
1600
|
+
const cwd = process.cwd();
|
|
1601
|
+
const projectName = path.basename(cwd);
|
|
1602
|
+
const projectRoot = getProjectRoot();
|
|
1603
|
+
// 1. Get local decisions
|
|
1604
|
+
const localDecisions = await listDecisions();
|
|
1605
|
+
// 2. Get cloud decisions
|
|
1606
|
+
const cloudDecisions = await pullDecisionsFromCloud(projectName);
|
|
1607
|
+
if (!cloudDecisions) {
|
|
1608
|
+
console.log('ā Failed to fetch cloud decisions.');
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
// 3. Detect conflicts/updates
|
|
1612
|
+
const { toPull, conflicts } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions);
|
|
1613
|
+
// 4. Save results to incoming.json
|
|
1614
|
+
await saveIncomingChanges(projectRoot, { toPull, conflicts });
|
|
1615
|
+
// 5. Report status
|
|
1616
|
+
if (toPull.length === 0 && conflicts.length === 0) {
|
|
1617
|
+
console.log('ā
Local decisions are up to date.');
|
|
1618
|
+
}
|
|
1619
|
+
else {
|
|
1620
|
+
if (toPull.length > 0) {
|
|
1621
|
+
console.log(`ā¬ļø ${toPull.length} incoming change(s) available.`);
|
|
1622
|
+
toPull.slice(0, 3).forEach(d => console.log(` - ${d.decision_id} (${d.scope})`));
|
|
1623
|
+
if (toPull.length > 3)
|
|
1624
|
+
console.log(` ...and ${toPull.length - 3} more`);
|
|
1625
|
+
}
|
|
1626
|
+
if (conflicts.length > 0) {
|
|
1627
|
+
console.log(`\nā ļø ${conflicts.length} conflict(s) detected.`);
|
|
1628
|
+
}
|
|
1629
|
+
console.log('\nš Run "decide sync --pull-only" to apply changes.');
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function printUsage() {
|
|
1633
|
+
console.log(`
|
|
1634
|
+
DecisionNode CLI
|
|
1635
|
+
|
|
1636
|
+
Usage:
|
|
1637
|
+
decide <command> [options]
|
|
1638
|
+
|
|
1639
|
+
Commands:
|
|
1640
|
+
init Initialize DecisionNode in current project
|
|
1641
|
+
setup Configure Gemini API key
|
|
1642
|
+
list [--scope <s>] List all decisions (includes global)
|
|
1643
|
+
list --global List only global decisions
|
|
1644
|
+
get <id> View a decision
|
|
1645
|
+
search "<query>" Semantic search (includes global)
|
|
1646
|
+
|
|
1647
|
+
add Add a new decision interactively (auto-embeds)
|
|
1648
|
+
add -s <scope> -d <decision> [-r <rationale>] [-c <constraints>]
|
|
1649
|
+
Add a decision in one command
|
|
1650
|
+
add --global Add a global decision (applies to all projects)
|
|
1651
|
+
edit <id> Edit a decision (auto-embeds)
|
|
1652
|
+
deprecate <id> Deprecate a decision (hides from search)
|
|
1653
|
+
activate <id> Re-activate a deprecated decision
|
|
1654
|
+
delete <id> Delete a decision permanently
|
|
1655
|
+
|
|
1656
|
+
import <file.json> Import decisions from JSON (auto-embeds)
|
|
1657
|
+
import <file> --global Import into global decisions
|
|
1658
|
+
export [format] Export decisions (md, json, csv)
|
|
1659
|
+
export --global Export global decisions
|
|
1660
|
+
check Show which decisions are missing embeddings
|
|
1661
|
+
embed Embed any unembedded decisions
|
|
1662
|
+
clean Remove orphaned vectors and reviews
|
|
1663
|
+
history [entry-id] View activity log or snapshot
|
|
1664
|
+
|
|
1665
|
+
projects List all available projects
|
|
1666
|
+
config View/set configuration
|
|
1667
|
+
delete-scope <scope> Delete all decisions in a scope
|
|
1668
|
+
|
|
1669
|
+
Global decision IDs use the "global:" prefix (e.g., global:ui-001).
|
|
1670
|
+
Use this prefix with get, edit, and delete commands.
|
|
1671
|
+
|
|
1672
|
+
Examples:
|
|
1673
|
+
decide init
|
|
1674
|
+
decide add
|
|
1675
|
+
decide add --global
|
|
1676
|
+
decide search "What font should I use?"
|
|
1677
|
+
decide list --global
|
|
1678
|
+
decide get global:ui-001
|
|
1679
|
+
decide edit global:ui-001
|
|
1680
|
+
`);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Handle config command - view or set configuration options
|
|
1684
|
+
*/
|
|
1685
|
+
async function handleConfig() {
|
|
1686
|
+
const subCommand = args[1];
|
|
1687
|
+
const value = args[2];
|
|
1688
|
+
if (!subCommand) {
|
|
1689
|
+
// Show current config
|
|
1690
|
+
const sensitivity = getSearchSensitivity();
|
|
1691
|
+
console.log('\nāļø DecisionNode Configuration\n');
|
|
1692
|
+
console.log(` search-sensitivity: ${sensitivity}`);
|
|
1693
|
+
console.log('\n Options:');
|
|
1694
|
+
console.log(' search-sensitivity high|medium');
|
|
1695
|
+
console.log('\n Usage: decide config <option> <value>');
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (subCommand === 'search-sensitivity') {
|
|
1699
|
+
if (!value) {
|
|
1700
|
+
const current = getSearchSensitivity();
|
|
1701
|
+
console.log(`\nš Current search-sensitivity: ${current}`);
|
|
1702
|
+
console.log('\nUsage: decide config search-sensitivity <high|medium>');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (value !== 'high' && value !== 'medium') {
|
|
1706
|
+
console.error('ā Invalid value. Use "high" or "medium"');
|
|
1707
|
+
process.exit(1);
|
|
1708
|
+
}
|
|
1709
|
+
setSearchSensitivity(value);
|
|
1710
|
+
console.log(`\nā
Search sensitivity set to: ${value}`);
|
|
1711
|
+
if (value === 'high') {
|
|
1712
|
+
console.log(' AI will be REQUIRED to search decisions before any code changes.');
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
console.log(' AI will check decisions for significant changes only.');
|
|
1716
|
+
}
|
|
1717
|
+
console.log('\nš” Refresh your MCP Server Config for changes to take effect.');
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
console.error(`ā Unknown config option: ${subCommand}`);
|
|
1721
|
+
console.log('Available options: search-sensitivity, auto-sync');
|
|
1722
|
+
process.exit(1);
|
|
1723
|
+
}
|
|
1724
|
+
main();
|