@wrongstack/cli 0.5.2 → 0.5.5
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/dist/index.js +1133 -279
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import * as
|
|
3
|
-
import {
|
|
2
|
+
import * as path20 from 'path';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import * as fs3 from 'fs/promises';
|
|
5
|
+
import { readdir, readFile } from 'fs/promises';
|
|
6
|
+
import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
|
|
7
|
+
import { createRequire } from 'module';
|
|
8
|
+
import * as os6 from 'os';
|
|
9
|
+
import os6__default from 'os';
|
|
4
10
|
import * as crypto from 'crypto';
|
|
5
11
|
import { randomUUID } from 'crypto';
|
|
6
|
-
import * as fs5 from 'fs/promises';
|
|
7
12
|
import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
|
|
8
13
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
14
|
import { writeFileSync } from 'fs';
|
|
10
|
-
import { createRequire } from 'module';
|
|
11
15
|
import { MCPRegistry } from '@wrongstack/mcp';
|
|
12
16
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
|
|
13
17
|
import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
|
|
14
18
|
import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
|
|
15
|
-
import * as os4 from 'os';
|
|
16
19
|
import * as readline from 'readline';
|
|
17
20
|
import { spawn } from 'child_process';
|
|
18
21
|
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
@@ -22,6 +25,12 @@ var __defProp = Object.defineProperty;
|
|
|
22
25
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
23
26
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
24
27
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
28
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
29
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
30
|
+
}) : x)(function(x) {
|
|
31
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
32
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
33
|
+
});
|
|
25
34
|
var __esm = (fn, res) => function __init() {
|
|
26
35
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
27
36
|
};
|
|
@@ -54,28 +63,37 @@ __export(sdd_exports, {
|
|
|
54
63
|
trySaveSpecFromAIOutput: () => trySaveSpecFromAIOutput,
|
|
55
64
|
trySaveTasksFromAIOutput: () => trySaveTasksFromAIOutput
|
|
56
65
|
});
|
|
66
|
+
function getSessionState(ctx) {
|
|
67
|
+
if (!ctx) {
|
|
68
|
+
return sddState;
|
|
69
|
+
}
|
|
70
|
+
let state = ctx.meta[SDD_META_KEY];
|
|
71
|
+
if (!state) {
|
|
72
|
+
state = new SDDState();
|
|
73
|
+
ctx.meta[SDD_META_KEY] = state;
|
|
74
|
+
}
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
57
77
|
function getActiveSDDContext() {
|
|
58
|
-
|
|
59
|
-
const session = activeBuilder.getSession();
|
|
60
|
-
if (session.phase === "done") return null;
|
|
61
|
-
return activeBuilder.getAIPrompt();
|
|
78
|
+
return sddState.getContext();
|
|
62
79
|
}
|
|
63
80
|
function getActiveSDDPhase() {
|
|
64
|
-
|
|
65
|
-
return activeBuilder.getPhase();
|
|
81
|
+
return sddState.getPhase();
|
|
66
82
|
}
|
|
67
83
|
async function trySaveSpecFromAIOutput(aiOutput) {
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
const builder = sddState.getBuilder();
|
|
85
|
+
if (!builder) return false;
|
|
86
|
+
const spec = builder.tryParseSpecFromOutput(aiOutput);
|
|
70
87
|
if (!spec) return false;
|
|
71
|
-
|
|
88
|
+
builder.setSpec(spec);
|
|
72
89
|
return true;
|
|
73
90
|
}
|
|
74
91
|
async function trySaveTasksFromAIOutput(aiOutput) {
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const builder = sddState.getBuilder();
|
|
93
|
+
if (!builder) return false;
|
|
94
|
+
const session = builder.getSession();
|
|
77
95
|
if (!session.spec) return false;
|
|
78
|
-
const json =
|
|
96
|
+
const json = builder.extractJSONArray(aiOutput);
|
|
79
97
|
if (!json) return false;
|
|
80
98
|
let tasks;
|
|
81
99
|
try {
|
|
@@ -106,15 +124,16 @@ async function trySaveTasksFromAIOutput(aiOutput) {
|
|
|
106
124
|
tags
|
|
107
125
|
});
|
|
108
126
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
sddState.setTaskStore(store);
|
|
128
|
+
sddState.setTaskTracker(tracker);
|
|
129
|
+
sddState.setTaskGraphId(graph.id);
|
|
130
|
+
builder.setTaskGraphId(graph.id);
|
|
113
131
|
return true;
|
|
114
132
|
}
|
|
115
133
|
function getTaskProgress() {
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
const tracker = sddState.getTaskTracker();
|
|
135
|
+
if (!tracker) return null;
|
|
136
|
+
const progress = tracker.getProgress();
|
|
118
137
|
return {
|
|
119
138
|
total: progress.total,
|
|
120
139
|
completed: progress.completed,
|
|
@@ -123,8 +142,9 @@ function getTaskProgress() {
|
|
|
123
142
|
};
|
|
124
143
|
}
|
|
125
144
|
function getTaskListText() {
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
const tracker = sddState.getTaskTracker();
|
|
146
|
+
if (!tracker) return null;
|
|
147
|
+
const nodes = tracker.getAllNodes();
|
|
128
148
|
if (nodes.length === 0) return null;
|
|
129
149
|
const lines = nodes.map((n, i) => {
|
|
130
150
|
const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : "\u23F3";
|
|
@@ -133,18 +153,20 @@ function getTaskListText() {
|
|
|
133
153
|
return lines.join("\n");
|
|
134
154
|
}
|
|
135
155
|
function markTaskCompleted(taskTitle) {
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
const tracker = sddState.getTaskTracker();
|
|
157
|
+
if (!tracker) return false;
|
|
158
|
+
const nodes = tracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
138
159
|
const match = nodes.find(
|
|
139
160
|
(n) => n.title.toLowerCase().includes(taskTitle.toLowerCase()) || taskTitle.toLowerCase().includes(n.title.toLowerCase())
|
|
140
161
|
);
|
|
141
162
|
if (!match) return false;
|
|
142
|
-
|
|
163
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
143
164
|
return true;
|
|
144
165
|
}
|
|
145
166
|
function autoDetectTaskCompletion(aiOutput) {
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
const tracker = sddState.getTaskTracker();
|
|
168
|
+
if (!tracker) return 0;
|
|
169
|
+
const pending = tracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
148
170
|
if (pending.length === 0) return 0;
|
|
149
171
|
let completed = 0;
|
|
150
172
|
const lines = aiOutput.split("\n");
|
|
@@ -157,7 +179,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
157
179
|
if (!Number.isNaN(num) && num >= 1 && num <= pending.length) {
|
|
158
180
|
const node = pending[num - 1];
|
|
159
181
|
if (node && node.status !== "completed") {
|
|
160
|
-
|
|
182
|
+
tracker.updateNodeStatus(node.id, "completed");
|
|
161
183
|
completed++;
|
|
162
184
|
}
|
|
163
185
|
} else {
|
|
@@ -165,7 +187,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
165
187
|
(n) => n.title.toLowerCase().includes(target.toLowerCase()) || target.toLowerCase().includes(n.title.toLowerCase())
|
|
166
188
|
);
|
|
167
189
|
if (match && match.status !== "completed") {
|
|
168
|
-
|
|
190
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
169
191
|
completed++;
|
|
170
192
|
}
|
|
171
193
|
}
|
|
@@ -178,7 +200,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
178
200
|
(n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
|
|
179
201
|
);
|
|
180
202
|
if (match && match.status !== "completed") {
|
|
181
|
-
|
|
203
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
182
204
|
completed++;
|
|
183
205
|
}
|
|
184
206
|
continue;
|
|
@@ -189,7 +211,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
189
211
|
if (num >= 1 && num <= pending.length) {
|
|
190
212
|
const node = pending[num - 1];
|
|
191
213
|
if (node && node.status !== "completed") {
|
|
192
|
-
|
|
214
|
+
tracker.updateNodeStatus(node.id, "completed");
|
|
193
215
|
completed++;
|
|
194
216
|
}
|
|
195
217
|
}
|
|
@@ -202,7 +224,7 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
202
224
|
(n) => n.title.toLowerCase().includes(title.toLowerCase()) || title.toLowerCase().includes(n.title.toLowerCase())
|
|
203
225
|
);
|
|
204
226
|
if (match && match.status !== "completed") {
|
|
205
|
-
|
|
227
|
+
tracker.updateNodeStatus(match.id, "completed");
|
|
206
228
|
completed++;
|
|
207
229
|
}
|
|
208
230
|
}
|
|
@@ -210,35 +232,37 @@ function autoDetectTaskCompletion(aiOutput) {
|
|
|
210
232
|
return completed;
|
|
211
233
|
}
|
|
212
234
|
function trySaveImplementationPlan(aiOutput) {
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
const builder = sddState.getBuilder();
|
|
236
|
+
if (!builder) return false;
|
|
237
|
+
const session = builder.getSession();
|
|
215
238
|
if (session.phase !== "implementation") return false;
|
|
216
239
|
const jsonMatch = aiOutput.match(/```json\s*\[/);
|
|
217
240
|
if (jsonMatch?.index && jsonMatch.index > 0) {
|
|
218
241
|
const plan = aiOutput.substring(0, jsonMatch.index).trim();
|
|
219
242
|
if (plan.length > 50) {
|
|
220
|
-
|
|
243
|
+
builder.setImplementation(plan);
|
|
221
244
|
return true;
|
|
222
245
|
}
|
|
223
246
|
}
|
|
224
247
|
if (aiOutput.length > 100 && !aiOutput.includes("```json")) {
|
|
225
|
-
|
|
248
|
+
builder.setImplementation(aiOutput.trim());
|
|
226
249
|
return true;
|
|
227
250
|
}
|
|
228
251
|
return false;
|
|
229
252
|
}
|
|
230
253
|
function getActiveBuilder() {
|
|
231
|
-
return
|
|
254
|
+
return sddState.getBuilder();
|
|
232
255
|
}
|
|
233
256
|
function buildSddCommand(opts) {
|
|
257
|
+
getSessionState(opts.context);
|
|
234
258
|
return {
|
|
235
259
|
name: "sdd",
|
|
236
260
|
description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
|
|
237
261
|
async run(args) {
|
|
238
262
|
const ctx = opts.context;
|
|
239
263
|
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
240
|
-
const specsDir =
|
|
241
|
-
const graphsDir =
|
|
264
|
+
const specsDir = path20.join(projectRoot, ".wrongstack", "specs");
|
|
265
|
+
const graphsDir = path20.join(projectRoot, ".wrongstack", "task-graphs");
|
|
242
266
|
const specStore = new SpecStore({ baseDir: specsDir });
|
|
243
267
|
new TaskGraphStore({ baseDir: graphsDir });
|
|
244
268
|
const versioning = new SpecVersioning();
|
|
@@ -253,11 +277,10 @@ function buildSddCommand(opts) {
|
|
|
253
277
|
case "create": {
|
|
254
278
|
const forceFlag = rest.includes("--force") || rest.includes("-f");
|
|
255
279
|
const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
|
|
256
|
-
if (!
|
|
257
|
-
const sessionPath =
|
|
280
|
+
if (!sddState.getBuilder() && !forceFlag) {
|
|
281
|
+
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
258
282
|
try {
|
|
259
|
-
|
|
260
|
-
await fsp.access(sessionPath);
|
|
283
|
+
await fs3.access(sessionPath);
|
|
261
284
|
const projectContext2 = await gatherProjectContext(projectRoot);
|
|
262
285
|
const tempBuilder = new AISpecBuilder({
|
|
263
286
|
store: specStore,
|
|
@@ -283,19 +306,18 @@ function buildSddCommand(opts) {
|
|
|
283
306
|
} catch {
|
|
284
307
|
}
|
|
285
308
|
}
|
|
286
|
-
|
|
287
|
-
activeTaskTracker = null;
|
|
288
|
-
activeTaskGraphId = null;
|
|
309
|
+
sddState.clearTaskState();
|
|
289
310
|
const projectContext = await gatherProjectContext(projectRoot);
|
|
290
|
-
|
|
311
|
+
sddState.setBuilder(new AISpecBuilder({
|
|
291
312
|
store: specStore,
|
|
292
313
|
projectContext,
|
|
293
314
|
minQuestions: 2,
|
|
294
315
|
maxQuestions: 10,
|
|
295
|
-
sessionPath:
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
|
|
316
|
+
sessionPath: path20.join(projectRoot, ".wrongstack", "sdd-session.json")
|
|
317
|
+
}));
|
|
318
|
+
const builder = sddState.getBuilder();
|
|
319
|
+
builder.startSession(title);
|
|
320
|
+
const aiPrompt = builder.getAIPrompt();
|
|
299
321
|
return {
|
|
300
322
|
message: [
|
|
301
323
|
`\u2554\u2550\u2550\u2550 SDD: AI Spec Builder \u2550\u2550\u2550\u2557`,
|
|
@@ -319,14 +341,15 @@ Start the specification interview for "${title}". Ask your first contextual ques
|
|
|
319
341
|
case "approve":
|
|
320
342
|
case "ok":
|
|
321
343
|
case "confirm": {
|
|
322
|
-
|
|
344
|
+
const builder = sddState.getBuilder();
|
|
345
|
+
if (!builder) {
|
|
323
346
|
return {
|
|
324
347
|
message: "No active SDD session. Use /sdd new to start one."
|
|
325
348
|
};
|
|
326
349
|
}
|
|
327
|
-
const phase =
|
|
350
|
+
const phase = builder.getSession().phase;
|
|
328
351
|
if (phase === "questioning") {
|
|
329
|
-
const sddCtx =
|
|
352
|
+
const sddCtx = builder.getAIPrompt();
|
|
330
353
|
return {
|
|
331
354
|
message: "No spec generated yet. Generating now...",
|
|
332
355
|
runText: `[SDD SESSION ACTIVE]
|
|
@@ -338,14 +361,14 @@ Generate the complete specification now based on the conversation so far.`
|
|
|
338
361
|
};
|
|
339
362
|
}
|
|
340
363
|
if (phase === "spec_review") {
|
|
341
|
-
const spec =
|
|
364
|
+
const spec = builder.getSession().spec;
|
|
342
365
|
if (!spec) {
|
|
343
366
|
return { message: "No spec to approve." };
|
|
344
367
|
}
|
|
345
|
-
await
|
|
368
|
+
await builder.saveSpec();
|
|
346
369
|
versioning.recordVersion(spec, "Initial spec approved");
|
|
347
|
-
|
|
348
|
-
const implPrompt =
|
|
370
|
+
builder.approve();
|
|
371
|
+
const implPrompt = builder.getAIPrompt();
|
|
349
372
|
return {
|
|
350
373
|
message: [
|
|
351
374
|
`\u2705 Spec "${spec.title}" approved and saved!`,
|
|
@@ -363,8 +386,8 @@ Generate the implementation plan and tasks for the approved spec.`
|
|
|
363
386
|
};
|
|
364
387
|
}
|
|
365
388
|
if (phase === "task_review") {
|
|
366
|
-
|
|
367
|
-
const execPrompt =
|
|
389
|
+
builder.approve();
|
|
390
|
+
const execPrompt = builder.getAIPrompt();
|
|
368
391
|
return {
|
|
369
392
|
message: "\u2705 Tasks approved! The AI will now execute them one by one.",
|
|
370
393
|
runText: `[SDD SESSION ACTIVE]
|
|
@@ -382,18 +405,19 @@ Start executing the tasks one by one.`
|
|
|
382
405
|
// ── Task Execution ─────────────────────────────────────────────────
|
|
383
406
|
case "execute":
|
|
384
407
|
case "run": {
|
|
385
|
-
|
|
408
|
+
const runBuilder = sddState.getBuilder();
|
|
409
|
+
if (!runBuilder) {
|
|
386
410
|
return {
|
|
387
411
|
message: "No active SDD session. Use /sdd new to start one."
|
|
388
412
|
};
|
|
389
413
|
}
|
|
390
|
-
const session =
|
|
414
|
+
const session = runBuilder.getSession();
|
|
391
415
|
if (session.phase !== "executing" && session.phase !== "task_review") {
|
|
392
416
|
return {
|
|
393
417
|
message: `Cannot execute in phase "${session.phase}". Use /sdd approve first.`
|
|
394
418
|
};
|
|
395
419
|
}
|
|
396
|
-
const execPrompt =
|
|
420
|
+
const execPrompt = runBuilder.getAIPrompt();
|
|
397
421
|
return {
|
|
398
422
|
message: "\u26A1 Starting task execution. The AI will execute tasks one by one.",
|
|
399
423
|
runText: `[SDD SESSION ACTIVE]
|
|
@@ -406,34 +430,36 @@ Start executing the tasks one by one.`
|
|
|
406
430
|
}
|
|
407
431
|
case "plan":
|
|
408
432
|
case "impl": {
|
|
409
|
-
|
|
433
|
+
const planBuilder = sddState.getBuilder();
|
|
434
|
+
if (!planBuilder) {
|
|
410
435
|
return { message: "No active SDD session. Use /sdd new to start one." };
|
|
411
436
|
}
|
|
412
|
-
const
|
|
413
|
-
if (!
|
|
437
|
+
const planSession = planBuilder.getSession();
|
|
438
|
+
if (!planSession.implementation) {
|
|
414
439
|
return {
|
|
415
|
-
message:
|
|
440
|
+
message: planSession.phase === "implementation" ? "No implementation plan yet. The AI will generate it after /sdd approve." : "No implementation plan in this session."
|
|
416
441
|
};
|
|
417
442
|
}
|
|
418
443
|
return {
|
|
419
444
|
message: [
|
|
420
445
|
"\u2550\u2550\u2550 Implementation Plan \u2550\u2550\u2550",
|
|
421
446
|
"",
|
|
422
|
-
|
|
447
|
+
planSession.implementation
|
|
423
448
|
].join("\n")
|
|
424
449
|
};
|
|
425
450
|
}
|
|
426
451
|
case "spec": {
|
|
427
|
-
|
|
452
|
+
const specBuilder = sddState.getBuilder();
|
|
453
|
+
if (!specBuilder) {
|
|
428
454
|
return { message: "No active SDD session. Use /sdd new to start one." };
|
|
429
455
|
}
|
|
430
|
-
const
|
|
431
|
-
if (!
|
|
456
|
+
const specSession = specBuilder.getSession();
|
|
457
|
+
if (!specSession.spec) {
|
|
432
458
|
return {
|
|
433
|
-
message:
|
|
459
|
+
message: specSession.phase === "questioning" ? "No spec generated yet. Keep answering the AI's questions." : "No spec in this session."
|
|
434
460
|
};
|
|
435
461
|
}
|
|
436
|
-
const spec =
|
|
462
|
+
const spec = specSession.spec;
|
|
437
463
|
const lines = [
|
|
438
464
|
`\u2550\u2550\u2550 Current Spec \u2550\u2550\u2550`,
|
|
439
465
|
"",
|
|
@@ -455,14 +481,15 @@ Start executing the tasks one by one.`
|
|
|
455
481
|
}
|
|
456
482
|
case "tasks":
|
|
457
483
|
case "task": {
|
|
458
|
-
|
|
484
|
+
const taskTracker = sddState.getTaskTracker();
|
|
485
|
+
if (!taskTracker) {
|
|
459
486
|
return { message: "No tasks generated yet. Use /sdd new to start." };
|
|
460
487
|
}
|
|
461
|
-
const nodes =
|
|
488
|
+
const nodes = taskTracker.getAllNodes();
|
|
462
489
|
if (nodes.length === 0) {
|
|
463
490
|
return { message: "No tasks in the current graph." };
|
|
464
491
|
}
|
|
465
|
-
const progress =
|
|
492
|
+
const progress = taskTracker.getProgress();
|
|
466
493
|
const lines = [
|
|
467
494
|
`\u2550\u2550\u2550 Task List (${progress.completed}/${progress.total} done) \u2550\u2550\u2550`,
|
|
468
495
|
""
|
|
@@ -479,19 +506,20 @@ Start executing the tasks one by one.`
|
|
|
479
506
|
}
|
|
480
507
|
case "done":
|
|
481
508
|
case "complete": {
|
|
482
|
-
|
|
509
|
+
const doneTracker = sddState.getTaskTracker();
|
|
510
|
+
if (!doneTracker) {
|
|
483
511
|
return { message: "No tasks to complete." };
|
|
484
512
|
}
|
|
485
513
|
if (!restJoined) {
|
|
486
514
|
return { message: "Usage: /sdd done <task title or number>" };
|
|
487
515
|
}
|
|
488
|
-
const nodes =
|
|
516
|
+
const nodes = doneTracker.getAllNodes({ status: ["pending", "in_progress"] });
|
|
489
517
|
const num = Number(restJoined);
|
|
490
518
|
let matched = false;
|
|
491
519
|
if (!Number.isNaN(num) && num >= 1 && num <= nodes.length) {
|
|
492
520
|
const node = nodes[num - 1];
|
|
493
521
|
if (node) {
|
|
494
|
-
|
|
522
|
+
doneTracker.updateNodeStatus(node.id, "completed");
|
|
495
523
|
matched = true;
|
|
496
524
|
}
|
|
497
525
|
}
|
|
@@ -500,24 +528,25 @@ Start executing the tasks one by one.`
|
|
|
500
528
|
(n) => n.title.toLowerCase().includes(restJoined.toLowerCase()) || restJoined.toLowerCase().includes(n.title.toLowerCase())
|
|
501
529
|
);
|
|
502
530
|
if (match) {
|
|
503
|
-
|
|
531
|
+
doneTracker.updateNodeStatus(match.id, "completed");
|
|
504
532
|
matched = true;
|
|
505
533
|
}
|
|
506
534
|
}
|
|
507
535
|
if (!matched) {
|
|
508
536
|
return { message: `No pending task matching "${restJoined}".` };
|
|
509
537
|
}
|
|
510
|
-
const remaining =
|
|
538
|
+
const remaining = doneTracker.getProgress();
|
|
511
539
|
return {
|
|
512
540
|
message: `\u2705 Task completed! ${remaining.completed}/${remaining.total} done (${remaining.percentComplete}%)`
|
|
513
541
|
};
|
|
514
542
|
}
|
|
515
543
|
// ── Session Management ─────────────────────────────────────────────
|
|
516
544
|
case "status": {
|
|
517
|
-
|
|
545
|
+
const statusBuilder = sddState.getBuilder();
|
|
546
|
+
if (!statusBuilder) {
|
|
518
547
|
return { message: "No active SDD session." };
|
|
519
548
|
}
|
|
520
|
-
const session =
|
|
549
|
+
const session = statusBuilder.getSession();
|
|
521
550
|
const phaseEmoji = {
|
|
522
551
|
questioning: "\u2753",
|
|
523
552
|
spec_review: "\u{1F4CB}",
|
|
@@ -552,21 +581,19 @@ Start executing the tasks one by one.`
|
|
|
552
581
|
};
|
|
553
582
|
}
|
|
554
583
|
case "cancel": {
|
|
555
|
-
const sessionPath =
|
|
584
|
+
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
556
585
|
let deletedFromDisk = false;
|
|
557
586
|
try {
|
|
558
|
-
|
|
559
|
-
await fsp.unlink(sessionPath);
|
|
587
|
+
await fs3.unlink(sessionPath);
|
|
560
588
|
deletedFromDisk = true;
|
|
561
589
|
} catch {
|
|
562
590
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
activeTaskGraphId = null;
|
|
591
|
+
const cancelBuilder = sddState.getBuilder();
|
|
592
|
+
if (cancelBuilder) {
|
|
593
|
+
const title = cancelBuilder.getSession().title;
|
|
594
|
+
await cancelBuilder.deleteSession();
|
|
595
|
+
sddState.setBuilder(null);
|
|
596
|
+
sddState.clearTaskState();
|
|
570
597
|
return { message: `SDD session for "${title}" cancelled.` };
|
|
571
598
|
}
|
|
572
599
|
if (deletedFromDisk) {
|
|
@@ -575,36 +602,37 @@ Start executing the tasks one by one.`
|
|
|
575
602
|
return { message: "No active SDD session." };
|
|
576
603
|
}
|
|
577
604
|
case "resume": {
|
|
578
|
-
if (
|
|
605
|
+
if (sddState.getBuilder()) {
|
|
579
606
|
return { message: "An SDD session is already active. Use /sdd cancel first." };
|
|
580
607
|
}
|
|
581
|
-
const sessionPath =
|
|
608
|
+
const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
582
609
|
const projectContext = await gatherProjectContext(projectRoot);
|
|
583
|
-
|
|
610
|
+
sddState.setBuilder(new AISpecBuilder({
|
|
584
611
|
store: specStore,
|
|
585
612
|
projectContext,
|
|
586
613
|
minQuestions: 2,
|
|
587
614
|
maxQuestions: 10,
|
|
588
615
|
sessionPath
|
|
589
|
-
});
|
|
590
|
-
const
|
|
616
|
+
}));
|
|
617
|
+
const resumeBuilder = sddState.getBuilder();
|
|
618
|
+
const loaded = await resumeBuilder.loadSession();
|
|
591
619
|
if (!loaded) {
|
|
592
|
-
|
|
620
|
+
sddState.setBuilder(null);
|
|
593
621
|
return { message: "No saved SDD session found. Use /sdd new to start one." };
|
|
594
622
|
}
|
|
595
|
-
const session =
|
|
623
|
+
const session = resumeBuilder.getSession();
|
|
596
624
|
let taskCount = 0;
|
|
597
625
|
let completedCount = 0;
|
|
598
|
-
const taskGraphId =
|
|
626
|
+
const taskGraphId = resumeBuilder.getTaskGraphId();
|
|
599
627
|
if (taskGraphId) {
|
|
600
628
|
try {
|
|
601
629
|
const store = new DefaultTaskStore();
|
|
602
630
|
const tracker = new TaskTracker({ store });
|
|
603
631
|
const graph = await tracker.loadGraph(taskGraphId);
|
|
604
632
|
if (graph) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
633
|
+
sddState.setTaskStore(store);
|
|
634
|
+
sddState.setTaskTracker(tracker);
|
|
635
|
+
sddState.setTaskGraphId(taskGraphId);
|
|
608
636
|
const progress = tracker.getProgress();
|
|
609
637
|
taskCount = progress.total;
|
|
610
638
|
completedCount = progress.completed;
|
|
@@ -612,7 +640,7 @@ Start executing the tasks one by one.`
|
|
|
612
640
|
} catch {
|
|
613
641
|
}
|
|
614
642
|
}
|
|
615
|
-
const resumePrompt =
|
|
643
|
+
const resumePrompt = resumeBuilder.getAIPrompt();
|
|
616
644
|
return {
|
|
617
645
|
message: [
|
|
618
646
|
`\u2554\u2550\u2550\u2550 SDD Session Resumed \u2550\u2550\u2550\u2557`,
|
|
@@ -803,9 +831,8 @@ function sddHelp() {
|
|
|
803
831
|
async function gatherProjectContext(projectRoot) {
|
|
804
832
|
const parts = [];
|
|
805
833
|
try {
|
|
806
|
-
const
|
|
807
|
-
const
|
|
808
|
-
const pkgRaw = await fsp.readFile(pkgPath, "utf8");
|
|
834
|
+
const pkgPath = path20.join(projectRoot, "package.json");
|
|
835
|
+
const pkgRaw = await fs3.readFile(pkgPath, "utf8");
|
|
809
836
|
const pkg = JSON.parse(pkgRaw);
|
|
810
837
|
parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
|
|
811
838
|
parts.push(`Description: ${String(pkg.description ?? "none")}`);
|
|
@@ -820,16 +847,14 @@ async function gatherProjectContext(projectRoot) {
|
|
|
820
847
|
} catch {
|
|
821
848
|
}
|
|
822
849
|
try {
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
await fsp.access(tsconfigPath);
|
|
850
|
+
const tsconfigPath = path20.join(projectRoot, "tsconfig.json");
|
|
851
|
+
await fs3.access(tsconfigPath);
|
|
826
852
|
parts.push("Language: TypeScript");
|
|
827
853
|
} catch {
|
|
828
854
|
}
|
|
829
855
|
try {
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
const entries = await fsp.readdir(srcDir, { withFileTypes: true });
|
|
856
|
+
const srcDir = path20.join(projectRoot, "src");
|
|
857
|
+
const entries = await fs3.readdir(srcDir, { withFileTypes: true });
|
|
833
858
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
834
859
|
if (dirs.length > 0) {
|
|
835
860
|
parts.push(`Source structure: src/${dirs.join(", src/")}`);
|
|
@@ -849,13 +874,55 @@ async function findSpec(store, idOrTitle) {
|
|
|
849
874
|
if (match) return store.load(match.id);
|
|
850
875
|
return null;
|
|
851
876
|
}
|
|
852
|
-
var
|
|
877
|
+
var SDD_META_KEY, SDDState, sddState;
|
|
853
878
|
var init_sdd = __esm({
|
|
854
879
|
"src/slash-commands/sdd.ts"() {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
880
|
+
SDD_META_KEY = "sdd.state";
|
|
881
|
+
SDDState = class {
|
|
882
|
+
builder = null;
|
|
883
|
+
taskStore = null;
|
|
884
|
+
taskTracker = null;
|
|
885
|
+
taskGraphId = null;
|
|
886
|
+
getBuilder() {
|
|
887
|
+
return this.builder;
|
|
888
|
+
}
|
|
889
|
+
setBuilder(b) {
|
|
890
|
+
this.builder = b;
|
|
891
|
+
}
|
|
892
|
+
getTaskStore() {
|
|
893
|
+
return this.taskStore;
|
|
894
|
+
}
|
|
895
|
+
setTaskStore(s) {
|
|
896
|
+
this.taskStore = s;
|
|
897
|
+
}
|
|
898
|
+
getTaskTracker() {
|
|
899
|
+
return this.taskTracker;
|
|
900
|
+
}
|
|
901
|
+
setTaskTracker(t) {
|
|
902
|
+
this.taskTracker = t;
|
|
903
|
+
}
|
|
904
|
+
getTaskGraphId() {
|
|
905
|
+
return this.taskGraphId;
|
|
906
|
+
}
|
|
907
|
+
setTaskGraphId(id) {
|
|
908
|
+
this.taskGraphId = id;
|
|
909
|
+
}
|
|
910
|
+
clearTaskState() {
|
|
911
|
+
this.taskStore = null;
|
|
912
|
+
this.taskTracker = null;
|
|
913
|
+
this.taskGraphId = null;
|
|
914
|
+
}
|
|
915
|
+
getContext() {
|
|
916
|
+
if (!this.builder) return null;
|
|
917
|
+
const session = this.builder.getSession();
|
|
918
|
+
if (session.phase === "done") return null;
|
|
919
|
+
return this.builder.getAIPrompt();
|
|
920
|
+
}
|
|
921
|
+
getPhase() {
|
|
922
|
+
return this.builder?.getPhase() ?? null;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
sddState = new SDDState();
|
|
859
926
|
}
|
|
860
927
|
});
|
|
861
928
|
function normalizeKeys(cfg) {
|
|
@@ -900,6 +967,130 @@ var init_provider_config_utils = __esm({
|
|
|
900
967
|
}
|
|
901
968
|
});
|
|
902
969
|
|
|
970
|
+
// src/update-check.ts
|
|
971
|
+
var update_check_exports = {};
|
|
972
|
+
__export(update_check_exports, {
|
|
973
|
+
cachePath: () => cachePath,
|
|
974
|
+
checkForUpdate: () => checkForUpdate,
|
|
975
|
+
currentVersion: () => currentVersion,
|
|
976
|
+
getUpdateNotification: () => getUpdateNotification
|
|
977
|
+
});
|
|
978
|
+
function cachePath(homeFn = defaultHomeDir2) {
|
|
979
|
+
return path20.join(homeFn(), ".wrongstack", "update-cache.json");
|
|
980
|
+
}
|
|
981
|
+
function currentVersion() {
|
|
982
|
+
const req2 = createRequire(import.meta.url);
|
|
983
|
+
const candidates = ["../package.json", "../../package.json"];
|
|
984
|
+
for (const rel of candidates) {
|
|
985
|
+
try {
|
|
986
|
+
const pkg = req2(rel);
|
|
987
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return "dev";
|
|
992
|
+
}
|
|
993
|
+
function isNewer(a, b) {
|
|
994
|
+
const parse = (v) => v.replace(/^v/i, "").split(".").map((p) => parseInt(p, 10) || 0);
|
|
995
|
+
const [ap, bp] = [parse(a), parse(b)];
|
|
996
|
+
for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
|
|
997
|
+
const ai = ap[i] ?? 0;
|
|
998
|
+
const bi = bp[i] ?? 0;
|
|
999
|
+
if (ai > bi) return true;
|
|
1000
|
+
if (ai < bi) return false;
|
|
1001
|
+
}
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
async function readCache(homeFn = defaultHomeDir2) {
|
|
1005
|
+
try {
|
|
1006
|
+
const raw = await fs3.readFile(cachePath(homeFn), "utf8");
|
|
1007
|
+
const entry = JSON.parse(raw);
|
|
1008
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
|
|
1009
|
+
return entry;
|
|
1010
|
+
} catch {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async function writeCache(entry, homeFn = defaultHomeDir2) {
|
|
1015
|
+
try {
|
|
1016
|
+
const dir = path20.dirname(cachePath(homeFn));
|
|
1017
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
1018
|
+
await fs3.writeFile(cachePath(homeFn), JSON.stringify(entry, null, 2), "utf8");
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
async function fetchLatestFromNpm(timeoutMs = 3e3) {
|
|
1023
|
+
const controller = new AbortController();
|
|
1024
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1025
|
+
try {
|
|
1026
|
+
const res = await fetch("https://registry.npmjs.org/wrongstack/latest", {
|
|
1027
|
+
signal: controller.signal,
|
|
1028
|
+
headers: { "Accept": "application/json" }
|
|
1029
|
+
});
|
|
1030
|
+
clearTimeout(timer);
|
|
1031
|
+
if (!res.ok) throw new Error(`npm registry responded ${res.status}`);
|
|
1032
|
+
const data = await res.json();
|
|
1033
|
+
if (typeof data.version === "string") return data.version;
|
|
1034
|
+
throw new Error("No version field in npm response");
|
|
1035
|
+
} finally {
|
|
1036
|
+
clearTimeout(timer);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async function checkForUpdate(signal, homeFn) {
|
|
1040
|
+
const current = currentVersion();
|
|
1041
|
+
const aborted = () => signal?.aborted ?? false;
|
|
1042
|
+
const hf = homeFn ?? defaultHomeDir2;
|
|
1043
|
+
if (aborted()) {
|
|
1044
|
+
return { current, latest: current, outdated: false, checkFailed: true };
|
|
1045
|
+
}
|
|
1046
|
+
const cached = await readCache(hf);
|
|
1047
|
+
if (cached && !cached.error) {
|
|
1048
|
+
return {
|
|
1049
|
+
current,
|
|
1050
|
+
latest: cached.latestVersion,
|
|
1051
|
+
outdated: isNewer(cached.latestVersion, current),
|
|
1052
|
+
checkFailed: false
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const latest = await fetchLatestFromNpm();
|
|
1057
|
+
await writeCache({ timestamp: Date.now(), latestVersion: latest }, hf);
|
|
1058
|
+
return {
|
|
1059
|
+
current,
|
|
1060
|
+
latest,
|
|
1061
|
+
outdated: isNewer(latest, current),
|
|
1062
|
+
checkFailed: false
|
|
1063
|
+
};
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
if (aborted()) {
|
|
1066
|
+
return { current, latest: current, outdated: false, checkFailed: true };
|
|
1067
|
+
}
|
|
1068
|
+
if (cached?.latestVersion) {
|
|
1069
|
+
return {
|
|
1070
|
+
current,
|
|
1071
|
+
latest: cached.latestVersion,
|
|
1072
|
+
outdated: isNewer(cached.latestVersion, current),
|
|
1073
|
+
checkFailed: true
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return { current, latest: current, outdated: false, checkFailed: true };
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function getUpdateNotification(signal, homeFn) {
|
|
1080
|
+
const info = await checkForUpdate(signal, homeFn);
|
|
1081
|
+
if (info.outdated) {
|
|
1082
|
+
return `Update available: v${info.current} \u2192 v${info.latest}`;
|
|
1083
|
+
}
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
var defaultHomeDir2, CACHE_TTL_MS;
|
|
1087
|
+
var init_update_check = __esm({
|
|
1088
|
+
"src/update-check.ts"() {
|
|
1089
|
+
defaultHomeDir2 = () => os6.homedir();
|
|
1090
|
+
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
|
|
903
1094
|
// src/webui-server.ts
|
|
904
1095
|
var webui_server_exports = {};
|
|
905
1096
|
__export(webui_server_exports, {
|
|
@@ -1006,7 +1197,7 @@ async function runWebUI(opts) {
|
|
|
1006
1197
|
})
|
|
1007
1198
|
);
|
|
1008
1199
|
}
|
|
1009
|
-
return new Promise((
|
|
1200
|
+
return new Promise((resolve4) => {
|
|
1010
1201
|
wss.on("listening", () => {
|
|
1011
1202
|
console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
|
|
1012
1203
|
setupEvents();
|
|
@@ -1076,7 +1267,7 @@ async function runWebUI(opts) {
|
|
|
1076
1267
|
clients.clear();
|
|
1077
1268
|
wss.close(() => {
|
|
1078
1269
|
console.log("[WebUI] Server stopped");
|
|
1079
|
-
|
|
1270
|
+
resolve4();
|
|
1080
1271
|
});
|
|
1081
1272
|
}
|
|
1082
1273
|
process.on("SIGINT", shutdown);
|
|
@@ -1382,7 +1573,7 @@ async function runWebUI(opts) {
|
|
|
1382
1573
|
if (!opts.globalConfigPath) return {};
|
|
1383
1574
|
let raw;
|
|
1384
1575
|
try {
|
|
1385
|
-
raw = await
|
|
1576
|
+
raw = await fs3.readFile(opts.globalConfigPath, "utf8");
|
|
1386
1577
|
} catch {
|
|
1387
1578
|
return {};
|
|
1388
1579
|
}
|
|
@@ -1393,7 +1584,7 @@ async function runWebUI(opts) {
|
|
|
1393
1584
|
return {};
|
|
1394
1585
|
}
|
|
1395
1586
|
if (!parsed.providers) return {};
|
|
1396
|
-
const keyFile =
|
|
1587
|
+
const keyFile = path20.join(path20.dirname(opts.globalConfigPath), ".key");
|
|
1397
1588
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1398
1589
|
return decryptConfigSecrets$1(parsed.providers, vault);
|
|
1399
1590
|
}
|
|
@@ -1401,7 +1592,7 @@ async function runWebUI(opts) {
|
|
|
1401
1592
|
if (!opts.globalConfigPath) return;
|
|
1402
1593
|
let raw;
|
|
1403
1594
|
try {
|
|
1404
|
-
raw = await
|
|
1595
|
+
raw = await fs3.readFile(opts.globalConfigPath, "utf8");
|
|
1405
1596
|
} catch {
|
|
1406
1597
|
raw = "{}";
|
|
1407
1598
|
}
|
|
@@ -1412,7 +1603,7 @@ async function runWebUI(opts) {
|
|
|
1412
1603
|
parsed = {};
|
|
1413
1604
|
}
|
|
1414
1605
|
parsed.providers = providers;
|
|
1415
|
-
const keyFile =
|
|
1606
|
+
const keyFile = path20.join(path20.dirname(opts.globalConfigPath), ".key");
|
|
1416
1607
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1417
1608
|
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1418
1609
|
await atomicWrite(opts.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
@@ -1440,6 +1631,37 @@ var init_plugin_api_factory = __esm({
|
|
|
1440
1631
|
}
|
|
1441
1632
|
});
|
|
1442
1633
|
|
|
1634
|
+
// src/slash-commands/commit-llm.ts
|
|
1635
|
+
async function generateCommitMessageWithLLM(diff, opts) {
|
|
1636
|
+
const systemPrompt = "You are a helpful assistant that generates concise, conventional-commit-formatted git commit messages. Analyze the provided diff and output ONLY the commit message (no explanation, no quotes). Format: <type>(<scope>): <short description> \u2014 <type> is one of: feat, fix, docs, style, refactor, test, chore, perf, ci, build, temp. If the diff contains multiple unrelated changes, pick the most important one. Keep the description under 72 characters. Example: feat(cli): add /commit LLM integration";
|
|
1637
|
+
const userPrompt = `Here is the git diff:
|
|
1638
|
+
|
|
1639
|
+
${diff}`;
|
|
1640
|
+
try {
|
|
1641
|
+
const signal = new AbortController();
|
|
1642
|
+
const timeout = setTimeout(() => signal.abort(), 15e3);
|
|
1643
|
+
const resp = await opts.provider.complete(
|
|
1644
|
+
{
|
|
1645
|
+
model: opts.model,
|
|
1646
|
+
system: [{ type: "text", text: systemPrompt }],
|
|
1647
|
+
messages: [{ role: "user", content: [{ type: "text", text: userPrompt }] }],
|
|
1648
|
+
maxTokens: 80,
|
|
1649
|
+
temperature: 0.3
|
|
1650
|
+
},
|
|
1651
|
+
{ signal: signal.signal }
|
|
1652
|
+
);
|
|
1653
|
+
clearTimeout(timeout);
|
|
1654
|
+
const rawContent = resp.content;
|
|
1655
|
+
const text = Array.isArray(rawContent) ? rawContent[0]?.text ?? "" : typeof rawContent === "object" && rawContent !== null ? rawContent.text ?? "" : String(rawContent);
|
|
1656
|
+
const message = text.trim().split("\n")[0];
|
|
1657
|
+
if (message.length > 0 && message.length < 200) {
|
|
1658
|
+
return message;
|
|
1659
|
+
}
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
return "chore: update";
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1443
1665
|
// src/arg-parser.ts
|
|
1444
1666
|
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
1445
1667
|
"yolo",
|
|
@@ -1458,7 +1680,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
1458
1680
|
"output-json",
|
|
1459
1681
|
"prompt",
|
|
1460
1682
|
"metrics",
|
|
1461
|
-
"webui"
|
|
1683
|
+
"webui",
|
|
1684
|
+
"no-check"
|
|
1462
1685
|
]);
|
|
1463
1686
|
function parseArgs(argv) {
|
|
1464
1687
|
const flags = {};
|
|
@@ -1564,10 +1787,10 @@ function parseSpawnFlags(input) {
|
|
|
1564
1787
|
return { description: rest.trim(), opts };
|
|
1565
1788
|
}
|
|
1566
1789
|
async function bootConfig(flags) {
|
|
1567
|
-
const cwd = typeof flags["cwd"] === "string" ?
|
|
1790
|
+
const cwd = typeof flags["cwd"] === "string" ? path20.resolve(flags["cwd"]) : process.cwd();
|
|
1568
1791
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
1569
1792
|
const projectRoot = pathResolver.projectRoot;
|
|
1570
|
-
const userHome =
|
|
1793
|
+
const userHome = os6.homedir();
|
|
1571
1794
|
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
1572
1795
|
await ensureProjectMeta(wpaths, projectRoot);
|
|
1573
1796
|
const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
|
|
@@ -1615,13 +1838,13 @@ function flagsToConfigPatch(flags) {
|
|
|
1615
1838
|
}
|
|
1616
1839
|
async function ensureProjectMeta(paths, projectRoot) {
|
|
1617
1840
|
try {
|
|
1618
|
-
await
|
|
1841
|
+
await fs3.mkdir(paths.projectDir, { recursive: true });
|
|
1619
1842
|
const meta = {
|
|
1620
1843
|
hash: paths.projectHash,
|
|
1621
1844
|
root: projectRoot,
|
|
1622
1845
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1623
1846
|
};
|
|
1624
|
-
await
|
|
1847
|
+
await fs3.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
1625
1848
|
} catch {
|
|
1626
1849
|
}
|
|
1627
1850
|
}
|
|
@@ -1631,11 +1854,11 @@ var ReadlineInputReader = class {
|
|
|
1631
1854
|
history = [];
|
|
1632
1855
|
pending = false;
|
|
1633
1856
|
constructor(opts = {}) {
|
|
1634
|
-
this.historyFile = opts.historyFile ??
|
|
1857
|
+
this.historyFile = opts.historyFile ?? path20.join(os6.homedir(), ".wrongstack", "history");
|
|
1635
1858
|
}
|
|
1636
1859
|
async loadHistory() {
|
|
1637
1860
|
try {
|
|
1638
|
-
const raw = await
|
|
1861
|
+
const raw = await fs3.readFile(this.historyFile, "utf8");
|
|
1639
1862
|
this.history = raw.split("\n").filter(Boolean).slice(-1e3);
|
|
1640
1863
|
} catch {
|
|
1641
1864
|
this.history = [];
|
|
@@ -1643,8 +1866,8 @@ var ReadlineInputReader = class {
|
|
|
1643
1866
|
}
|
|
1644
1867
|
async saveHistory() {
|
|
1645
1868
|
try {
|
|
1646
|
-
await
|
|
1647
|
-
await
|
|
1869
|
+
await fs3.mkdir(path20.dirname(this.historyFile), { recursive: true });
|
|
1870
|
+
await fs3.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
|
|
1648
1871
|
} catch {
|
|
1649
1872
|
}
|
|
1650
1873
|
}
|
|
@@ -1662,7 +1885,7 @@ var ReadlineInputReader = class {
|
|
|
1662
1885
|
async readLine(prompt) {
|
|
1663
1886
|
if (this.history.length === 0) await this.loadHistory();
|
|
1664
1887
|
while (this.pending) {
|
|
1665
|
-
await new Promise((
|
|
1888
|
+
await new Promise((resolve4) => setTimeout(resolve4, 50));
|
|
1666
1889
|
}
|
|
1667
1890
|
this.pending = true;
|
|
1668
1891
|
try {
|
|
@@ -1672,15 +1895,15 @@ var ReadlineInputReader = class {
|
|
|
1672
1895
|
this.rl = void 0;
|
|
1673
1896
|
}
|
|
1674
1897
|
const fresh = this.ensure();
|
|
1675
|
-
return new Promise((
|
|
1898
|
+
return new Promise((resolve4) => {
|
|
1676
1899
|
fresh.question(prompt ?? "> ", (line) => {
|
|
1677
1900
|
if (line.trim()) {
|
|
1678
1901
|
this.history.push(line);
|
|
1679
1902
|
void this.saveHistory();
|
|
1680
1903
|
}
|
|
1681
|
-
|
|
1904
|
+
resolve4(line);
|
|
1682
1905
|
});
|
|
1683
|
-
fresh.once("close", () =>
|
|
1906
|
+
fresh.once("close", () => resolve4(""));
|
|
1684
1907
|
}).then((result) => {
|
|
1685
1908
|
this.rl?.close();
|
|
1686
1909
|
this.rl = void 0;
|
|
@@ -1692,7 +1915,7 @@ var ReadlineInputReader = class {
|
|
|
1692
1915
|
}
|
|
1693
1916
|
async readKey(prompt, options) {
|
|
1694
1917
|
process.stdout.write(prompt);
|
|
1695
|
-
return new Promise((
|
|
1918
|
+
return new Promise((resolve4) => {
|
|
1696
1919
|
const stdin = process.stdin;
|
|
1697
1920
|
const wasRaw = stdin.isRaw;
|
|
1698
1921
|
const wasPaused = stdin.isPaused();
|
|
@@ -1703,7 +1926,7 @@ var ReadlineInputReader = class {
|
|
|
1703
1926
|
if (key === "") {
|
|
1704
1927
|
cleanup();
|
|
1705
1928
|
process.stdout.write("\n");
|
|
1706
|
-
|
|
1929
|
+
resolve4("");
|
|
1707
1930
|
return;
|
|
1708
1931
|
}
|
|
1709
1932
|
const opt = options.find(
|
|
@@ -1713,12 +1936,12 @@ var ReadlineInputReader = class {
|
|
|
1713
1936
|
cleanup();
|
|
1714
1937
|
process.stdout.write(`${opt.key}
|
|
1715
1938
|
`);
|
|
1716
|
-
|
|
1939
|
+
resolve4(opt.value);
|
|
1717
1940
|
}
|
|
1718
1941
|
};
|
|
1719
1942
|
const onClose = () => {
|
|
1720
1943
|
cleanup();
|
|
1721
|
-
|
|
1944
|
+
resolve4("");
|
|
1722
1945
|
};
|
|
1723
1946
|
const cleanup = () => {
|
|
1724
1947
|
stdin.off("data", onData);
|
|
@@ -1746,7 +1969,7 @@ var ReadlineInputReader = class {
|
|
|
1746
1969
|
this.rl?.close();
|
|
1747
1970
|
this.rl = void 0;
|
|
1748
1971
|
process.stdout.write(prompt);
|
|
1749
|
-
return new Promise((
|
|
1972
|
+
return new Promise((resolve4) => {
|
|
1750
1973
|
let buf = "";
|
|
1751
1974
|
const wasRaw = stdin.isRaw;
|
|
1752
1975
|
stdin.setRawMode(true);
|
|
@@ -1764,7 +1987,7 @@ var ReadlineInputReader = class {
|
|
|
1764
1987
|
cleanup();
|
|
1765
1988
|
process.stdout.write(` ${dim(`[${buf.length} chars]`)}
|
|
1766
1989
|
`);
|
|
1767
|
-
|
|
1990
|
+
resolve4(buf);
|
|
1768
1991
|
return;
|
|
1769
1992
|
}
|
|
1770
1993
|
if (ch === "") {
|
|
@@ -1861,8 +2084,247 @@ async function buildPickableProviders(modelsRegistry, config) {
|
|
|
1861
2084
|
}
|
|
1862
2085
|
return out;
|
|
1863
2086
|
}
|
|
2087
|
+
var PROTECTED_BASENAMES = /* @__PURE__ */ new Set([
|
|
2088
|
+
"config.json",
|
|
2089
|
+
".key",
|
|
2090
|
+
"index.json"
|
|
2091
|
+
]);
|
|
2092
|
+
function assertSafeToDelete(filename, parentDir) {
|
|
2093
|
+
if (PROTECTED_BASENAMES.has(filename)) {
|
|
2094
|
+
throw new Error(`Refusing to delete protected file: ${filename}`);
|
|
2095
|
+
}
|
|
2096
|
+
if (filename !== path20.basename(filename)) {
|
|
2097
|
+
throw new Error(`Refusing to delete path with traversal: ${filename}`);
|
|
2098
|
+
}
|
|
2099
|
+
if (!filename.startsWith("config.json.") || !filename.endsWith(".bak")) {
|
|
2100
|
+
throw new Error(`Refusing to delete unknown file: ${filename}`);
|
|
2101
|
+
}
|
|
2102
|
+
const resolvedParent = path20.resolve(parentDir);
|
|
2103
|
+
if (!resolvedParent.endsWith(".wrongstack")) {
|
|
2104
|
+
throw new Error(`Unexpected parent directory for bak prune: ${resolvedParent}`);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
async function safeDelete(filePath) {
|
|
2108
|
+
const dir = path20.dirname(filePath);
|
|
2109
|
+
const filename = path20.basename(filePath);
|
|
2110
|
+
try {
|
|
2111
|
+
assertSafeToDelete(filename, dir);
|
|
2112
|
+
await fs3.unlink(filePath);
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
if (err instanceof Error && err.message.startsWith("Refusing")) {
|
|
2115
|
+
process.stderr.write(`[config-history] SAFETY: ${err.message}
|
|
2116
|
+
`);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function maskConfigSecrets(cfg) {
|
|
2121
|
+
if (typeof cfg !== "object" || cfg === null) return {};
|
|
2122
|
+
const out = {};
|
|
2123
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
2124
|
+
if (k === "apiKey" || k === "apiKeys" || k === "secret" || k === "secrets") {
|
|
2125
|
+
out[k] = "[REDACTED]";
|
|
2126
|
+
} else if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
2127
|
+
out[k] = maskConfigSecrets(v);
|
|
2128
|
+
} else {
|
|
2129
|
+
out[k] = v;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return out;
|
|
2133
|
+
}
|
|
2134
|
+
function diffSummary(oldCfg, newCfg) {
|
|
2135
|
+
const changes = [];
|
|
2136
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldCfg), ...Object.keys(newCfg)]);
|
|
2137
|
+
for (const k of allKeys) {
|
|
2138
|
+
const o = JSON.stringify(oldCfg[k]);
|
|
2139
|
+
const n = JSON.stringify(newCfg[k]);
|
|
2140
|
+
if (o !== n) {
|
|
2141
|
+
if (k === "apiKey" || k === "apiKeys" || k === "secret") {
|
|
2142
|
+
changes.push(`${k}: [CHANGED]`);
|
|
2143
|
+
} else if (typeof newCfg[k] !== "object") {
|
|
2144
|
+
changes.push(`${k}: ${oldCfg[k] ?? "(unset)"} \u2192 ${newCfg[k]}`);
|
|
2145
|
+
} else {
|
|
2146
|
+
changes.push(`${k}: [CHANGED]`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return changes.length > 0 ? changes.slice(0, 5).join(", ") : "no changes";
|
|
2151
|
+
}
|
|
2152
|
+
var defaultHomeDir = () => os6__default.homedir();
|
|
2153
|
+
function historyDir(homeFn = defaultHomeDir) {
|
|
2154
|
+
return path20.join(homeFn(), ".wrongstack", "config.history", "entries");
|
|
2155
|
+
}
|
|
2156
|
+
function historyIndexPath(homeFn = defaultHomeDir) {
|
|
2157
|
+
return path20.join(homeFn(), ".wrongstack", "config.history", "index.json");
|
|
2158
|
+
}
|
|
2159
|
+
function configPath(homeFn = defaultHomeDir) {
|
|
2160
|
+
return path20.join(homeFn(), ".wrongstack", "config.json");
|
|
2161
|
+
}
|
|
2162
|
+
function backupLastPath(homeFn = defaultHomeDir) {
|
|
2163
|
+
return path20.join(homeFn(), ".wrongstack", "config.json.last");
|
|
2164
|
+
}
|
|
2165
|
+
function entryId(ts) {
|
|
2166
|
+
return ts.replace(/[:.]/g, "-").slice(0, 19);
|
|
2167
|
+
}
|
|
2168
|
+
async function ensureHistoryDir(homeFn = defaultHomeDir) {
|
|
2169
|
+
await fs3.mkdir(historyDir(homeFn), { recursive: true });
|
|
2170
|
+
}
|
|
2171
|
+
async function readIndex(homeFn = defaultHomeDir) {
|
|
2172
|
+
try {
|
|
2173
|
+
const raw = await fs3.readFile(historyIndexPath(homeFn), "utf8");
|
|
2174
|
+
return JSON.parse(raw);
|
|
2175
|
+
} catch {
|
|
2176
|
+
return { version: 1, entries: [] };
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
async function writeIndex(idx, homeFn = defaultHomeDir) {
|
|
2180
|
+
await ensureHistoryDir(homeFn);
|
|
2181
|
+
await fs3.writeFile(historyIndexPath(homeFn), JSON.stringify(idx, null, 2), "utf8");
|
|
2182
|
+
}
|
|
2183
|
+
async function backupCurrent(homeFn = defaultHomeDir) {
|
|
2184
|
+
const cfg = configPath(homeFn);
|
|
2185
|
+
const last = backupLastPath(homeFn);
|
|
2186
|
+
const ts = Date.now();
|
|
2187
|
+
let content;
|
|
2188
|
+
try {
|
|
2189
|
+
content = await fs3.readFile(cfg, "utf8");
|
|
2190
|
+
} catch {
|
|
2191
|
+
}
|
|
2192
|
+
if (content !== void 0) {
|
|
2193
|
+
try {
|
|
2194
|
+
await atomicWrite(last, content);
|
|
2195
|
+
} catch {
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (content !== void 0) {
|
|
2199
|
+
try {
|
|
2200
|
+
const bakPath = path20.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
|
|
2201
|
+
await atomicWrite(bakPath, content);
|
|
2202
|
+
} catch {
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
try {
|
|
2206
|
+
const dir = path20.join(homeFn(), ".wrongstack");
|
|
2207
|
+
const files = await fs3.readdir(dir);
|
|
2208
|
+
const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
|
|
2209
|
+
for (const f of baks.slice(10)) {
|
|
2210
|
+
await safeDelete(path20.join(dir, f));
|
|
2211
|
+
}
|
|
2212
|
+
} catch {
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDir) {
|
|
2216
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2217
|
+
const id = entryId(timestamp);
|
|
2218
|
+
await ensureHistoryDir(homeFn);
|
|
2219
|
+
const entry = {
|
|
2220
|
+
id,
|
|
2221
|
+
timestamp,
|
|
2222
|
+
description,
|
|
2223
|
+
snapshotMasked: maskConfigSecrets(newCfg),
|
|
2224
|
+
diffSummary: diffSummary(oldCfg, newCfg)
|
|
2225
|
+
};
|
|
2226
|
+
await fs3.writeFile(
|
|
2227
|
+
path20.join(historyDir(homeFn), `${id}.json`),
|
|
2228
|
+
JSON.stringify(entry, null, 2),
|
|
2229
|
+
"utf8"
|
|
2230
|
+
);
|
|
2231
|
+
const idx = await readIndex(homeFn);
|
|
2232
|
+
idx.entries.unshift({ id, timestamp, description });
|
|
2233
|
+
await writeIndex(idx, homeFn);
|
|
2234
|
+
return id;
|
|
2235
|
+
}
|
|
2236
|
+
async function listHistory(homeFn = defaultHomeDir) {
|
|
2237
|
+
const idx = await readIndex(homeFn);
|
|
2238
|
+
return idx.entries;
|
|
2239
|
+
}
|
|
2240
|
+
async function getHistoryEntry(id, homeFn = defaultHomeDir) {
|
|
2241
|
+
try {
|
|
2242
|
+
const raw = await fs3.readFile(path20.join(historyDir(homeFn), `${id}.json`), "utf8");
|
|
2243
|
+
return JSON.parse(raw);
|
|
2244
|
+
} catch {
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
async function restoreFromHistory(id, homeFn = defaultHomeDir) {
|
|
2249
|
+
const entry = await getHistoryEntry(id, homeFn);
|
|
2250
|
+
if (!entry) return { ok: false, backupId: null, error: "History entry not found" };
|
|
2251
|
+
await backupCurrent(homeFn);
|
|
2252
|
+
let oldCfg = {};
|
|
2253
|
+
try {
|
|
2254
|
+
const raw = await fs3.readFile(configPath(homeFn), "utf8");
|
|
2255
|
+
oldCfg = JSON.parse(raw);
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
await atomicWrite(configPath(homeFn), JSON.stringify(entry.snapshotMasked, null, 2));
|
|
2260
|
+
} catch (err) {
|
|
2261
|
+
return { ok: false, backupId: null, error: String(err) };
|
|
2262
|
+
}
|
|
2263
|
+
const backupId = await appendHistory(
|
|
2264
|
+
oldCfg,
|
|
2265
|
+
entry.snapshotMasked,
|
|
2266
|
+
`Restored from history ${id}`,
|
|
2267
|
+
homeFn
|
|
2268
|
+
);
|
|
2269
|
+
return { ok: true, backupId };
|
|
2270
|
+
}
|
|
2271
|
+
async function restoreLast(homeFn = defaultHomeDir) {
|
|
2272
|
+
const last = backupLastPath(homeFn);
|
|
2273
|
+
const cfg = configPath(homeFn);
|
|
2274
|
+
let oldCfg = {};
|
|
2275
|
+
try {
|
|
2276
|
+
const raw = await fs3.readFile(cfg, "utf8");
|
|
2277
|
+
oldCfg = JSON.parse(raw);
|
|
2278
|
+
} catch {
|
|
2279
|
+
}
|
|
2280
|
+
let lastCfg = {};
|
|
2281
|
+
try {
|
|
2282
|
+
const raw = await fs3.readFile(last, "utf8");
|
|
2283
|
+
lastCfg = JSON.parse(raw);
|
|
2284
|
+
} catch {
|
|
2285
|
+
return { ok: false, error: "No prior backup found" };
|
|
2286
|
+
}
|
|
2287
|
+
await backupCurrent(homeFn);
|
|
2288
|
+
try {
|
|
2289
|
+
await atomicWrite(cfg, JSON.stringify(lastCfg, null, 2));
|
|
2290
|
+
} catch (err) {
|
|
2291
|
+
return { ok: false, error: String(err) };
|
|
2292
|
+
}
|
|
2293
|
+
await appendHistory(oldCfg, lastCfg, "Restored from config.json.last", homeFn);
|
|
2294
|
+
return { ok: true };
|
|
2295
|
+
}
|
|
1864
2296
|
|
|
1865
2297
|
// src/picker.ts
|
|
2298
|
+
var theme = { primary: color.amber };
|
|
2299
|
+
async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
|
|
2300
|
+
try {
|
|
2301
|
+
const { atomicWrite: atomicWrite7 } = await import('@wrongstack/core');
|
|
2302
|
+
const fs17 = await import('fs/promises');
|
|
2303
|
+
let existing = {};
|
|
2304
|
+
try {
|
|
2305
|
+
const raw = await fs17.readFile(configPath2, "utf8");
|
|
2306
|
+
existing = JSON.parse(raw);
|
|
2307
|
+
} catch {
|
|
2308
|
+
}
|
|
2309
|
+
const oldCfg = { ...existing };
|
|
2310
|
+
existing.provider = provider;
|
|
2311
|
+
existing.model = model;
|
|
2312
|
+
await backupCurrent(homeFn);
|
|
2313
|
+
await atomicWrite7(configPath2, JSON.stringify(existing, null, 2));
|
|
2314
|
+
try {
|
|
2315
|
+
await appendHistory(
|
|
2316
|
+
oldCfg,
|
|
2317
|
+
existing,
|
|
2318
|
+
`Provider/model changed: ${oldCfg.provider ?? "(none)"} \u2192 ${provider}, ${oldCfg.model ?? "(none)"} \u2192 ${model}`,
|
|
2319
|
+
homeFn
|
|
2320
|
+
);
|
|
2321
|
+
} catch {
|
|
2322
|
+
}
|
|
2323
|
+
return true;
|
|
2324
|
+
} catch {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
1866
2328
|
async function runPicker(deps) {
|
|
1867
2329
|
const { modelsRegistry, renderer, reader, config, defaultProvider, defaultModel } = deps;
|
|
1868
2330
|
renderer.write(
|
|
@@ -2093,28 +2555,9 @@ async function resolveModelSelection(answer, models, provider, _registry, render
|
|
|
2093
2555
|
`);
|
|
2094
2556
|
return { provider: provider.id, model: modelId };
|
|
2095
2557
|
}
|
|
2096
|
-
var theme = { primary: color.amber };
|
|
2097
|
-
async function saveToGlobalConfig(configPath, provider, model) {
|
|
2098
|
-
try {
|
|
2099
|
-
const { atomicWrite: atomicWrite6 } = await import('@wrongstack/core');
|
|
2100
|
-
const fs15 = await import('fs/promises');
|
|
2101
|
-
let existing = {};
|
|
2102
|
-
try {
|
|
2103
|
-
const raw = await fs15.readFile(configPath, "utf8");
|
|
2104
|
-
existing = JSON.parse(raw);
|
|
2105
|
-
} catch {
|
|
2106
|
-
}
|
|
2107
|
-
existing.provider = provider;
|
|
2108
|
-
existing.model = model;
|
|
2109
|
-
await atomicWrite6(configPath, JSON.stringify(existing, null, 2));
|
|
2110
|
-
return true;
|
|
2111
|
-
} catch {
|
|
2112
|
-
return false;
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
2558
|
async function pathExists(file) {
|
|
2116
2559
|
try {
|
|
2117
|
-
await
|
|
2560
|
+
await fs3.access(file);
|
|
2118
2561
|
return true;
|
|
2119
2562
|
} catch {
|
|
2120
2563
|
return false;
|
|
@@ -2125,10 +2568,10 @@ async function detectPackageManager(root, declared) {
|
|
|
2125
2568
|
const name = declared.split("@")[0];
|
|
2126
2569
|
if (name) return name;
|
|
2127
2570
|
}
|
|
2128
|
-
if (await pathExists(
|
|
2129
|
-
if (await pathExists(
|
|
2130
|
-
if (await pathExists(
|
|
2131
|
-
if (await pathExists(
|
|
2571
|
+
if (await pathExists(path20.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2572
|
+
if (await pathExists(path20.join(root, "bun.lockb"))) return "bun";
|
|
2573
|
+
if (await pathExists(path20.join(root, "bun.lock"))) return "bun";
|
|
2574
|
+
if (await pathExists(path20.join(root, "yarn.lock"))) return "yarn";
|
|
2132
2575
|
return "npm";
|
|
2133
2576
|
}
|
|
2134
2577
|
function hasUsableScript(scripts, name) {
|
|
@@ -2149,7 +2592,7 @@ function parseMakeTargets(makefile) {
|
|
|
2149
2592
|
async function detectProjectFacts(root) {
|
|
2150
2593
|
const facts = { hints: [] };
|
|
2151
2594
|
try {
|
|
2152
|
-
const pkg = JSON.parse(await
|
|
2595
|
+
const pkg = JSON.parse(await fs3.readFile(path20.join(root, "package.json"), "utf8"));
|
|
2153
2596
|
const scripts = pkg.scripts ?? {};
|
|
2154
2597
|
const pm = await detectPackageManager(root, pkg.packageManager);
|
|
2155
2598
|
if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
|
|
@@ -2163,14 +2606,14 @@ async function detectProjectFacts(root) {
|
|
|
2163
2606
|
} catch {
|
|
2164
2607
|
}
|
|
2165
2608
|
try {
|
|
2166
|
-
if (!await pathExists(
|
|
2609
|
+
if (!await pathExists(path20.join(root, "pyproject.toml"))) throw new Error("not python");
|
|
2167
2610
|
facts.test ??= "pytest";
|
|
2168
2611
|
facts.lint ??= "ruff check .";
|
|
2169
2612
|
facts.hints.push("pyproject.toml");
|
|
2170
2613
|
} catch {
|
|
2171
2614
|
}
|
|
2172
2615
|
try {
|
|
2173
|
-
if (!await pathExists(
|
|
2616
|
+
if (!await pathExists(path20.join(root, "go.mod"))) throw new Error("not go");
|
|
2174
2617
|
facts.build ??= "go build ./...";
|
|
2175
2618
|
facts.test ??= "go test ./...";
|
|
2176
2619
|
facts.run ??= "go run .";
|
|
@@ -2178,7 +2621,7 @@ async function detectProjectFacts(root) {
|
|
|
2178
2621
|
} catch {
|
|
2179
2622
|
}
|
|
2180
2623
|
try {
|
|
2181
|
-
if (!await pathExists(
|
|
2624
|
+
if (!await pathExists(path20.join(root, "Cargo.toml"))) throw new Error("not rust");
|
|
2182
2625
|
facts.build ??= "cargo build";
|
|
2183
2626
|
facts.test ??= "cargo test";
|
|
2184
2627
|
facts.lint ??= "cargo clippy";
|
|
@@ -2187,7 +2630,7 @@ async function detectProjectFacts(root) {
|
|
|
2187
2630
|
} catch {
|
|
2188
2631
|
}
|
|
2189
2632
|
try {
|
|
2190
|
-
const makefile = await
|
|
2633
|
+
const makefile = await fs3.readFile(path20.join(root, "Makefile"), "utf8");
|
|
2191
2634
|
const targets = parseMakeTargets(makefile);
|
|
2192
2635
|
facts.build ??= targets.has("build") ? "make build" : "make";
|
|
2193
2636
|
if (targets.has("test")) facts.test ??= "make test";
|
|
@@ -2322,7 +2765,7 @@ function buildClearCommand(opts) {
|
|
|
2322
2765
|
};
|
|
2323
2766
|
}
|
|
2324
2767
|
async function runGit(args, cwd) {
|
|
2325
|
-
return new Promise((
|
|
2768
|
+
return new Promise((resolve4) => {
|
|
2326
2769
|
const child = spawn("git", args, {
|
|
2327
2770
|
cwd,
|
|
2328
2771
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -2331,7 +2774,7 @@ async function runGit(args, cwd) {
|
|
|
2331
2774
|
let stderr = "";
|
|
2332
2775
|
child.stdout?.on("data", (d) => stdout += d);
|
|
2333
2776
|
child.stderr?.on("data", (d) => stderr += d);
|
|
2334
|
-
child.on("close", (code) =>
|
|
2777
|
+
child.on("close", (code) => resolve4({ stdout, stderr, code: code ?? 0 }));
|
|
2335
2778
|
});
|
|
2336
2779
|
}
|
|
2337
2780
|
function detectCommitType(stats) {
|
|
@@ -2350,7 +2793,7 @@ function detectCommitType(stats) {
|
|
|
2350
2793
|
if (hasConfig) return "chore";
|
|
2351
2794
|
return "feat";
|
|
2352
2795
|
}
|
|
2353
|
-
async function
|
|
2796
|
+
async function generateCommitMessageHeuristics(cwd) {
|
|
2354
2797
|
const statsResult = await runGit(["diff", "--stat"], cwd);
|
|
2355
2798
|
if (statsResult.code !== 0) return "chore: update";
|
|
2356
2799
|
const nameResult = await runGit(["diff", "--name-only"], cwd);
|
|
@@ -2381,7 +2824,7 @@ async function isGitRepo(cwd) {
|
|
|
2381
2824
|
const result = await runGit(["rev-parse", "--git-dir"], cwd);
|
|
2382
2825
|
return result.code === 0;
|
|
2383
2826
|
}
|
|
2384
|
-
function buildCommitCommand(_opts) {
|
|
2827
|
+
function buildCommitCommand(_opts, generateCommitMessage) {
|
|
2385
2828
|
return {
|
|
2386
2829
|
name: "commit",
|
|
2387
2830
|
description: "Stage all changes and commit with auto-generated message.",
|
|
@@ -2395,7 +2838,11 @@ function buildCommitCommand(_opts) {
|
|
|
2395
2838
|
return { message: "Nothing to commit (working tree clean)." };
|
|
2396
2839
|
}
|
|
2397
2840
|
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2398
|
-
|
|
2841
|
+
args.includes("--no-llm");
|
|
2842
|
+
let message;
|
|
2843
|
+
{
|
|
2844
|
+
message = await generateCommitMessageHeuristics(cwd);
|
|
2845
|
+
}
|
|
2399
2846
|
if (dryRun) {
|
|
2400
2847
|
return {
|
|
2401
2848
|
message: `Would commit:
|
|
@@ -2819,10 +3266,10 @@ function buildInitCommand(opts) {
|
|
|
2819
3266
|
description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
|
|
2820
3267
|
async run(args, ctx) {
|
|
2821
3268
|
const force = args.trim() === "--force";
|
|
2822
|
-
const dir =
|
|
2823
|
-
const file =
|
|
3269
|
+
const dir = path20.join(ctx.projectRoot, ".wrongstack");
|
|
3270
|
+
const file = path20.join(dir, "AGENTS.md");
|
|
2824
3271
|
try {
|
|
2825
|
-
await
|
|
3272
|
+
await fs3.access(file);
|
|
2826
3273
|
if (!force) {
|
|
2827
3274
|
const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
2828
3275
|
opts.renderer.writeWarning(msg2);
|
|
@@ -2832,8 +3279,8 @@ function buildInitCommand(opts) {
|
|
|
2832
3279
|
}
|
|
2833
3280
|
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
2834
3281
|
const body = renderAgentsTemplate(detected);
|
|
2835
|
-
await
|
|
2836
|
-
await
|
|
3282
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
3283
|
+
await fs3.writeFile(file, body, "utf8");
|
|
2837
3284
|
if (detected.hints.length > 0) {
|
|
2838
3285
|
const msg2 = `Wrote ${file}
|
|
2839
3286
|
Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
|
|
@@ -3440,12 +3887,231 @@ ${lines.join("\n\n")}
|
|
|
3440
3887
|
}
|
|
3441
3888
|
};
|
|
3442
3889
|
}
|
|
3890
|
+
function buildSecurityCommand(opts) {
|
|
3891
|
+
return {
|
|
3892
|
+
name: "security",
|
|
3893
|
+
description: "Security scanning: scan, audit, report",
|
|
3894
|
+
argsHint: "[scan|audit|report] [options]",
|
|
3895
|
+
help: `
|
|
3896
|
+
# /security \u2014 Security Scanner
|
|
3897
|
+
|
|
3898
|
+
Automated security scanning with tech stack detection.
|
|
3899
|
+
|
|
3900
|
+
## Commands
|
|
3901
|
+
|
|
3902
|
+
### /security scan [options]
|
|
3903
|
+
Run a full security scan on the current project.
|
|
3904
|
+
Options:
|
|
3905
|
+
--depth quick|standard|deep Scan depth (default: standard)
|
|
3906
|
+
--format markdown|json|html Report format (default: markdown)
|
|
3907
|
+
|
|
3908
|
+
### /security audit
|
|
3909
|
+
Run dependency audit + security scan.
|
|
3910
|
+
|
|
3911
|
+
### /security report [id]
|
|
3912
|
+
List or view security reports.
|
|
3913
|
+
|
|
3914
|
+
## Examples
|
|
3915
|
+
|
|
3916
|
+
/security scan
|
|
3917
|
+
/security scan --depth deep --format html
|
|
3918
|
+
/security audit
|
|
3919
|
+
/security report
|
|
3920
|
+
`,
|
|
3921
|
+
async run(args, ctx) {
|
|
3922
|
+
const parts = args.trim().split(/\s+/);
|
|
3923
|
+
const subcommand = parts[0] || "";
|
|
3924
|
+
switch (subcommand) {
|
|
3925
|
+
case "scan":
|
|
3926
|
+
return handleScan(parts.slice(1).join(" "), ctx, opts);
|
|
3927
|
+
case "audit":
|
|
3928
|
+
return handleAudit(ctx, opts);
|
|
3929
|
+
case "report":
|
|
3930
|
+
return handleReport(parts[1] || "");
|
|
3931
|
+
default:
|
|
3932
|
+
return { message: getHelpMessage() };
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
async function handleScan(args, ctx, opts) {
|
|
3938
|
+
const options = parseArgs2(args);
|
|
3939
|
+
const projectRoot = ctx.projectRoot || opts.projectRoot;
|
|
3940
|
+
try {
|
|
3941
|
+
const orchestratorContext = ctx.provider ? ctx : { provider: opts.llmProvider, model: opts.llmModel };
|
|
3942
|
+
if (!orchestratorContext.provider) {
|
|
3943
|
+
return { message: "\u274C Security scan requires an active LLM provider. No provider configured." };
|
|
3944
|
+
}
|
|
3945
|
+
const result = await defaultOrchestrator.run(orchestratorContext, {
|
|
3946
|
+
projectRoot,
|
|
3947
|
+
scanOptions: {
|
|
3948
|
+
depth: options.depth || "standard",
|
|
3949
|
+
includeSecrets: true,
|
|
3950
|
+
includeInjection: true,
|
|
3951
|
+
includeConfig: true
|
|
3952
|
+
},
|
|
3953
|
+
reportOptions: {
|
|
3954
|
+
format: options.format || "markdown"
|
|
3955
|
+
}
|
|
3956
|
+
});
|
|
3957
|
+
const summary = result.scanResult.summary;
|
|
3958
|
+
const status = summary.total === 0 ? "\u2705 No issues found" : `\u26A0\uFE0F Found ${summary.total} issues`;
|
|
3959
|
+
const reportContent = result.synthesizedReport || `# Security Scan Complete
|
|
3960
|
+
|
|
3961
|
+
**Project:** ${projectRoot}
|
|
3962
|
+
**Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
|
|
3963
|
+
**Scanned Files:** ${result.scanResult.scannedFiles}
|
|
3964
|
+
**Duration:** ${result.scanResult.scanDurationMs}ms
|
|
3965
|
+
|
|
3966
|
+
## Summary
|
|
3967
|
+
|
|
3968
|
+
| Severity | Count |
|
|
3969
|
+
|----------|-------|
|
|
3970
|
+
| \u{1F534} Critical | ${summary.critical} |
|
|
3971
|
+
| \u{1F7E0} High | ${summary.high} |
|
|
3972
|
+
| \u{1F7E1} Medium | ${summary.medium} |
|
|
3973
|
+
| \u{1F7E2} Low | ${summary.low} |
|
|
3974
|
+
|
|
3975
|
+
**Status:** ${status}
|
|
3976
|
+
|
|
3977
|
+
**Report:** ${result.reportPath}
|
|
3978
|
+
`;
|
|
3979
|
+
return {
|
|
3980
|
+
message: reportContent,
|
|
3981
|
+
metadata: {
|
|
3982
|
+
scanResult: result.scanResult,
|
|
3983
|
+
reportPath: result.reportPath,
|
|
3984
|
+
techStack: result.detectionResult.detectedStacks[0]
|
|
3985
|
+
}
|
|
3986
|
+
};
|
|
3987
|
+
} catch (error) {
|
|
3988
|
+
return { message: `\u274C Scan failed: ${error}` };
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
async function handleAudit(ctx, opts) {
|
|
3992
|
+
const projectRoot = ctx.projectRoot || opts.projectRoot;
|
|
3993
|
+
try {
|
|
3994
|
+
const orchestratorContext = ctx.provider ? ctx : { provider: opts.llmProvider, model: opts.llmModel };
|
|
3995
|
+
if (!orchestratorContext.provider) {
|
|
3996
|
+
return { message: "\u274C Security audit requires an active LLM provider. No provider configured." };
|
|
3997
|
+
}
|
|
3998
|
+
const result = await defaultOrchestrator.run(orchestratorContext, {
|
|
3999
|
+
projectRoot,
|
|
4000
|
+
reportOptions: { format: "markdown" }
|
|
4001
|
+
});
|
|
4002
|
+
const summary = result.scanResult.summary;
|
|
4003
|
+
const depIssues = summary.critical + summary.high;
|
|
4004
|
+
const reportContent = result.synthesizedReport || `# Security Audit Complete
|
|
4005
|
+
|
|
4006
|
+
**Project:** ${projectRoot}
|
|
4007
|
+
**Tech Stack:** ${result.detectionResult.detectedStacks[0]?.stack || "unknown"}
|
|
4008
|
+
|
|
4009
|
+
## Dependency Health
|
|
4010
|
+
|
|
4011
|
+
| Status | Count |
|
|
4012
|
+
|--------|-------|
|
|
4013
|
+
| Critical Issues | ${summary.critical} |
|
|
4014
|
+
| High Priority | ${summary.high} |
|
|
4015
|
+
| Medium Priority | ${summary.medium} |
|
|
4016
|
+
| Low Priority | ${summary.low} |
|
|
4017
|
+
|
|
4018
|
+
${depIssues === 0 ? "\u2705 No known vulnerabilities detected" : `\u26A0\uFE0F ${depIssues} vulnerabilities need attention`}
|
|
4019
|
+
|
|
4020
|
+
**Full Report:** ${result.reportPath}
|
|
4021
|
+
`;
|
|
4022
|
+
return {
|
|
4023
|
+
message: reportContent,
|
|
4024
|
+
metadata: {
|
|
4025
|
+
scanResult: result.scanResult,
|
|
4026
|
+
reportPath: result.reportPath
|
|
4027
|
+
}
|
|
4028
|
+
};
|
|
4029
|
+
} catch (error) {
|
|
4030
|
+
return { message: `\u274C Audit failed: ${error}` };
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
async function handleReport(reportId) {
|
|
4034
|
+
const reportsDir = "security-reports";
|
|
4035
|
+
try {
|
|
4036
|
+
const files = await readdir(reportsDir);
|
|
4037
|
+
const reports = files.filter((f) => f.startsWith("security-report-") && (f.endsWith(".md") || f.endsWith(".json"))).sort().reverse();
|
|
4038
|
+
if (!reportId) {
|
|
4039
|
+
if (reports.length === 0) {
|
|
4040
|
+
return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
|
|
4041
|
+
}
|
|
4042
|
+
const list = reports.map((r, i) => {
|
|
4043
|
+
const date = r.replace("security-report-", "").replace(/\.(md|json)$/, "");
|
|
4044
|
+
return ` ${i + 1}. ${date}`;
|
|
4045
|
+
}).join("\n");
|
|
4046
|
+
return { message: `# Available Security Reports
|
|
4047
|
+
|
|
4048
|
+
${list}
|
|
4049
|
+
|
|
4050
|
+
Use \`/security report <number>\` to view a specific report.` };
|
|
4051
|
+
}
|
|
4052
|
+
const index = parseInt(reportId, 10) - 1;
|
|
4053
|
+
if (!isNaN(index) && reports[index]) {
|
|
4054
|
+
const content = await readFile(join(reportsDir, reports[index]), "utf-8");
|
|
4055
|
+
return { message: `# Security Report
|
|
4056
|
+
|
|
4057
|
+
${content}` };
|
|
4058
|
+
}
|
|
4059
|
+
const match = reports.find((r) => r.includes(reportId));
|
|
4060
|
+
if (match) {
|
|
4061
|
+
const content = await readFile(join(reportsDir, match), "utf-8");
|
|
4062
|
+
return { message: `# Security Report
|
|
4063
|
+
|
|
4064
|
+
${content}` };
|
|
4065
|
+
}
|
|
4066
|
+
return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
|
|
4067
|
+
} catch {
|
|
4068
|
+
return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
function parseArgs2(args) {
|
|
4072
|
+
const result = {};
|
|
4073
|
+
const parts = args.split(/\s+/);
|
|
4074
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4075
|
+
const part = parts[i];
|
|
4076
|
+
if (!part || !part.startsWith("--")) continue;
|
|
4077
|
+
const key = part.slice(2);
|
|
4078
|
+
const next = parts[i + 1];
|
|
4079
|
+
if (next && !next.startsWith("--")) {
|
|
4080
|
+
result[key] = next;
|
|
4081
|
+
i++;
|
|
4082
|
+
} else {
|
|
4083
|
+
result[key] = "true";
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
return result;
|
|
4087
|
+
}
|
|
4088
|
+
function getHelpMessage() {
|
|
4089
|
+
return `# /security \u2014 Security Scanner
|
|
4090
|
+
|
|
4091
|
+
**Available Commands:**
|
|
4092
|
+
|
|
4093
|
+
1. **/security scan** \u2014 Run full security scan
|
|
4094
|
+
\`/security scan --depth deep --format html\`
|
|
4095
|
+
|
|
4096
|
+
2. **/security audit** \u2014 Run dependency audit + security scan
|
|
4097
|
+
|
|
4098
|
+
3. **/security report** \u2014 List available reports
|
|
4099
|
+
|
|
4100
|
+
**Features:**
|
|
4101
|
+
- Automatic tech stack detection
|
|
4102
|
+
- Dynamic security skill generation
|
|
4103
|
+
- Secrets, injection, and config vulnerability scanning
|
|
4104
|
+
- Markdown/JSON/HTML reports
|
|
4105
|
+
- .gitignore auto-update
|
|
4106
|
+
|
|
4107
|
+
Run \`/security scan\` to start.`;
|
|
4108
|
+
}
|
|
3443
4109
|
function makeInstaller(opts, projectRoot, global) {
|
|
3444
|
-
const globalRoot =
|
|
4110
|
+
const globalRoot = path20.join(os6.homedir(), ".wrongstack");
|
|
3445
4111
|
return new SkillInstaller({
|
|
3446
|
-
manifestPath:
|
|
3447
|
-
projectSkillsDir:
|
|
3448
|
-
globalSkillsDir:
|
|
4112
|
+
manifestPath: path20.join(globalRoot, "installed-skills.json"),
|
|
4113
|
+
projectSkillsDir: path20.join(projectRoot, ".wrongstack", "skills"),
|
|
4114
|
+
globalSkillsDir: path20.join(globalRoot, "skills"),
|
|
3449
4115
|
projectHash: projectHash(projectRoot),
|
|
3450
4116
|
skillLoader: opts.skillLoader
|
|
3451
4117
|
});
|
|
@@ -3637,7 +4303,8 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
3637
4303
|
buildExitCommand(opts),
|
|
3638
4304
|
buildCommitCommand(),
|
|
3639
4305
|
buildGitcheckCommand(),
|
|
3640
|
-
buildPushCommand()
|
|
4306
|
+
buildPushCommand(),
|
|
4307
|
+
buildSecurityCommand(opts)
|
|
3641
4308
|
];
|
|
3642
4309
|
}
|
|
3643
4310
|
|
|
@@ -3656,13 +4323,13 @@ var MANIFESTS = [
|
|
|
3656
4323
|
];
|
|
3657
4324
|
async function detectProjectKind(projectRoot) {
|
|
3658
4325
|
try {
|
|
3659
|
-
await
|
|
4326
|
+
await fs3.access(path20.join(projectRoot, ".wrongstack", "AGENTS.md"));
|
|
3660
4327
|
return "initialized";
|
|
3661
4328
|
} catch {
|
|
3662
4329
|
}
|
|
3663
4330
|
for (const m of MANIFESTS) {
|
|
3664
4331
|
try {
|
|
3665
|
-
await
|
|
4332
|
+
await fs3.access(path20.join(projectRoot, m));
|
|
3666
4333
|
return "project";
|
|
3667
4334
|
} catch {
|
|
3668
4335
|
}
|
|
@@ -3670,12 +4337,12 @@ async function detectProjectKind(projectRoot) {
|
|
|
3670
4337
|
return "empty";
|
|
3671
4338
|
}
|
|
3672
4339
|
async function scaffoldAgentsMd(projectRoot) {
|
|
3673
|
-
const dir =
|
|
3674
|
-
const file =
|
|
4340
|
+
const dir = path20.join(projectRoot, ".wrongstack");
|
|
4341
|
+
const file = path20.join(dir, "AGENTS.md");
|
|
3675
4342
|
const facts = await detectProjectFacts(projectRoot);
|
|
3676
4343
|
const body = renderAgentsTemplate(facts);
|
|
3677
|
-
await
|
|
3678
|
-
await
|
|
4344
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
4345
|
+
await fs3.writeFile(file, body, "utf8");
|
|
3679
4346
|
return file;
|
|
3680
4347
|
}
|
|
3681
4348
|
async function runProjectCheck(opts) {
|
|
@@ -3684,7 +4351,7 @@ async function runProjectCheck(opts) {
|
|
|
3684
4351
|
if (kind === "initialized") {
|
|
3685
4352
|
renderer.write(
|
|
3686
4353
|
`
|
|
3687
|
-
${color.green("\u2713")} Project initialized ${color.dim(`(${
|
|
4354
|
+
${color.green("\u2713")} Project initialized ${color.dim(`(${path20.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
|
|
3688
4355
|
`
|
|
3689
4356
|
);
|
|
3690
4357
|
return true;
|
|
@@ -3711,10 +4378,10 @@ async function runProjectCheck(opts) {
|
|
|
3711
4378
|
}
|
|
3712
4379
|
return true;
|
|
3713
4380
|
}
|
|
3714
|
-
const gitDir =
|
|
4381
|
+
const gitDir = path20.join(projectRoot, ".git");
|
|
3715
4382
|
let hasGit = false;
|
|
3716
4383
|
try {
|
|
3717
|
-
await
|
|
4384
|
+
await fs3.access(gitDir);
|
|
3718
4385
|
hasGit = true;
|
|
3719
4386
|
} catch {
|
|
3720
4387
|
}
|
|
@@ -3729,10 +4396,10 @@ async function runProjectCheck(opts) {
|
|
|
3729
4396
|
)).trim().toLowerCase();
|
|
3730
4397
|
if (answer2 === "y" || answer2 === "yes") {
|
|
3731
4398
|
try {
|
|
3732
|
-
const { spawn:
|
|
3733
|
-
await new Promise((
|
|
3734
|
-
const child =
|
|
3735
|
-
child.on("close", (code) => code === 0 ?
|
|
4399
|
+
const { spawn: spawn3 } = await import('child_process');
|
|
4400
|
+
await new Promise((resolve4, reject) => {
|
|
4401
|
+
const child = spawn3("git", ["init"], { cwd: projectRoot });
|
|
4402
|
+
child.on("close", (code) => code === 0 ? resolve4() : reject(new Error(`git init failed with ${code}`)));
|
|
3736
4403
|
});
|
|
3737
4404
|
renderer.write(` ${color.green("\u2713")} Git repository initialized
|
|
3738
4405
|
`);
|
|
@@ -4012,14 +4679,14 @@ function summarize(value, name) {
|
|
|
4012
4679
|
if (typeof v === "object" && v !== null) {
|
|
4013
4680
|
const o = v;
|
|
4014
4681
|
if (name === "edit") {
|
|
4015
|
-
const
|
|
4682
|
+
const path21 = typeof o["path"] === "string" ? o["path"] : "";
|
|
4016
4683
|
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
4017
|
-
return `${
|
|
4684
|
+
return `${path21} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
4018
4685
|
}
|
|
4019
4686
|
if (name === "write") {
|
|
4020
|
-
const
|
|
4687
|
+
const path21 = typeof o["path"] === "string" ? o["path"] : "";
|
|
4021
4688
|
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
4022
|
-
return bytes !== void 0 ? `${
|
|
4689
|
+
return bytes !== void 0 ? `${path21} ${bytes}B` : path21;
|
|
4023
4690
|
}
|
|
4024
4691
|
if (typeof o["count"] === "number") {
|
|
4025
4692
|
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
@@ -4615,7 +5282,7 @@ async function readKeyInput(deps, intent) {
|
|
|
4615
5282
|
async function loadProviders(deps) {
|
|
4616
5283
|
let raw;
|
|
4617
5284
|
try {
|
|
4618
|
-
raw = await
|
|
5285
|
+
raw = await fs3.readFile(deps.globalConfigPath, "utf8");
|
|
4619
5286
|
} catch {
|
|
4620
5287
|
return {};
|
|
4621
5288
|
}
|
|
@@ -4631,7 +5298,7 @@ async function loadProviders(deps) {
|
|
|
4631
5298
|
async function mutateProviders(deps, mutator) {
|
|
4632
5299
|
let raw;
|
|
4633
5300
|
try {
|
|
4634
|
-
raw = await
|
|
5301
|
+
raw = await fs3.readFile(deps.globalConfigPath, "utf8");
|
|
4635
5302
|
} catch {
|
|
4636
5303
|
raw = "{}";
|
|
4637
5304
|
}
|
|
@@ -4668,6 +5335,66 @@ var authCmd = async (args, deps) => {
|
|
|
4668
5335
|
envVars: flags.envVars
|
|
4669
5336
|
});
|
|
4670
5337
|
};
|
|
5338
|
+
|
|
5339
|
+
// src/subcommands/handlers/update.ts
|
|
5340
|
+
init_update_check();
|
|
5341
|
+
var updateCmd = async (args, deps) => {
|
|
5342
|
+
const cwd = deps.cwd;
|
|
5343
|
+
const checkOnly = args.includes("--check-only") || args.includes("-c");
|
|
5344
|
+
const info = await checkForUpdate();
|
|
5345
|
+
if (checkOnly) {
|
|
5346
|
+
if (info.outdated) {
|
|
5347
|
+
deps.renderer.write(`Update available: v${info.current} \u2192 v${info.latest}
|
|
5348
|
+
`);
|
|
5349
|
+
} else {
|
|
5350
|
+
deps.renderer.write(`You are on the latest version: v${info.current}
|
|
5351
|
+
`);
|
|
5352
|
+
}
|
|
5353
|
+
return 0;
|
|
5354
|
+
}
|
|
5355
|
+
if (!info.outdated) {
|
|
5356
|
+
deps.renderer.write(`You are already on the latest version: v${info.current}
|
|
5357
|
+
`);
|
|
5358
|
+
return 0;
|
|
5359
|
+
}
|
|
5360
|
+
deps.renderer.write(`Updating wrongstack from v${info.current} to v${info.latest}...
|
|
5361
|
+
`);
|
|
5362
|
+
try {
|
|
5363
|
+
const result = await new Promise((resolve4) => {
|
|
5364
|
+
const child = spawn("npm", ["install", "-g", "wrongstack@latest"], {
|
|
5365
|
+
cwd,
|
|
5366
|
+
stdio: "pipe"
|
|
5367
|
+
});
|
|
5368
|
+
let stderr = "";
|
|
5369
|
+
child.stderr?.on("data", (d) => {
|
|
5370
|
+
stderr += d;
|
|
5371
|
+
});
|
|
5372
|
+
child.on("close", (code) => resolve4({ code: code ?? 0 }));
|
|
5373
|
+
});
|
|
5374
|
+
if (result.code === 0) {
|
|
5375
|
+
deps.renderer.write(`
|
|
5376
|
+
Updated to v${info.latest}. Restart wrongstack to use the new version.
|
|
5377
|
+
`);
|
|
5378
|
+
} else {
|
|
5379
|
+
deps.renderer.write(`
|
|
5380
|
+
Update failed with exit code ${result.code}.
|
|
5381
|
+
`);
|
|
5382
|
+
}
|
|
5383
|
+
return result.code;
|
|
5384
|
+
} catch (err) {
|
|
5385
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5386
|
+
if (msg.includes("ENOENT")) {
|
|
5387
|
+
deps.renderer.write(`
|
|
5388
|
+
Update failed: npm not found in PATH.
|
|
5389
|
+
`);
|
|
5390
|
+
return 1;
|
|
5391
|
+
}
|
|
5392
|
+
deps.renderer.write(`
|
|
5393
|
+
Update failed: ${msg}
|
|
5394
|
+
`);
|
|
5395
|
+
return 1;
|
|
5396
|
+
}
|
|
5397
|
+
};
|
|
4671
5398
|
var req = createRequire(import.meta.url);
|
|
4672
5399
|
function readOwnVersion() {
|
|
4673
5400
|
const candidates = ["../package.json", "../../package.json"];
|
|
@@ -4703,7 +5430,7 @@ var diagCmd = async (_args, deps) => {
|
|
|
4703
5430
|
` modelsCache: ${deps.paths.modelsCache}`,
|
|
4704
5431
|
` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
|
|
4705
5432
|
` node: ${process.version}`,
|
|
4706
|
-
` os: ${
|
|
5433
|
+
` os: ${os6.platform()} ${os6.release()}`,
|
|
4707
5434
|
` provider: ${cfg.provider ?? "<unset>"}`,
|
|
4708
5435
|
` model: ${cfg.model ?? "<unset>"}`,
|
|
4709
5436
|
` tools: ${deps.toolRegistry?.list().length ?? 0}`,
|
|
@@ -4771,7 +5498,7 @@ var doctorCmd = async (_args, deps) => {
|
|
|
4771
5498
|
});
|
|
4772
5499
|
}
|
|
4773
5500
|
try {
|
|
4774
|
-
await
|
|
5501
|
+
await fs3.access(deps.paths.secretsKey);
|
|
4775
5502
|
checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
|
|
4776
5503
|
} catch {
|
|
4777
5504
|
checks.push({
|
|
@@ -4781,10 +5508,10 @@ var doctorCmd = async (_args, deps) => {
|
|
|
4781
5508
|
});
|
|
4782
5509
|
}
|
|
4783
5510
|
try {
|
|
4784
|
-
await
|
|
4785
|
-
const probe =
|
|
4786
|
-
await
|
|
4787
|
-
await
|
|
5511
|
+
await fs3.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
5512
|
+
const probe = path20.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
5513
|
+
await fs3.writeFile(probe, "");
|
|
5514
|
+
await fs3.unlink(probe);
|
|
4788
5515
|
checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
|
|
4789
5516
|
} catch (err) {
|
|
4790
5517
|
checks.push({
|
|
@@ -4885,8 +5612,8 @@ var exportCmd = async (args, deps) => {
|
|
|
4885
5612
|
return 1;
|
|
4886
5613
|
}
|
|
4887
5614
|
if (output) {
|
|
4888
|
-
await
|
|
4889
|
-
await
|
|
5615
|
+
await fs3.mkdir(path20.dirname(path20.resolve(deps.cwd, output)), { recursive: true });
|
|
5616
|
+
await fs3.writeFile(path20.resolve(deps.cwd, output), rendered, "utf8");
|
|
4890
5617
|
deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
|
|
4891
5618
|
`);
|
|
4892
5619
|
} else {
|
|
@@ -4943,17 +5670,17 @@ var initCmd = async (_args, deps) => {
|
|
|
4943
5670
|
} else {
|
|
4944
5671
|
deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
|
|
4945
5672
|
}
|
|
4946
|
-
await
|
|
5673
|
+
await fs3.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
4947
5674
|
const config = { version: 1, provider: providerId, model: modelId };
|
|
4948
5675
|
if (apiKey) config.apiKey = apiKey;
|
|
4949
|
-
const keyFile =
|
|
5676
|
+
const keyFile = path20.join(path20.dirname(deps.paths.globalConfig), ".key");
|
|
4950
5677
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
4951
5678
|
const encrypted = encryptConfigSecrets(config, vault);
|
|
4952
5679
|
await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
|
|
4953
|
-
await
|
|
4954
|
-
const agentsFile =
|
|
5680
|
+
await fs3.mkdir(path20.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
5681
|
+
const agentsFile = path20.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
4955
5682
|
try {
|
|
4956
|
-
await
|
|
5683
|
+
await fs3.access(agentsFile);
|
|
4957
5684
|
} catch {
|
|
4958
5685
|
const detected2 = await detectProjectFacts(deps.projectRoot);
|
|
4959
5686
|
await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
|
|
@@ -5029,7 +5756,7 @@ async function addMcpServer(args, deps) {
|
|
|
5029
5756
|
serverCfg.enabled = enable;
|
|
5030
5757
|
let existing = {};
|
|
5031
5758
|
try {
|
|
5032
|
-
existing = JSON.parse(await
|
|
5759
|
+
existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
|
|
5033
5760
|
} catch {
|
|
5034
5761
|
}
|
|
5035
5762
|
const mcpServers = existing.mcpServers ?? {};
|
|
@@ -5049,7 +5776,7 @@ async function addMcpServer(args, deps) {
|
|
|
5049
5776
|
async function removeMcpServer(name, deps) {
|
|
5050
5777
|
let existing = {};
|
|
5051
5778
|
try {
|
|
5052
|
-
existing = JSON.parse(await
|
|
5779
|
+
existing = JSON.parse(await fs3.readFile(deps.paths.globalConfig, "utf8"));
|
|
5053
5780
|
} catch {
|
|
5054
5781
|
deps.renderer.writeError("No config file found.\n");
|
|
5055
5782
|
return 1;
|
|
@@ -5170,7 +5897,7 @@ function renderConfiguredPlugins(config) {
|
|
|
5170
5897
|
}
|
|
5171
5898
|
async function readConfig(file) {
|
|
5172
5899
|
try {
|
|
5173
|
-
return JSON.parse(await
|
|
5900
|
+
return JSON.parse(await fs3.readFile(file, "utf8"));
|
|
5174
5901
|
} catch {
|
|
5175
5902
|
return {};
|
|
5176
5903
|
}
|
|
@@ -5261,9 +5988,9 @@ var usageCmd = async (_args, deps) => {
|
|
|
5261
5988
|
return 0;
|
|
5262
5989
|
};
|
|
5263
5990
|
var projectsCmd = async (_args, deps) => {
|
|
5264
|
-
const projectsRoot =
|
|
5991
|
+
const projectsRoot = path20.join(deps.paths.globalRoot, "projects");
|
|
5265
5992
|
try {
|
|
5266
|
-
const entries = await
|
|
5993
|
+
const entries = await fs3.readdir(projectsRoot);
|
|
5267
5994
|
if (entries.length === 0) {
|
|
5268
5995
|
deps.renderer.write("No projects tracked.\n");
|
|
5269
5996
|
return 0;
|
|
@@ -5271,7 +5998,7 @@ var projectsCmd = async (_args, deps) => {
|
|
|
5271
5998
|
for (const hash of entries) {
|
|
5272
5999
|
try {
|
|
5273
6000
|
const meta = JSON.parse(
|
|
5274
|
-
await
|
|
6001
|
+
await fs3.readFile(path20.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
5275
6002
|
);
|
|
5276
6003
|
deps.renderer.write(
|
|
5277
6004
|
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
@@ -5445,9 +6172,94 @@ var configCmd = async (args, deps) => {
|
|
|
5445
6172
|
`);
|
|
5446
6173
|
return 0;
|
|
5447
6174
|
}
|
|
5448
|
-
|
|
6175
|
+
if (sub === "history") {
|
|
6176
|
+
return runHistory(args.slice(1), deps);
|
|
6177
|
+
}
|
|
6178
|
+
if (sub === "restore") {
|
|
6179
|
+
return runRestore(args.slice(1), deps);
|
|
6180
|
+
}
|
|
6181
|
+
deps.renderer.writeError(`Unknown config subcommand: ${sub}
|
|
6182
|
+
`);
|
|
5449
6183
|
return 1;
|
|
5450
6184
|
};
|
|
6185
|
+
function extractArg(args, key) {
|
|
6186
|
+
const idx = args.indexOf(key);
|
|
6187
|
+
if (idx !== -1 && args[idx + 1] !== void 0) return args[idx + 1];
|
|
6188
|
+
const eq = key.startsWith("--") ? args.find((a) => a.startsWith(`${key}=`)) : null;
|
|
6189
|
+
if (eq) return eq.slice(eq.indexOf("=") + 1);
|
|
6190
|
+
return null;
|
|
6191
|
+
}
|
|
6192
|
+
async function runHistory(args, deps) {
|
|
6193
|
+
const idFlag = extractArg(args, "--id");
|
|
6194
|
+
if (idFlag) {
|
|
6195
|
+
const entry = await getHistoryEntry(idFlag);
|
|
6196
|
+
if (!entry) {
|
|
6197
|
+
deps.renderer.writeError(`History entry '${idFlag}' not found.
|
|
6198
|
+
`);
|
|
6199
|
+
return 1;
|
|
6200
|
+
}
|
|
6201
|
+
deps.renderer.write(
|
|
6202
|
+
[
|
|
6203
|
+
`ID: ${entry.id}`,
|
|
6204
|
+
`Time: ${new Date(entry.timestamp).toLocaleString()}`,
|
|
6205
|
+
`Change: ${entry.description}`,
|
|
6206
|
+
`Diff: ${entry.diffSummary}`,
|
|
6207
|
+
"",
|
|
6208
|
+
"Snapshot (secrets masked):",
|
|
6209
|
+
JSON.stringify(entry.snapshotMasked, null, 2)
|
|
6210
|
+
].join("\n") + "\n"
|
|
6211
|
+
);
|
|
6212
|
+
return 0;
|
|
6213
|
+
}
|
|
6214
|
+
const entries = await listHistory();
|
|
6215
|
+
if (entries.length === 0) {
|
|
6216
|
+
deps.renderer.write("No config history yet.\n");
|
|
6217
|
+
return 0;
|
|
6218
|
+
}
|
|
6219
|
+
deps.renderer.write(
|
|
6220
|
+
[
|
|
6221
|
+
color.bold("Config History"),
|
|
6222
|
+
"",
|
|
6223
|
+
...entries.map((e, i) => {
|
|
6224
|
+
const ts = new Date(e.timestamp).toLocaleString();
|
|
6225
|
+
const desc = e.description.length > 60 ? e.description.slice(0, 60) + "\u2026" : e.description;
|
|
6226
|
+
return ` [${i + 1}] ${e.id} ${color.dim(ts)}
|
|
6227
|
+
${desc}`;
|
|
6228
|
+
}),
|
|
6229
|
+
"",
|
|
6230
|
+
" Run `wrongstack config history --id <id>` for details.",
|
|
6231
|
+
" Run `wrongstack config restore <id>` to restore."
|
|
6232
|
+
].join("\n") + "\n"
|
|
6233
|
+
);
|
|
6234
|
+
return 0;
|
|
6235
|
+
}
|
|
6236
|
+
async function runRestore(args, deps) {
|
|
6237
|
+
const latest = args.includes("--latest") || args.includes("-l");
|
|
6238
|
+
const id = extractArg(args, "--id") ?? (args[0] && !args[0].startsWith("-") ? args[0] : null);
|
|
6239
|
+
if (latest) {
|
|
6240
|
+
const result2 = await restoreLast();
|
|
6241
|
+
if (!result2.ok) {
|
|
6242
|
+
deps.renderer.writeError(`Restore failed: ${result2.error}
|
|
6243
|
+
`);
|
|
6244
|
+
return 1;
|
|
6245
|
+
}
|
|
6246
|
+
deps.renderer.write("Restored from config.json.last.\n");
|
|
6247
|
+
return 0;
|
|
6248
|
+
}
|
|
6249
|
+
if (!id) {
|
|
6250
|
+
deps.renderer.write("Usage: wrongstack config restore <id> | --latest\n");
|
|
6251
|
+
return 1;
|
|
6252
|
+
}
|
|
6253
|
+
const result = await restoreFromHistory(id);
|
|
6254
|
+
if (!result.ok) {
|
|
6255
|
+
deps.renderer.writeError(`Restore failed: ${result.error}
|
|
6256
|
+
`);
|
|
6257
|
+
return 1;
|
|
6258
|
+
}
|
|
6259
|
+
deps.renderer.write(`Restored to history entry '${id}'. Backup created.
|
|
6260
|
+
`);
|
|
6261
|
+
return 0;
|
|
6262
|
+
}
|
|
5451
6263
|
function parseRewindFlags(args) {
|
|
5452
6264
|
const flags = {};
|
|
5453
6265
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -5463,7 +6275,7 @@ function parseRewindFlags(args) {
|
|
|
5463
6275
|
var rewindCmd = async (args, deps) => {
|
|
5464
6276
|
const flags = parseRewindFlags(args);
|
|
5465
6277
|
const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
|
|
5466
|
-
const sessionsDir =
|
|
6278
|
+
const sessionsDir = path20.join(wpaths.globalRoot, "sessions");
|
|
5467
6279
|
const rewind = new DefaultSessionRewinder(sessionsDir);
|
|
5468
6280
|
let sessionId = args.find((a) => !a.startsWith("--"));
|
|
5469
6281
|
if (!sessionId) {
|
|
@@ -5595,7 +6407,7 @@ var skillsCmd = async (_args, deps) => {
|
|
|
5595
6407
|
};
|
|
5596
6408
|
var versionCmd = async (_args, deps) => {
|
|
5597
6409
|
deps.renderer.write(
|
|
5598
|
-
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${
|
|
6410
|
+
`WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os6.platform()})
|
|
5599
6411
|
`
|
|
5600
6412
|
);
|
|
5601
6413
|
return 0;
|
|
@@ -5633,6 +6445,7 @@ var helpCmd = async (_args, deps) => {
|
|
|
5633
6445
|
var subcommands = {
|
|
5634
6446
|
init: initCmd,
|
|
5635
6447
|
auth: authCmd,
|
|
6448
|
+
update: updateCmd,
|
|
5636
6449
|
sessions: sessionsCmd,
|
|
5637
6450
|
config: configCmd,
|
|
5638
6451
|
rewind: rewindCmd,
|
|
@@ -5690,11 +6503,14 @@ function fmtTaskResultLine(r, color32) {
|
|
|
5690
6503
|
return { mark: color32.red("\u2717"), stats: `${color32.red("failed")} ${stats}`, tail: errSnip };
|
|
5691
6504
|
}
|
|
5692
6505
|
}
|
|
6506
|
+
|
|
6507
|
+
// src/boot.ts
|
|
6508
|
+
init_update_check();
|
|
5693
6509
|
function resolveBundledSkillsDir() {
|
|
5694
6510
|
try {
|
|
5695
6511
|
const req2 = createRequire(import.meta.url);
|
|
5696
6512
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
5697
|
-
return
|
|
6513
|
+
return path20.join(path20.dirname(corePkg), "skills");
|
|
5698
6514
|
} catch {
|
|
5699
6515
|
return void 0;
|
|
5700
6516
|
}
|
|
@@ -5723,6 +6539,13 @@ async function boot(argv) {
|
|
|
5723
6539
|
cacheFile: wpaths.modelsCache,
|
|
5724
6540
|
ttlSeconds: 24 * 3600
|
|
5725
6541
|
});
|
|
6542
|
+
let updateInfo;
|
|
6543
|
+
if (!flags["no-check"] && !process.env["WRONGSTACK_NO_CHECK"]) {
|
|
6544
|
+
checkForUpdate().then((info) => {
|
|
6545
|
+
updateInfo = info;
|
|
6546
|
+
}).catch(() => {
|
|
6547
|
+
});
|
|
6548
|
+
}
|
|
5726
6549
|
const first = positional[0];
|
|
5727
6550
|
if (first && subcommands[first]) {
|
|
5728
6551
|
const container = createDefaultContainer({
|
|
@@ -5831,7 +6654,8 @@ async function boot(argv) {
|
|
|
5831
6654
|
modelsRegistry,
|
|
5832
6655
|
renderer,
|
|
5833
6656
|
reader,
|
|
5834
|
-
logger
|
|
6657
|
+
logger,
|
|
6658
|
+
updateInfo
|
|
5835
6659
|
};
|
|
5836
6660
|
}
|
|
5837
6661
|
|
|
@@ -6412,7 +7236,7 @@ async function execute(deps) {
|
|
|
6412
7236
|
supportsVision,
|
|
6413
7237
|
attachments,
|
|
6414
7238
|
effectiveMaxContext,
|
|
6415
|
-
projectName:
|
|
7239
|
+
projectName: path20.basename(projectRoot) || void 0,
|
|
6416
7240
|
getAutonomy,
|
|
6417
7241
|
skillLoader
|
|
6418
7242
|
});
|
|
@@ -6430,7 +7254,7 @@ async function execute(deps) {
|
|
|
6430
7254
|
supportsVision,
|
|
6431
7255
|
attachments,
|
|
6432
7256
|
effectiveMaxContext,
|
|
6433
|
-
projectName:
|
|
7257
|
+
projectName: path20.basename(projectRoot) || void 0,
|
|
6434
7258
|
getAutonomy,
|
|
6435
7259
|
skillLoader
|
|
6436
7260
|
});
|
|
@@ -6702,7 +7526,7 @@ var MultiAgentHost = class {
|
|
|
6702
7526
|
model: opts?.model,
|
|
6703
7527
|
tools: opts?.tools
|
|
6704
7528
|
};
|
|
6705
|
-
const transcriptPath = this.sessionFactory ?
|
|
7529
|
+
const transcriptPath = this.sessionFactory ? path20.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
|
|
6706
7530
|
if (this.director) {
|
|
6707
7531
|
const subagentId = await this.director.spawn(subagentConfig);
|
|
6708
7532
|
const taskId2 = randomUUID();
|
|
@@ -6862,16 +7686,16 @@ var MultiAgentHost = class {
|
|
|
6862
7686
|
}
|
|
6863
7687
|
this.opts.directorMode = true;
|
|
6864
7688
|
if (this.opts.fleetRoot && !this.opts.manifestPath) {
|
|
6865
|
-
this.opts.manifestPath =
|
|
7689
|
+
this.opts.manifestPath = path20.join(this.opts.fleetRoot, "fleet.json");
|
|
6866
7690
|
}
|
|
6867
7691
|
if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
|
|
6868
|
-
this.opts.sharedScratchpadPath =
|
|
7692
|
+
this.opts.sharedScratchpadPath = path20.join(this.opts.fleetRoot, "shared");
|
|
6869
7693
|
}
|
|
6870
7694
|
if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
|
|
6871
|
-
this.opts.sessionsRoot =
|
|
7695
|
+
this.opts.sessionsRoot = path20.join(this.opts.fleetRoot, "subagents");
|
|
6872
7696
|
}
|
|
6873
7697
|
if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
|
|
6874
|
-
this.opts.stateCheckpointPath =
|
|
7698
|
+
this.opts.stateCheckpointPath = path20.join(this.opts.fleetRoot, "director-state.json");
|
|
6875
7699
|
}
|
|
6876
7700
|
await this.ensureDirector();
|
|
6877
7701
|
return this.director ?? null;
|
|
@@ -6992,11 +7816,11 @@ var SessionStats = class {
|
|
|
6992
7816
|
if (e.name === "bash") this.bashCommands++;
|
|
6993
7817
|
else if (e.name === "fetch") this.fetches++;
|
|
6994
7818
|
if (!e.ok) return;
|
|
6995
|
-
const
|
|
6996
|
-
if (e.name === "read" &&
|
|
6997
|
-
else if (e.name === "edit" &&
|
|
6998
|
-
else if (e.name === "write" &&
|
|
6999
|
-
this.writtenPaths.add(
|
|
7819
|
+
const path21 = typeof input?.path === "string" ? input.path : void 0;
|
|
7820
|
+
if (e.name === "read" && path21) this.readPaths.add(path21);
|
|
7821
|
+
else if (e.name === "edit" && path21) this.editedPaths.add(path21);
|
|
7822
|
+
else if (e.name === "write" && path21) {
|
|
7823
|
+
this.writtenPaths.add(path21);
|
|
7000
7824
|
const content = typeof input?.content === "string" ? input.content : "";
|
|
7001
7825
|
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
7002
7826
|
}
|
|
@@ -7327,12 +8151,12 @@ async function setupSession(params) {
|
|
|
7327
8151
|
}
|
|
7328
8152
|
const sessionRef = { current: session };
|
|
7329
8153
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
7330
|
-
const attachments = new DefaultAttachmentStore({ spoolDir:
|
|
7331
|
-
const queueStore = new QueueStore({ dir:
|
|
8154
|
+
const attachments = new DefaultAttachmentStore({ spoolDir: path20.join(wpaths.projectSessions, session.id, "attachments") });
|
|
8155
|
+
const queueStore = new QueueStore({ dir: path20.join(wpaths.projectSessions, session.id) });
|
|
7332
8156
|
const ctxSignal = new AbortController().signal;
|
|
7333
8157
|
const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
|
|
7334
8158
|
if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
|
|
7335
|
-
const todosCheckpointPath =
|
|
8159
|
+
const todosCheckpointPath = path20.join(wpaths.projectSessions, `${session.id}.todos.json`);
|
|
7336
8160
|
if (resumeId) {
|
|
7337
8161
|
try {
|
|
7338
8162
|
const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
|
|
@@ -7344,12 +8168,12 @@ async function setupSession(params) {
|
|
|
7344
8168
|
}
|
|
7345
8169
|
}
|
|
7346
8170
|
const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
|
|
7347
|
-
const planPath =
|
|
8171
|
+
const planPath = path20.join(wpaths.projectSessions, `${session.id}.plan.json`);
|
|
7348
8172
|
context.state.setMeta("plan.path", planPath);
|
|
7349
8173
|
if (resumeId) {
|
|
7350
8174
|
try {
|
|
7351
|
-
const fleetRoot =
|
|
7352
|
-
const dirState = await loadDirectorState(
|
|
8175
|
+
const fleetRoot = path20.join(wpaths.projectSessions, session.id);
|
|
8176
|
+
const dirState = await loadDirectorState(path20.join(fleetRoot, "director-state.json"));
|
|
7353
8177
|
if (dirState) {
|
|
7354
8178
|
const tCounts = {};
|
|
7355
8179
|
for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
|
|
@@ -7376,7 +8200,7 @@ function resolveBundledSkillsDir2() {
|
|
|
7376
8200
|
try {
|
|
7377
8201
|
const req2 = createRequire(import.meta.url);
|
|
7378
8202
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
7379
|
-
return
|
|
8203
|
+
return path20.join(path20.dirname(corePkg), "skills");
|
|
7380
8204
|
} catch {
|
|
7381
8205
|
return void 0;
|
|
7382
8206
|
}
|
|
@@ -7407,8 +8231,28 @@ async function main(argv) {
|
|
|
7407
8231
|
modelsRegistry,
|
|
7408
8232
|
renderer,
|
|
7409
8233
|
reader,
|
|
7410
|
-
logger
|
|
8234
|
+
logger,
|
|
8235
|
+
updateInfo
|
|
7411
8236
|
} = ctx;
|
|
8237
|
+
if (!updateInfo?.outdated) {
|
|
8238
|
+
const ac = new AbortController();
|
|
8239
|
+
const timer = setTimeout(() => ac.abort(), 2e3);
|
|
8240
|
+
try {
|
|
8241
|
+
const { checkForUpdate: checkForUpdate2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
|
|
8242
|
+
updateInfo = await checkForUpdate2(ac.signal);
|
|
8243
|
+
} catch {
|
|
8244
|
+
} finally {
|
|
8245
|
+
clearTimeout(timer);
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
if (updateInfo?.outdated) {
|
|
8249
|
+
process.stderr.write(
|
|
8250
|
+
`
|
|
8251
|
+
\x1B[33m\u2191 Update available: v${updateInfo.current} \u2192 v${updateInfo.latest}\x1B[0m Run \`wrongstack update\` to upgrade.
|
|
8252
|
+
|
|
8253
|
+
`
|
|
8254
|
+
);
|
|
8255
|
+
}
|
|
7412
8256
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
7413
8257
|
const container = createDefaultContainer({
|
|
7414
8258
|
config,
|
|
@@ -7463,7 +8307,7 @@ async function main(argv) {
|
|
|
7463
8307
|
modeId,
|
|
7464
8308
|
modePrompt,
|
|
7465
8309
|
modelCapabilities,
|
|
7466
|
-
planPath: () => sessionRef.current ?
|
|
8310
|
+
planPath: () => sessionRef.current ? path20.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
|
|
7467
8311
|
})
|
|
7468
8312
|
);
|
|
7469
8313
|
const toolRegistry = new ToolRegistry();
|
|
@@ -7491,7 +8335,7 @@ async function main(argv) {
|
|
|
7491
8335
|
name: "session-store",
|
|
7492
8336
|
check: async () => {
|
|
7493
8337
|
try {
|
|
7494
|
-
await
|
|
8338
|
+
await fs3.access(wpaths.projectSessions);
|
|
7495
8339
|
return { status: "healthy" };
|
|
7496
8340
|
} catch (e) {
|
|
7497
8341
|
return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
|
|
@@ -7508,7 +8352,7 @@ async function main(argv) {
|
|
|
7508
8352
|
const dumpMetrics = () => {
|
|
7509
8353
|
if (!metricsSink) return;
|
|
7510
8354
|
try {
|
|
7511
|
-
const out =
|
|
8355
|
+
const out = path20.join(wpaths.projectSessions, "metrics.json");
|
|
7512
8356
|
const snap = metricsSink.snapshot();
|
|
7513
8357
|
writeFileSync(out, JSON.stringify(snap, null, 2));
|
|
7514
8358
|
} catch {
|
|
@@ -7727,12 +8571,12 @@ async function main(argv) {
|
|
|
7727
8571
|
const directorMode = flags["director"] === true;
|
|
7728
8572
|
let director = null;
|
|
7729
8573
|
let autonomyMode = "off";
|
|
7730
|
-
const fleetRoot = directorMode ?
|
|
7731
|
-
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] :
|
|
7732
|
-
const sharedScratchpadPath = directorMode ?
|
|
7733
|
-
const subagentSessionsRoot = directorMode ?
|
|
7734
|
-
const stateCheckpointPath = directorMode ?
|
|
7735
|
-
const fleetRootForPromotion =
|
|
8574
|
+
const fleetRoot = directorMode ? path20.join(wpaths.projectSessions, session.id) : void 0;
|
|
8575
|
+
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path20.join(fleetRoot, "fleet.json") : void 0;
|
|
8576
|
+
const sharedScratchpadPath = directorMode ? path20.join(fleetRoot, "shared") : void 0;
|
|
8577
|
+
const subagentSessionsRoot = directorMode ? path20.join(fleetRoot, "subagents") : void 0;
|
|
8578
|
+
const stateCheckpointPath = directorMode ? path20.join(fleetRoot, "director-state.json") : void 0;
|
|
8579
|
+
const fleetRootForPromotion = path20.join(wpaths.projectSessions, session.id);
|
|
7736
8580
|
const multiAgentHost = new MultiAgentHost(
|
|
7737
8581
|
{
|
|
7738
8582
|
container,
|
|
@@ -7800,11 +8644,15 @@ async function main(argv) {
|
|
|
7800
8644
|
renderer,
|
|
7801
8645
|
memoryStore,
|
|
7802
8646
|
context,
|
|
8647
|
+
cwd,
|
|
8648
|
+
projectRoot,
|
|
7803
8649
|
metricsSink,
|
|
7804
8650
|
healthRegistry,
|
|
7805
8651
|
planPath,
|
|
7806
8652
|
modeStore,
|
|
7807
8653
|
fleetStreamController,
|
|
8654
|
+
llmProvider: provider,
|
|
8655
|
+
llmModel: config.model,
|
|
7808
8656
|
onSpawn: async (description, spawnOpts) => {
|
|
7809
8657
|
const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
|
|
7810
8658
|
const tags = [];
|
|
@@ -7911,27 +8759,27 @@ async function main(argv) {
|
|
|
7911
8759
|
return `Unknown fleet action: ${action}`;
|
|
7912
8760
|
},
|
|
7913
8761
|
onFleetLog: async (subagentId, mode) => {
|
|
7914
|
-
const subagentsRoot =
|
|
8762
|
+
const subagentsRoot = path20.join(fleetRootForPromotion, "subagents");
|
|
7915
8763
|
let runDirs;
|
|
7916
8764
|
try {
|
|
7917
|
-
runDirs = await
|
|
8765
|
+
runDirs = await fs3.readdir(subagentsRoot);
|
|
7918
8766
|
} catch {
|
|
7919
8767
|
return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
|
|
7920
8768
|
}
|
|
7921
8769
|
const found = [];
|
|
7922
8770
|
for (const runId of runDirs) {
|
|
7923
|
-
const runDir =
|
|
8771
|
+
const runDir = path20.join(subagentsRoot, runId);
|
|
7924
8772
|
let files;
|
|
7925
8773
|
try {
|
|
7926
|
-
files = await
|
|
8774
|
+
files = await fs3.readdir(runDir);
|
|
7927
8775
|
} catch {
|
|
7928
8776
|
continue;
|
|
7929
8777
|
}
|
|
7930
8778
|
for (const f of files) {
|
|
7931
8779
|
if (!f.endsWith(".jsonl")) continue;
|
|
7932
|
-
const full =
|
|
8780
|
+
const full = path20.join(runDir, f);
|
|
7933
8781
|
try {
|
|
7934
|
-
const stat2 = await
|
|
8782
|
+
const stat2 = await fs3.stat(full);
|
|
7935
8783
|
found.push({
|
|
7936
8784
|
runId,
|
|
7937
8785
|
subagentId: f.replace(/\.jsonl$/, ""),
|
|
@@ -7970,7 +8818,7 @@ async function main(argv) {
|
|
|
7970
8818
|
].join("\n");
|
|
7971
8819
|
}
|
|
7972
8820
|
const t = matches[0];
|
|
7973
|
-
const raw = await
|
|
8821
|
+
const raw = await fs3.readFile(t.file, "utf8");
|
|
7974
8822
|
if (mode === "raw") return raw;
|
|
7975
8823
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
7976
8824
|
const counts = {};
|
|
@@ -8026,7 +8874,7 @@ async function main(argv) {
|
|
|
8026
8874
|
}
|
|
8027
8875
|
const dir = await multiAgentHost.ensureDirector();
|
|
8028
8876
|
if (!dir) return "Director is not available.";
|
|
8029
|
-
const dirStatePath =
|
|
8877
|
+
const dirStatePath = path20.join(fleetRootForPromotion, "director-state.json");
|
|
8030
8878
|
const prior = await loadDirectorState(dirStatePath);
|
|
8031
8879
|
if (!prior) {
|
|
8032
8880
|
return "No prior director-state.json found \u2014 nothing to retry.";
|
|
@@ -8097,9 +8945,9 @@ async function main(argv) {
|
|
|
8097
8945
|
for (const tool of director2.tools(FLEET_ROSTER)) {
|
|
8098
8946
|
toolRegistry.register(tool);
|
|
8099
8947
|
}
|
|
8100
|
-
const mp =
|
|
8101
|
-
const sp =
|
|
8102
|
-
const ss =
|
|
8948
|
+
const mp = path20.join(fleetRootForPromotion, "fleet.json");
|
|
8949
|
+
const sp = path20.join(fleetRootForPromotion, "shared");
|
|
8950
|
+
const ss = path20.join(fleetRootForPromotion, "subagents");
|
|
8103
8951
|
const lines = [
|
|
8104
8952
|
`${color.green("\u2713")} Promoted to director mode.`,
|
|
8105
8953
|
` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
|
|
@@ -8146,13 +8994,13 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
8146
8994
|
void mcpRegistry.stopAll();
|
|
8147
8995
|
},
|
|
8148
8996
|
onBeforeExit: async () => {
|
|
8149
|
-
const { spawn:
|
|
8997
|
+
const { spawn: spawn3 } = await import('child_process');
|
|
8150
8998
|
const cwd2 = projectRoot;
|
|
8151
|
-
const statusResult = await new Promise((
|
|
8152
|
-
const child =
|
|
8999
|
+
const statusResult = await new Promise((resolve4) => {
|
|
9000
|
+
const child = spawn3("git", ["status", "--porcelain"], { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
|
|
8153
9001
|
let stdout = "";
|
|
8154
9002
|
child.stdout?.on("data", (d) => stdout += d);
|
|
8155
|
-
child.on("close", (code) =>
|
|
9003
|
+
child.on("close", (code) => resolve4({ stdout, code: code ?? 0 }));
|
|
8156
9004
|
});
|
|
8157
9005
|
if (statusResult.stdout.trim().length > 0) {
|
|
8158
9006
|
const lines = statusResult.stdout.split("\n").filter(Boolean);
|
|
@@ -8190,7 +9038,13 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
8190
9038
|
...errSection
|
|
8191
9039
|
].join("\n");
|
|
8192
9040
|
},
|
|
8193
|
-
onStats: () => stats.format()
|
|
9041
|
+
onStats: () => stats.format(),
|
|
9042
|
+
generateCommitMessage: async (diff) => {
|
|
9043
|
+
return generateCommitMessageWithLLM(diff, {
|
|
9044
|
+
provider: context.provider,
|
|
9045
|
+
model: context.model
|
|
9046
|
+
});
|
|
9047
|
+
}
|
|
8194
9048
|
});
|
|
8195
9049
|
for (const cmd of slashCmds) slashRegistry.register(cmd);
|
|
8196
9050
|
const savedProviderCfg = config.providers?.[config.provider];
|