@wrongstack/tools 0.260.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 (88) hide show
  1. package/dist/audit.js +154 -10
  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 +674 -333
  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 +62 -27
  15. package/dist/codebase-index/index.js.map +1 -1
  16. package/dist/codebase-index/worker.js +59 -27
  17. package/dist/codebase-index/worker.js.map +1 -1
  18. package/dist/diff.js +14 -6
  19. package/dist/diff.js.map +1 -1
  20. package/dist/document.js +18 -11
  21. package/dist/document.js.map +1 -1
  22. package/dist/edit.js +22 -14
  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.d.ts +19 -1
  27. package/dist/fetch.js +2 -1
  28. package/dist/fetch.js.map +1 -1
  29. package/dist/format.js +153 -10
  30. package/dist/format.js.map +1 -1
  31. package/dist/git.js +1 -0
  32. package/dist/git.js.map +1 -1
  33. package/dist/glob.js +14 -6
  34. package/dist/glob.js.map +1 -1
  35. package/dist/grep.js +14 -6
  36. package/dist/grep.js.map +1 -1
  37. package/dist/index.d.ts +55 -3
  38. package/dist/index.js +833 -336
  39. package/dist/index.js.map +1 -1
  40. package/dist/install.js +154 -10
  41. package/dist/install.js.map +1 -1
  42. package/dist/json.js +2 -0
  43. package/dist/json.js.map +1 -1
  44. package/dist/lint.js +153 -10
  45. package/dist/lint.js.map +1 -1
  46. package/dist/logs.js +14 -6
  47. package/dist/logs.js.map +1 -1
  48. package/dist/memory.js +1 -0
  49. package/dist/memory.js.map +1 -1
  50. package/dist/mode.js +1 -0
  51. package/dist/mode.js.map +1 -1
  52. package/dist/outdated.js +16 -7
  53. package/dist/outdated.js.map +1 -1
  54. package/dist/pack.js +643 -332
  55. package/dist/pack.js.map +1 -1
  56. package/dist/patch.js +14 -6
  57. package/dist/patch.js.map +1 -1
  58. package/dist/process-registry.d.ts +56 -2
  59. package/dist/process-registry.js +138 -3
  60. package/dist/process-registry.js.map +1 -1
  61. package/dist/read.js +24 -18
  62. package/dist/read.js.map +1 -1
  63. package/dist/replace.js +14 -6
  64. package/dist/replace.js.map +1 -1
  65. package/dist/scaffold.js +14 -6
  66. package/dist/scaffold.js.map +1 -1
  67. package/dist/search.js +3 -3
  68. package/dist/search.js.map +1 -1
  69. package/dist/test.js +153 -10
  70. package/dist/test.js.map +1 -1
  71. package/dist/todo.js +1 -0
  72. package/dist/todo.js.map +1 -1
  73. package/dist/tool-help.js +1 -0
  74. package/dist/tool-help.js.map +1 -1
  75. package/dist/tool-icons.d.ts +20 -0
  76. package/dist/tool-icons.js +130 -0
  77. package/dist/tool-icons.js.map +1 -0
  78. package/dist/tool-search.js +1 -0
  79. package/dist/tool-search.js.map +1 -1
  80. package/dist/tool-use.js +1 -0
  81. package/dist/tool-use.js.map +1 -1
  82. package/dist/tree.js +14 -6
  83. package/dist/tree.js.map +1 -1
  84. package/dist/typecheck.js +153 -10
  85. package/dist/typecheck.js.map +1 -1
  86. package/dist/write.js +22 -14
  87. package/dist/write.js.map +1 -1
  88. package/package.json +6 -2
package/dist/lint.js CHANGED
@@ -131,6 +131,23 @@ var CircuitBreaker = class {
131
131
  lastSlowAt = null;
132
132
  /** Timestamp when the breaker was opened (for cooldown calculation). */
133
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;
134
151
  constructor(config = {}) {
135
152
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
136
153
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -139,12 +156,22 @@ var CircuitBreaker = class {
139
156
  this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
140
157
  this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
141
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
+ }
142
168
  /**
143
169
  * Returns true if the circuit allows a new call to proceed.
144
170
  * When false, callers should abort the tool call and return a
145
171
  * circuit-breaker error instead of spawning a process.
146
172
  */
147
173
  get canProceed() {
174
+ if (!this.enabled) return true;
148
175
  this._checkStateTransition();
149
176
  return this.state !== "open";
150
177
  }
@@ -180,7 +207,7 @@ var CircuitBreaker = class {
180
207
  * not affect breaker state.
181
208
  */
182
209
  beforeCall(bypass = false) {
183
- if (bypass) return true;
210
+ if (bypass || !this.enabled) return true;
184
211
  this._checkStateTransition();
185
212
  if (this.state === "open") return false;
186
213
  return true;
@@ -195,7 +222,7 @@ var CircuitBreaker = class {
195
222
  * Use for background/fire-and-forget processes.
196
223
  */
197
224
  afterCall(durationMs, failed, bypass = false) {
198
- if (bypass) return;
225
+ if (bypass || !this.enabled) return;
199
226
  const now = Date.now();
200
227
  if (this.state === "half-open") {
201
228
  if (failed) {
@@ -241,12 +268,23 @@ var CircuitBreaker = class {
241
268
  if (this.state === "open") return;
242
269
  this.state = "open";
243
270
  this.openedAt = Date.now();
271
+ try {
272
+ this.onTrip?.();
273
+ } catch {
274
+ }
244
275
  }
245
276
  _reset() {
277
+ const wasRecovering = this.state !== "closed";
246
278
  this.state = "closed";
247
279
  this.consecutiveFailures = 0;
248
280
  this.window = [];
249
281
  this.openedAt = null;
282
+ if (wasRecovering) {
283
+ try {
284
+ this.onReset?.();
285
+ } catch {
286
+ }
287
+ }
250
288
  }
251
289
  /** Transition from open → half-open when cooldown elapses. */
252
290
  _checkStateTransition() {
@@ -308,8 +346,21 @@ function killWin32Tree(pid) {
308
346
  var ProcessRegistryImpl = class {
309
347
  processes = /* @__PURE__ */ new Map();
310
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 = [];
311
359
  constructor(breakerConfig) {
312
360
  this.breaker = new CircuitBreaker(breakerConfig);
361
+ this.breaker.onTrip = () => this._armAutoKillReset();
362
+ this.breaker.onReset = () => this._cancelAutoKillReset();
363
+ this.breaker.setEnabled(false);
313
364
  }
314
365
  register(info) {
315
366
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
@@ -385,6 +436,90 @@ var ProcessRegistryImpl = class {
385
436
  forceBreakerReset() {
386
437
  this.breaker.forceReset();
387
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
+ }
388
523
  /** Kill a single process by PID.
389
524
  *
390
525
  * On POSIX: sends SIGTERM to the *process group* (-pid) so that
@@ -523,8 +658,9 @@ async function* spawnStream(opts) {
523
658
  let pending = "";
524
659
  let error;
525
660
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
526
- const cmd = resolveWin32Command(opts.cmd);
527
- 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;
528
664
  const child = spawn(cmd, opts.args, {
529
665
  cwd: opts.cwd,
530
666
  env: buildChildEnv(),
@@ -674,14 +810,20 @@ async function* spawnStream(opts) {
674
810
  function resolvePath(input, ctx) {
675
811
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
676
812
  }
813
+ function allowedRoots(ctx) {
814
+ return [path3.resolve(ctx.projectRoot), path3.resolve(Core.wstackGlobalRoot())];
815
+ }
816
+ function isInsideAny(target, roots) {
817
+ return roots.some((root) => {
818
+ const rel = path3.relative(root, target);
819
+ return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
820
+ });
821
+ }
677
822
  function ensureInsideRoot(absPath, ctx) {
678
- const root = path3.resolve(ctx.projectRoot);
679
823
  const target = path3.resolve(absPath);
680
- const rel = path3.relative(root, target);
681
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
682
- throw new Error(`Path "${absPath}" is outside project root "${root}"`);
683
- }
684
- return target;
824
+ if (ctx.allowOutsideProjectRoot) return target;
825
+ if (isInsideAny(target, allowedRoots(ctx))) return target;
826
+ throw new Error(`Path "${absPath}" is outside project root "${path3.resolve(ctx.projectRoot)}"`);
685
827
  }
686
828
  function safeResolve(input, ctx) {
687
829
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
@@ -767,6 +909,7 @@ var lintTool = {
767
909
  mutating: false,
768
910
  timeoutMs: 6e4,
769
911
  capabilities: ["shell.restricted"],
912
+ icon: "code",
770
913
  inputSchema: {
771
914
  type: "object",
772
915
  properties: {