@wrongstack/tools 0.264.0 → 0.265.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/audit.js +154 -11
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +138 -2
  4. package/dist/bash.js.map +1 -1
  5. package/dist/batch-tool-use.js +1 -0
  6. package/dist/batch-tool-use.js.map +1 -1
  7. package/dist/builtin.d.ts +20 -1
  8. package/dist/builtin.js +661 -325
  9. package/dist/builtin.js.map +1 -1
  10. package/dist/circuit-breaker.d.ts +20 -0
  11. package/dist/circuit-breaker.js +40 -2
  12. package/dist/circuit-breaker.js.map +1 -1
  13. package/dist/codebase-index/index.d.ts +16 -0
  14. package/dist/codebase-index/index.js +59 -25
  15. package/dist/codebase-index/index.js.map +1 -1
  16. package/dist/codebase-index/worker.js +56 -25
  17. package/dist/codebase-index/worker.js.map +1 -1
  18. package/dist/diff.js +14 -7
  19. package/dist/diff.js.map +1 -1
  20. package/dist/document.js +14 -8
  21. package/dist/document.js.map +1 -1
  22. package/dist/edit.js +21 -15
  23. package/dist/edit.js.map +1 -1
  24. package/dist/exec.js +140 -3
  25. package/dist/exec.js.map +1 -1
  26. package/dist/fetch.js +1 -0
  27. package/dist/fetch.js.map +1 -1
  28. package/dist/format.js +153 -11
  29. package/dist/format.js.map +1 -1
  30. package/dist/git.js +1 -0
  31. package/dist/git.js.map +1 -1
  32. package/dist/glob.js +14 -7
  33. package/dist/glob.js.map +1 -1
  34. package/dist/grep.js +14 -7
  35. package/dist/grep.js.map +1 -1
  36. package/dist/index.d.ts +55 -3
  37. package/dist/index.js +819 -325
  38. package/dist/index.js.map +1 -1
  39. package/dist/install.js +153 -11
  40. package/dist/install.js.map +1 -1
  41. package/dist/json.js +1 -0
  42. package/dist/json.js.map +1 -1
  43. package/dist/lint.js +153 -11
  44. package/dist/lint.js.map +1 -1
  45. package/dist/logs.js +14 -7
  46. package/dist/logs.js.map +1 -1
  47. package/dist/memory.js +1 -0
  48. package/dist/memory.js.map +1 -1
  49. package/dist/mode.js +1 -0
  50. package/dist/mode.js.map +1 -1
  51. package/dist/outdated.js +16 -8
  52. package/dist/outdated.js.map +1 -1
  53. package/dist/pack.js +630 -324
  54. package/dist/pack.js.map +1 -1
  55. package/dist/patch.js +14 -7
  56. package/dist/patch.js.map +1 -1
  57. package/dist/process-registry.d.ts +56 -2
  58. package/dist/process-registry.js +138 -3
  59. package/dist/process-registry.js.map +1 -1
  60. package/dist/read.js +21 -16
  61. package/dist/read.js.map +1 -1
  62. package/dist/replace.js +14 -7
  63. package/dist/replace.js.map +1 -1
  64. package/dist/scaffold.js +14 -7
  65. package/dist/scaffold.js.map +1 -1
  66. package/dist/search.js +1 -0
  67. package/dist/search.js.map +1 -1
  68. package/dist/test.js +153 -11
  69. package/dist/test.js.map +1 -1
  70. package/dist/todo.js +1 -0
  71. package/dist/todo.js.map +1 -1
  72. package/dist/tool-help.js +1 -0
  73. package/dist/tool-help.js.map +1 -1
  74. package/dist/tool-icons.d.ts +20 -0
  75. package/dist/tool-icons.js +130 -0
  76. package/dist/tool-icons.js.map +1 -0
  77. package/dist/tool-search.js +1 -0
  78. package/dist/tool-search.js.map +1 -1
  79. package/dist/tool-use.js +1 -0
  80. package/dist/tool-use.js.map +1 -1
  81. package/dist/tree.js +14 -7
  82. package/dist/tree.js.map +1 -1
  83. package/dist/typecheck.js +153 -11
  84. package/dist/typecheck.js.map +1 -1
  85. package/dist/write.js +21 -15
  86. package/dist/write.js.map +1 -1
  87. package/package.json +6 -2
package/dist/audit.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import * as Core from '@wrongstack/core';
2
3
  import { buildChildEnv, expectDefined, wstackGlobalRoot } from '@wrongstack/core';
3
4
  import * as fs from 'node:fs';
4
5
  import { mkdirSync, createWriteStream } from 'node:fs';
@@ -130,6 +131,23 @@ var CircuitBreaker = class {
130
131
  lastSlowAt = null;
131
132
  /** Timestamp when the breaker was opened (for cooldown calculation). */
132
133
  openedAt = null;
134
+ /**
135
+ * Master enable flag. When false the breaker is bypassed: `beforeCall`
136
+ * always returns true and `afterCall` records nothing. The class itself
137
+ * defaults to enabled (so the standalone unit tests exercise tripping); the
138
+ * ProcessRegistry flips this off until the user opts in via `/settings`.
139
+ */
140
+ enabled = true;
141
+ /**
142
+ * Fired (best-effort) when the breaker transitions into the `open` state.
143
+ * The registry uses this to arm its auto kill/reset countdown.
144
+ */
145
+ onTrip;
146
+ /**
147
+ * Fired (best-effort) when the breaker returns to `closed` after having been
148
+ * open/half-open. The registry uses this to cancel a pending kill/reset.
149
+ */
150
+ onReset;
133
151
  constructor(config = {}) {
134
152
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
135
153
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -138,12 +156,22 @@ var CircuitBreaker = class {
138
156
  this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
139
157
  this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
140
158
  }
159
+ /** Toggle the master enable. Disabling resets to a clean `closed` state. */
160
+ setEnabled(enabled) {
161
+ if (this.enabled === enabled) return;
162
+ this.enabled = enabled;
163
+ if (!enabled) this._reset();
164
+ }
165
+ get isEnabled() {
166
+ return this.enabled;
167
+ }
141
168
  /**
142
169
  * Returns true if the circuit allows a new call to proceed.
143
170
  * When false, callers should abort the tool call and return a
144
171
  * circuit-breaker error instead of spawning a process.
145
172
  */
146
173
  get canProceed() {
174
+ if (!this.enabled) return true;
147
175
  this._checkStateTransition();
148
176
  return this.state !== "open";
149
177
  }
@@ -179,7 +207,7 @@ var CircuitBreaker = class {
179
207
  * not affect breaker state.
180
208
  */
181
209
  beforeCall(bypass = false) {
182
- if (bypass) return true;
210
+ if (bypass || !this.enabled) return true;
183
211
  this._checkStateTransition();
184
212
  if (this.state === "open") return false;
185
213
  return true;
@@ -194,7 +222,7 @@ var CircuitBreaker = class {
194
222
  * Use for background/fire-and-forget processes.
195
223
  */
196
224
  afterCall(durationMs, failed, bypass = false) {
197
- if (bypass) return;
225
+ if (bypass || !this.enabled) return;
198
226
  const now = Date.now();
199
227
  if (this.state === "half-open") {
200
228
  if (failed) {
@@ -240,12 +268,23 @@ var CircuitBreaker = class {
240
268
  if (this.state === "open") return;
241
269
  this.state = "open";
242
270
  this.openedAt = Date.now();
271
+ try {
272
+ this.onTrip?.();
273
+ } catch {
274
+ }
243
275
  }
244
276
  _reset() {
277
+ const wasRecovering = this.state !== "closed";
245
278
  this.state = "closed";
246
279
  this.consecutiveFailures = 0;
247
280
  this.window = [];
248
281
  this.openedAt = null;
282
+ if (wasRecovering) {
283
+ try {
284
+ this.onReset?.();
285
+ } catch {
286
+ }
287
+ }
249
288
  }
250
289
  /** Transition from open → half-open when cooldown elapses. */
251
290
  _checkStateTransition() {
@@ -307,8 +346,21 @@ function killWin32Tree(pid) {
307
346
  var ProcessRegistryImpl = class {
308
347
  processes = /* @__PURE__ */ new Map();
309
348
  breaker;
349
+ /**
350
+ * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
351
+ * a countdown is armed; on expiry all tracked processes are killed and the
352
+ * breaker is reset to closed (forced recovery). Zero means manual recovery
353
+ * only (`/kill reset`).
354
+ */
355
+ autoKillResetMs = 0;
356
+ autoKillTimer = null;
357
+ autoKillArmedAt = null;
358
+ breakerCountdownListeners = [];
310
359
  constructor(breakerConfig) {
311
360
  this.breaker = new CircuitBreaker(breakerConfig);
361
+ this.breaker.onTrip = () => this._armAutoKillReset();
362
+ this.breaker.onReset = () => this._cancelAutoKillReset();
363
+ this.breaker.setEnabled(false);
312
364
  }
313
365
  register(info) {
314
366
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
@@ -384,6 +436,90 @@ var ProcessRegistryImpl = class {
384
436
  forceBreakerReset() {
385
437
  this.breaker.forceReset();
386
438
  }
439
+ /**
440
+ * Configure circuit-breaker protection at runtime. Called from `/settings`
441
+ * (instant, all modes) and on TUI mount (applies persisted config).
442
+ *
443
+ * - `enabled` toggles whether the breaker gates `bash`/`exec`.
444
+ * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
445
+ * trips (0 = manual recovery only).
446
+ *
447
+ * Re-applies cleanly on every call: cancels a pending countdown when the
448
+ * timeout is cleared or protection disabled, and re-arms if the breaker is
449
+ * currently open under the new settings.
450
+ */
451
+ setBreakerConfig(cfg) {
452
+ if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
453
+ if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
454
+ if (this.autoKillResetMs <= 0) {
455
+ this._cancelAutoKillReset();
456
+ return;
457
+ }
458
+ if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
459
+ this._armAutoKillReset();
460
+ }
461
+ }
462
+ /**
463
+ * Live countdown to the next auto kill/reset, or null when nothing is armed.
464
+ * The TUI polls this on a 1s tick while armed so the statusline decrements.
465
+ */
466
+ getBreakerCountdown() {
467
+ if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
468
+ const elapsed = Date.now() - this.autoKillArmedAt;
469
+ return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
470
+ }
471
+ /**
472
+ * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
473
+ * Use {@link getBreakerCountdown} for the live ticking value between events.
474
+ */
475
+ onBreakerCountdownChange(listener) {
476
+ this.breakerCountdownListeners.push(listener);
477
+ return () => {
478
+ this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
479
+ };
480
+ }
481
+ _emitBreakerCountdown() {
482
+ const snap = this.getBreakerCountdown();
483
+ for (const l of this.breakerCountdownListeners) {
484
+ try {
485
+ l(snap);
486
+ } catch {
487
+ }
488
+ }
489
+ }
490
+ /**
491
+ * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
492
+ * (a fresh trip after a failed half-open probe restarts the clock). No-op
493
+ * when protection is off or no timeout is configured.
494
+ */
495
+ _armAutoKillReset() {
496
+ if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
497
+ this._clearAutoKillTimer();
498
+ this.autoKillArmedAt = Date.now();
499
+ this.autoKillTimer = setTimeout(() => {
500
+ this.autoKillTimer = null;
501
+ this.autoKillArmedAt = null;
502
+ this.killAll({ force: false });
503
+ this.breaker.forceReset();
504
+ this._emitBreakerCountdown();
505
+ }, this.autoKillResetMs);
506
+ this.autoKillTimer.unref?.();
507
+ this._emitBreakerCountdown();
508
+ }
509
+ _cancelAutoKillReset() {
510
+ const wasArmed = this.autoKillArmedAt !== null;
511
+ this._clearAutoKillTimer();
512
+ if (wasArmed) {
513
+ this.autoKillArmedAt = null;
514
+ this._emitBreakerCountdown();
515
+ }
516
+ }
517
+ _clearAutoKillTimer() {
518
+ if (this.autoKillTimer !== null) {
519
+ clearTimeout(this.autoKillTimer);
520
+ this.autoKillTimer = null;
521
+ }
522
+ }
387
523
  /** Kill a single process by PID.
388
524
  *
389
525
  * On POSIX: sends SIGTERM to the *process group* (-pid) so that
@@ -522,8 +658,9 @@ async function* spawnStream(opts) {
522
658
  let pending = "";
523
659
  let error;
524
660
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
525
- const cmd = resolveWin32Command(opts.cmd);
526
- const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
661
+ const resolved = resolveWin32Command(opts.cmd);
662
+ const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
663
+ const cmd = needsShell ? opts.cmd : resolved;
527
664
  const child = spawn(cmd, opts.args, {
528
665
  cwd: opts.cwd,
529
666
  env: buildChildEnv(),
@@ -687,15 +824,20 @@ async function detectPackageManager(cwd) {
687
824
  function resolvePath(input, ctx) {
688
825
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
689
826
  }
827
+ function allowedRoots(ctx) {
828
+ return [path3.resolve(ctx.projectRoot), path3.resolve(Core.wstackGlobalRoot())];
829
+ }
830
+ function isInsideAny(target, roots) {
831
+ return roots.some((root) => {
832
+ const rel = path3.relative(root, target);
833
+ return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
834
+ });
835
+ }
690
836
  function ensureInsideRoot(absPath, ctx) {
691
- if (ctx.allowOutsideProjectRoot) return path3.resolve(absPath);
692
- const root = path3.resolve(ctx.projectRoot);
693
837
  const target = path3.resolve(absPath);
694
- const rel = path3.relative(root, target);
695
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
696
- throw new Error(`Path "${absPath}" is outside project root "${root}"`);
697
- }
698
- return target;
838
+ if (ctx.allowOutsideProjectRoot) return target;
839
+ if (isInsideAny(target, allowedRoots(ctx))) return target;
840
+ throw new Error(`Path "${absPath}" is outside project root "${path3.resolve(ctx.projectRoot)}"`);
699
841
  }
700
842
  function safeResolve(input, ctx) {
701
843
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
@@ -710,6 +852,7 @@ var auditTool = {
710
852
  permission: "confirm",
711
853
  mutating: false,
712
854
  capabilities: ["shell.restricted"],
855
+ icon: "package",
713
856
  timeoutMs: 6e4,
714
857
  inputSchema: {
715
858
  type: "object",