@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 CHANGED
@@ -1,18 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import * as path18 from 'path';
3
- 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, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
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
- if (!activeBuilder) return null;
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
- if (!activeBuilder) return null;
65
- return activeBuilder.getPhase();
81
+ return sddState.getPhase();
66
82
  }
67
83
  async function trySaveSpecFromAIOutput(aiOutput) {
68
- if (!activeBuilder) return false;
69
- const spec = activeBuilder.tryParseSpecFromOutput(aiOutput);
84
+ const builder = sddState.getBuilder();
85
+ if (!builder) return false;
86
+ const spec = builder.tryParseSpecFromOutput(aiOutput);
70
87
  if (!spec) return false;
71
- activeBuilder.setSpec(spec);
88
+ builder.setSpec(spec);
72
89
  return true;
73
90
  }
74
91
  async function trySaveTasksFromAIOutput(aiOutput) {
75
- if (!activeBuilder) return false;
76
- const session = activeBuilder.getSession();
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 = activeBuilder.extractJSONArray(aiOutput);
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
- activeTaskStore = store;
110
- activeTaskTracker = tracker;
111
- activeTaskGraphId = graph.id;
112
- activeBuilder.setTaskGraphId(graph.id);
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
- if (!activeTaskTracker) return null;
117
- const progress = activeTaskTracker.getProgress();
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
- if (!activeTaskTracker) return null;
127
- const nodes = activeTaskTracker.getAllNodes();
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
- if (!activeTaskTracker) return false;
137
- const nodes = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
163
+ tracker.updateNodeStatus(match.id, "completed");
143
164
  return true;
144
165
  }
145
166
  function autoDetectTaskCompletion(aiOutput) {
146
- if (!activeTaskTracker) return 0;
147
- const pending = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
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
- activeTaskTracker.updateNodeStatus(node.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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
- activeTaskTracker.updateNodeStatus(node.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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
- if (!activeBuilder) return false;
214
- const session = activeBuilder.getSession();
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
- activeBuilder.setImplementation(plan);
243
+ builder.setImplementation(plan);
221
244
  return true;
222
245
  }
223
246
  }
224
247
  if (aiOutput.length > 100 && !aiOutput.includes("```json")) {
225
- activeBuilder.setImplementation(aiOutput.trim());
248
+ builder.setImplementation(aiOutput.trim());
226
249
  return true;
227
250
  }
228
251
  return false;
229
252
  }
230
253
  function getActiveBuilder() {
231
- return activeBuilder;
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 = path18.join(projectRoot, ".wrongstack", "specs");
241
- const graphsDir = path18.join(projectRoot, ".wrongstack", "task-graphs");
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 (!activeBuilder && !forceFlag) {
257
- const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
280
+ if (!sddState.getBuilder() && !forceFlag) {
281
+ const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
258
282
  try {
259
- const fsp = await import('fs/promises');
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
- activeTaskStore = null;
287
- activeTaskTracker = null;
288
- activeTaskGraphId = null;
309
+ sddState.clearTaskState();
289
310
  const projectContext = await gatherProjectContext(projectRoot);
290
- activeBuilder = new AISpecBuilder({
311
+ sddState.setBuilder(new AISpecBuilder({
291
312
  store: specStore,
292
313
  projectContext,
293
314
  minQuestions: 2,
294
315
  maxQuestions: 10,
295
- sessionPath: path18.join(projectRoot, ".wrongstack", "sdd-session.json")
296
- });
297
- activeBuilder.startSession(title);
298
- const aiPrompt = activeBuilder.getAIPrompt();
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
- if (!activeBuilder) {
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 = activeBuilder.getSession().phase;
350
+ const phase = builder.getSession().phase;
328
351
  if (phase === "questioning") {
329
- const sddCtx = activeBuilder.getAIPrompt();
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 = activeBuilder.getSession().spec;
364
+ const spec = builder.getSession().spec;
342
365
  if (!spec) {
343
366
  return { message: "No spec to approve." };
344
367
  }
345
- await activeBuilder.saveSpec();
368
+ await builder.saveSpec();
346
369
  versioning.recordVersion(spec, "Initial spec approved");
347
- activeBuilder.approve();
348
- const implPrompt = activeBuilder.getAIPrompt();
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
- activeBuilder.approve();
367
- const execPrompt = activeBuilder.getAIPrompt();
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
- if (!activeBuilder) {
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 = activeBuilder.getSession();
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 = activeBuilder.getAIPrompt();
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
- if (!activeBuilder) {
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 session = activeBuilder.getSession();
413
- if (!session.implementation) {
437
+ const planSession = planBuilder.getSession();
438
+ if (!planSession.implementation) {
414
439
  return {
415
- message: session.phase === "implementation" ? "No implementation plan yet. The AI will generate it after /sdd approve." : "No implementation plan in this session."
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
- session.implementation
447
+ planSession.implementation
423
448
  ].join("\n")
424
449
  };
425
450
  }
426
451
  case "spec": {
427
- if (!activeBuilder) {
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 session = activeBuilder.getSession();
431
- if (!session.spec) {
456
+ const specSession = specBuilder.getSession();
457
+ if (!specSession.spec) {
432
458
  return {
433
- message: session.phase === "questioning" ? "No spec generated yet. Keep answering the AI's questions." : "No spec in this session."
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 = session.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
- if (!activeTaskTracker) {
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 = activeTaskTracker.getAllNodes();
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 = activeTaskTracker.getProgress();
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
- if (!activeTaskTracker) {
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 = activeTaskTracker.getAllNodes({ status: ["pending", "in_progress"] });
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
- activeTaskTracker.updateNodeStatus(node.id, "completed");
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
- activeTaskTracker.updateNodeStatus(match.id, "completed");
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 = activeTaskTracker.getProgress();
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
- if (!activeBuilder) {
545
+ const statusBuilder = sddState.getBuilder();
546
+ if (!statusBuilder) {
518
547
  return { message: "No active SDD session." };
519
548
  }
520
- const session = activeBuilder.getSession();
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 = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
584
+ const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
556
585
  let deletedFromDisk = false;
557
586
  try {
558
- const fsp = await import('fs/promises');
559
- await fsp.unlink(sessionPath);
587
+ await fs3.unlink(sessionPath);
560
588
  deletedFromDisk = true;
561
589
  } catch {
562
590
  }
563
- if (activeBuilder) {
564
- const title = activeBuilder.getSession().title;
565
- await activeBuilder.deleteSession();
566
- activeBuilder = null;
567
- activeTaskStore = null;
568
- activeTaskTracker = null;
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 (activeBuilder) {
605
+ if (sddState.getBuilder()) {
579
606
  return { message: "An SDD session is already active. Use /sdd cancel first." };
580
607
  }
581
- const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
608
+ const sessionPath = path20.join(projectRoot, ".wrongstack", "sdd-session.json");
582
609
  const projectContext = await gatherProjectContext(projectRoot);
583
- activeBuilder = new AISpecBuilder({
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 loaded = await activeBuilder.loadSession();
616
+ }));
617
+ const resumeBuilder = sddState.getBuilder();
618
+ const loaded = await resumeBuilder.loadSession();
591
619
  if (!loaded) {
592
- activeBuilder = null;
620
+ sddState.setBuilder(null);
593
621
  return { message: "No saved SDD session found. Use /sdd new to start one." };
594
622
  }
595
- const session = activeBuilder.getSession();
623
+ const session = resumeBuilder.getSession();
596
624
  let taskCount = 0;
597
625
  let completedCount = 0;
598
- const taskGraphId = activeBuilder.getTaskGraphId();
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
- activeTaskStore = store;
606
- activeTaskTracker = tracker;
607
- activeTaskGraphId = taskGraphId;
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 = activeBuilder.getAIPrompt();
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 fsp = await import('fs/promises');
807
- const pkgPath = path18.join(projectRoot, "package.json");
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 fsp = await import('fs/promises');
824
- const tsconfigPath = path18.join(projectRoot, "tsconfig.json");
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 fsp = await import('fs/promises');
831
- const srcDir = path18.join(projectRoot, "src");
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 activeBuilder, activeTaskStore, activeTaskTracker, activeTaskGraphId;
877
+ var SDD_META_KEY, SDDState, sddState;
853
878
  var init_sdd = __esm({
854
879
  "src/slash-commands/sdd.ts"() {
855
- activeBuilder = null;
856
- activeTaskStore = null;
857
- activeTaskTracker = null;
858
- activeTaskGraphId = null;
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((resolve3) => {
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
- resolve3();
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 fs5.readFile(opts.globalConfigPath, "utf8");
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 = path18.join(path18.dirname(opts.globalConfigPath), ".key");
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 fs5.readFile(opts.globalConfigPath, "utf8");
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 = path18.join(path18.dirname(opts.globalConfigPath), ".key");
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" ? path18.resolve(flags["cwd"]) : process.cwd();
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 = os4.homedir();
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 fs5.mkdir(paths.projectDir, { recursive: true });
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 fs5.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
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 ?? path18.join(os4.homedir(), ".wrongstack", "history");
1857
+ this.historyFile = opts.historyFile ?? path20.join(os6.homedir(), ".wrongstack", "history");
1635
1858
  }
1636
1859
  async loadHistory() {
1637
1860
  try {
1638
- const raw = await fs5.readFile(this.historyFile, "utf8");
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 fs5.mkdir(path18.dirname(this.historyFile), { recursive: true });
1647
- await fs5.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
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((resolve3) => setTimeout(resolve3, 50));
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((resolve3) => {
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
- resolve3(line);
1904
+ resolve4(line);
1682
1905
  });
1683
- fresh.once("close", () => resolve3(""));
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((resolve3) => {
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
- resolve3("");
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
- resolve3(opt.value);
1939
+ resolve4(opt.value);
1717
1940
  }
1718
1941
  };
1719
1942
  const onClose = () => {
1720
1943
  cleanup();
1721
- resolve3("");
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((resolve3) => {
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
- resolve3(buf);
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 fs5.access(file);
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(path18.join(root, "pnpm-lock.yaml"))) return "pnpm";
2129
- if (await pathExists(path18.join(root, "bun.lockb"))) return "bun";
2130
- if (await pathExists(path18.join(root, "bun.lock"))) return "bun";
2131
- if (await pathExists(path18.join(root, "yarn.lock"))) return "yarn";
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 fs5.readFile(path18.join(root, "package.json"), "utf8"));
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(path18.join(root, "pyproject.toml"))) throw new Error("not python");
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(path18.join(root, "go.mod"))) throw new Error("not go");
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(path18.join(root, "Cargo.toml"))) throw new Error("not rust");
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 fs5.readFile(path18.join(root, "Makefile"), "utf8");
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((resolve3) => {
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) => resolve3({ stdout, stderr, code: code ?? 0 }));
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 generateCommitMessage(cwd) {
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
- const message = await generateCommitMessage(cwd);
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 = path18.join(ctx.projectRoot, ".wrongstack");
2823
- const file = path18.join(dir, "AGENTS.md");
3269
+ const dir = path20.join(ctx.projectRoot, ".wrongstack");
3270
+ const file = path20.join(dir, "AGENTS.md");
2824
3271
  try {
2825
- await fs5.access(file);
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 fs5.mkdir(dir, { recursive: true });
2836
- await fs5.writeFile(file, body, "utf8");
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 = path18.join(os4.homedir(), ".wrongstack");
4110
+ const globalRoot = path20.join(os6.homedir(), ".wrongstack");
3445
4111
  return new SkillInstaller({
3446
- manifestPath: path18.join(globalRoot, "installed-skills.json"),
3447
- projectSkillsDir: path18.join(projectRoot, ".wrongstack", "skills"),
3448
- globalSkillsDir: path18.join(globalRoot, "skills"),
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 fs5.access(path18.join(projectRoot, ".wrongstack", "AGENTS.md"));
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 fs5.access(path18.join(projectRoot, m));
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 = path18.join(projectRoot, ".wrongstack");
3674
- const file = path18.join(dir, "AGENTS.md");
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 fs5.mkdir(dir, { recursive: true });
3678
- await fs5.writeFile(file, body, "utf8");
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(`(${path18.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
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 = path18.join(projectRoot, ".git");
4381
+ const gitDir = path20.join(projectRoot, ".git");
3715
4382
  let hasGit = false;
3716
4383
  try {
3717
- await fs5.access(gitDir);
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: spawn2 } = await import('child_process');
3733
- await new Promise((resolve3, reject) => {
3734
- const child = spawn2("git", ["init"], { cwd: projectRoot });
3735
- child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`)));
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 path19 = typeof o["path"] === "string" ? o["path"] : "";
4682
+ const path21 = typeof o["path"] === "string" ? o["path"] : "";
4016
4683
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
4017
- return `${path19} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
4684
+ return `${path21} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
4018
4685
  }
4019
4686
  if (name === "write") {
4020
- const path19 = typeof o["path"] === "string" ? o["path"] : "";
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 ? `${path19} ${bytes}B` : path19;
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 fs5.readFile(deps.globalConfigPath, "utf8");
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 fs5.readFile(deps.globalConfigPath, "utf8");
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: ${os4.platform()} ${os4.release()}`,
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 fs5.access(deps.paths.secretsKey);
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 fs5.mkdir(deps.paths.projectSessions, { recursive: true });
4785
- const probe = path18.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
4786
- await fs5.writeFile(probe, "");
4787
- await fs5.unlink(probe);
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 fs5.mkdir(path18.dirname(path18.resolve(deps.cwd, output)), { recursive: true });
4889
- await fs5.writeFile(path18.resolve(deps.cwd, output), rendered, "utf8");
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 fs5.mkdir(deps.paths.globalRoot, { recursive: true });
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 = path18.join(path18.dirname(deps.paths.globalConfig), ".key");
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 fs5.mkdir(path18.join(deps.projectRoot, ".wrongstack"), { recursive: true });
4954
- const agentsFile = path18.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
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 fs5.access(agentsFile);
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 fs5.readFile(deps.paths.globalConfig, "utf8"));
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 fs5.readFile(deps.paths.globalConfig, "utf8"));
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 fs5.readFile(file, "utf8"));
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 = path18.join(deps.paths.globalRoot, "projects");
5991
+ const projectsRoot = path20.join(deps.paths.globalRoot, "projects");
5265
5992
  try {
5266
- const entries = await fs5.readdir(projectsRoot);
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 fs5.readFile(path18.join(projectsRoot, hash, "meta.json"), "utf8")
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
- deps.renderer.writeError(`Unknown config subcommand: ${sub}`);
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 = path18.join(wpaths.globalRoot, "sessions");
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}, ${os4.platform()})
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 path18.join(path18.dirname(corePkg), "skills");
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: path18.basename(projectRoot) || void 0,
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: path18.basename(projectRoot) || void 0,
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 ? path18.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
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 = path18.join(this.opts.fleetRoot, "fleet.json");
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 = path18.join(this.opts.fleetRoot, "shared");
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 = path18.join(this.opts.fleetRoot, "subagents");
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 = path18.join(this.opts.fleetRoot, "director-state.json");
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 path19 = typeof input?.path === "string" ? input.path : void 0;
6996
- if (e.name === "read" && path19) this.readPaths.add(path19);
6997
- else if (e.name === "edit" && path19) this.editedPaths.add(path19);
6998
- else if (e.name === "write" && path19) {
6999
- this.writtenPaths.add(path19);
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: path18.join(wpaths.projectSessions, session.id, "attachments") });
7331
- const queueStore = new QueueStore({ dir: path18.join(wpaths.projectSessions, session.id) });
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 = path18.join(wpaths.projectSessions, `${session.id}.todos.json`);
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 = path18.join(wpaths.projectSessions, `${session.id}.plan.json`);
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 = path18.join(wpaths.projectSessions, session.id);
7352
- const dirState = await loadDirectorState(path18.join(fleetRoot, "director-state.json"));
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 path18.join(path18.dirname(corePkg), "skills");
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 ? path18.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
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 fs5.access(wpaths.projectSessions);
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 = path18.join(wpaths.projectSessions, "metrics.json");
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 ? path18.join(wpaths.projectSessions, session.id) : void 0;
7731
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path18.join(fleetRoot, "fleet.json") : void 0;
7732
- const sharedScratchpadPath = directorMode ? path18.join(fleetRoot, "shared") : void 0;
7733
- const subagentSessionsRoot = directorMode ? path18.join(fleetRoot, "subagents") : void 0;
7734
- const stateCheckpointPath = directorMode ? path18.join(fleetRoot, "director-state.json") : void 0;
7735
- const fleetRootForPromotion = path18.join(wpaths.projectSessions, session.id);
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 = path18.join(fleetRootForPromotion, "subagents");
8762
+ const subagentsRoot = path20.join(fleetRootForPromotion, "subagents");
7915
8763
  let runDirs;
7916
8764
  try {
7917
- runDirs = await fs5.readdir(subagentsRoot);
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 = path18.join(subagentsRoot, runId);
8771
+ const runDir = path20.join(subagentsRoot, runId);
7924
8772
  let files;
7925
8773
  try {
7926
- files = await fs5.readdir(runDir);
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 = path18.join(runDir, f);
8780
+ const full = path20.join(runDir, f);
7933
8781
  try {
7934
- const stat2 = await fs5.stat(full);
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 fs5.readFile(t.file, "utf8");
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 = path18.join(fleetRootForPromotion, "director-state.json");
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 = path18.join(fleetRootForPromotion, "fleet.json");
8101
- const sp = path18.join(fleetRootForPromotion, "shared");
8102
- const ss = path18.join(fleetRootForPromotion, "subagents");
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: spawn2 } = await import('child_process');
8997
+ const { spawn: spawn3 } = await import('child_process');
8150
8998
  const cwd2 = projectRoot;
8151
- const statusResult = await new Promise((resolve3) => {
8152
- const child = spawn2("git", ["status", "--porcelain"], { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
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) => resolve3({ stdout, code: code ?? 0 }));
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];