@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/install.js CHANGED
@@ -132,6 +132,23 @@ var CircuitBreaker = class {
132
132
  lastSlowAt = null;
133
133
  /** Timestamp when the breaker was opened (for cooldown calculation). */
134
134
  openedAt = null;
135
+ /**
136
+ * Master enable flag. When false the breaker is bypassed: `beforeCall`
137
+ * always returns true and `afterCall` records nothing. The class itself
138
+ * defaults to enabled (so the standalone unit tests exercise tripping); the
139
+ * ProcessRegistry flips this off until the user opts in via `/settings`.
140
+ */
141
+ enabled = true;
142
+ /**
143
+ * Fired (best-effort) when the breaker transitions into the `open` state.
144
+ * The registry uses this to arm its auto kill/reset countdown.
145
+ */
146
+ onTrip;
147
+ /**
148
+ * Fired (best-effort) when the breaker returns to `closed` after having been
149
+ * open/half-open. The registry uses this to cancel a pending kill/reset.
150
+ */
151
+ onReset;
135
152
  constructor(config = {}) {
136
153
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
137
154
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -140,12 +157,22 @@ var CircuitBreaker = class {
140
157
  this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
141
158
  this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
142
159
  }
160
+ /** Toggle the master enable. Disabling resets to a clean `closed` state. */
161
+ setEnabled(enabled) {
162
+ if (this.enabled === enabled) return;
163
+ this.enabled = enabled;
164
+ if (!enabled) this._reset();
165
+ }
166
+ get isEnabled() {
167
+ return this.enabled;
168
+ }
143
169
  /**
144
170
  * Returns true if the circuit allows a new call to proceed.
145
171
  * When false, callers should abort the tool call and return a
146
172
  * circuit-breaker error instead of spawning a process.
147
173
  */
148
174
  get canProceed() {
175
+ if (!this.enabled) return true;
149
176
  this._checkStateTransition();
150
177
  return this.state !== "open";
151
178
  }
@@ -181,7 +208,7 @@ var CircuitBreaker = class {
181
208
  * not affect breaker state.
182
209
  */
183
210
  beforeCall(bypass = false) {
184
- if (bypass) return true;
211
+ if (bypass || !this.enabled) return true;
185
212
  this._checkStateTransition();
186
213
  if (this.state === "open") return false;
187
214
  return true;
@@ -196,7 +223,7 @@ var CircuitBreaker = class {
196
223
  * Use for background/fire-and-forget processes.
197
224
  */
198
225
  afterCall(durationMs, failed, bypass = false) {
199
- if (bypass) return;
226
+ if (bypass || !this.enabled) return;
200
227
  const now = Date.now();
201
228
  if (this.state === "half-open") {
202
229
  if (failed) {
@@ -242,12 +269,23 @@ var CircuitBreaker = class {
242
269
  if (this.state === "open") return;
243
270
  this.state = "open";
244
271
  this.openedAt = Date.now();
272
+ try {
273
+ this.onTrip?.();
274
+ } catch {
275
+ }
245
276
  }
246
277
  _reset() {
278
+ const wasRecovering = this.state !== "closed";
247
279
  this.state = "closed";
248
280
  this.consecutiveFailures = 0;
249
281
  this.window = [];
250
282
  this.openedAt = null;
283
+ if (wasRecovering) {
284
+ try {
285
+ this.onReset?.();
286
+ } catch {
287
+ }
288
+ }
251
289
  }
252
290
  /** Transition from open → half-open when cooldown elapses. */
253
291
  _checkStateTransition() {
@@ -309,8 +347,21 @@ function killWin32Tree(pid) {
309
347
  var ProcessRegistryImpl = class {
310
348
  processes = /* @__PURE__ */ new Map();
311
349
  breaker;
350
+ /**
351
+ * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
352
+ * a countdown is armed; on expiry all tracked processes are killed and the
353
+ * breaker is reset to closed (forced recovery). Zero means manual recovery
354
+ * only (`/kill reset`).
355
+ */
356
+ autoKillResetMs = 0;
357
+ autoKillTimer = null;
358
+ autoKillArmedAt = null;
359
+ breakerCountdownListeners = [];
312
360
  constructor(breakerConfig) {
313
361
  this.breaker = new CircuitBreaker(breakerConfig);
362
+ this.breaker.onTrip = () => this._armAutoKillReset();
363
+ this.breaker.onReset = () => this._cancelAutoKillReset();
364
+ this.breaker.setEnabled(false);
314
365
  }
315
366
  register(info) {
316
367
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
@@ -386,6 +437,90 @@ var ProcessRegistryImpl = class {
386
437
  forceBreakerReset() {
387
438
  this.breaker.forceReset();
388
439
  }
440
+ /**
441
+ * Configure circuit-breaker protection at runtime. Called from `/settings`
442
+ * (instant, all modes) and on TUI mount (applies persisted config).
443
+ *
444
+ * - `enabled` toggles whether the breaker gates `bash`/`exec`.
445
+ * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
446
+ * trips (0 = manual recovery only).
447
+ *
448
+ * Re-applies cleanly on every call: cancels a pending countdown when the
449
+ * timeout is cleared or protection disabled, and re-arms if the breaker is
450
+ * currently open under the new settings.
451
+ */
452
+ setBreakerConfig(cfg) {
453
+ if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
454
+ if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
455
+ if (this.autoKillResetMs <= 0) {
456
+ this._cancelAutoKillReset();
457
+ return;
458
+ }
459
+ if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
460
+ this._armAutoKillReset();
461
+ }
462
+ }
463
+ /**
464
+ * Live countdown to the next auto kill/reset, or null when nothing is armed.
465
+ * The TUI polls this on a 1s tick while armed so the statusline decrements.
466
+ */
467
+ getBreakerCountdown() {
468
+ if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
469
+ const elapsed = Date.now() - this.autoKillArmedAt;
470
+ return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
471
+ }
472
+ /**
473
+ * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
474
+ * Use {@link getBreakerCountdown} for the live ticking value between events.
475
+ */
476
+ onBreakerCountdownChange(listener) {
477
+ this.breakerCountdownListeners.push(listener);
478
+ return () => {
479
+ this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
480
+ };
481
+ }
482
+ _emitBreakerCountdown() {
483
+ const snap = this.getBreakerCountdown();
484
+ for (const l of this.breakerCountdownListeners) {
485
+ try {
486
+ l(snap);
487
+ } catch {
488
+ }
489
+ }
490
+ }
491
+ /**
492
+ * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
493
+ * (a fresh trip after a failed half-open probe restarts the clock). No-op
494
+ * when protection is off or no timeout is configured.
495
+ */
496
+ _armAutoKillReset() {
497
+ if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
498
+ this._clearAutoKillTimer();
499
+ this.autoKillArmedAt = Date.now();
500
+ this.autoKillTimer = setTimeout(() => {
501
+ this.autoKillTimer = null;
502
+ this.autoKillArmedAt = null;
503
+ this.killAll({ force: false });
504
+ this.breaker.forceReset();
505
+ this._emitBreakerCountdown();
506
+ }, this.autoKillResetMs);
507
+ this.autoKillTimer.unref?.();
508
+ this._emitBreakerCountdown();
509
+ }
510
+ _cancelAutoKillReset() {
511
+ const wasArmed = this.autoKillArmedAt !== null;
512
+ this._clearAutoKillTimer();
513
+ if (wasArmed) {
514
+ this.autoKillArmedAt = null;
515
+ this._emitBreakerCountdown();
516
+ }
517
+ }
518
+ _clearAutoKillTimer() {
519
+ if (this.autoKillTimer !== null) {
520
+ clearTimeout(this.autoKillTimer);
521
+ this.autoKillTimer = null;
522
+ }
523
+ }
389
524
  /** Kill a single process by PID.
390
525
  *
391
526
  * On POSIX: sends SIGTERM to the *process group* (-pid) so that
@@ -524,8 +659,9 @@ async function* spawnStream(opts) {
524
659
  let pending = "";
525
660
  let error;
526
661
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
527
- const cmd = resolveWin32Command(opts.cmd);
528
- const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
662
+ const resolved = resolveWin32Command(opts.cmd);
663
+ const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
664
+ const cmd = needsShell ? opts.cmd : resolved;
529
665
  const child = spawn(cmd, opts.args, {
530
666
  cwd: opts.cwd,
531
667
  env: buildChildEnv(),
@@ -689,14 +825,20 @@ async function detectPackageManager(cwd) {
689
825
  function resolvePath(input, ctx) {
690
826
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
691
827
  }
828
+ function allowedRoots(ctx) {
829
+ return [path3.resolve(ctx.projectRoot), path3.resolve(Core.wstackGlobalRoot())];
830
+ }
831
+ function isInsideAny(target, roots) {
832
+ return roots.some((root) => {
833
+ const rel = path3.relative(root, target);
834
+ return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
835
+ });
836
+ }
692
837
  function ensureInsideRoot(absPath, ctx) {
693
- const root = path3.resolve(ctx.projectRoot);
694
838
  const target = path3.resolve(absPath);
695
- const rel = path3.relative(root, target);
696
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
697
- throw new Error(`Path "${absPath}" is outside project root "${root}"`);
698
- }
699
- return target;
839
+ if (ctx.allowOutsideProjectRoot) return target;
840
+ if (isInsideAny(target, allowedRoots(ctx))) return target;
841
+ throw new Error(`Path "${absPath}" is outside project root "${path3.resolve(ctx.projectRoot)}"`);
700
842
  }
701
843
  function safeResolve(input, ctx) {
702
844
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
@@ -781,6 +923,7 @@ var installTool = {
781
923
  permission: "confirm",
782
924
  mutating: true,
783
925
  riskTier: "standard",
926
+ icon: "package",
784
927
  timeoutMs: 12e4,
785
928
  capabilities: ["package.install", "shell.restricted"],
786
929
  inputSchema: {
@@ -905,6 +1048,7 @@ function resolveManifestPath(cwd, pkgManager) {
905
1048
  case "yarn":
906
1049
  case "npm":
907
1050
  return join(cwd, "package.json");
1051
+ /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
908
1052
  default:
909
1053
  return join(cwd, "package.json");
910
1054
  }