@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.
- package/dist/audit.js +154 -11
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +138 -2
- package/dist/bash.js.map +1 -1
- package/dist/batch-tool-use.js +1 -0
- package/dist/batch-tool-use.js.map +1 -1
- package/dist/builtin.d.ts +20 -1
- package/dist/builtin.js +661 -325
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +20 -0
- package/dist/circuit-breaker.js +40 -2
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/codebase-index/index.d.ts +16 -0
- package/dist/codebase-index/index.js +59 -25
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +56 -25
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/diff.js +14 -7
- package/dist/diff.js.map +1 -1
- package/dist/document.js +14 -8
- package/dist/document.js.map +1 -1
- package/dist/edit.js +21 -15
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +140 -3
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +1 -0
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +153 -11
- package/dist/format.js.map +1 -1
- package/dist/git.js +1 -0
- package/dist/git.js.map +1 -1
- package/dist/glob.js +14 -7
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +14 -7
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +55 -3
- package/dist/index.js +819 -325
- package/dist/index.js.map +1 -1
- package/dist/install.js +153 -11
- package/dist/install.js.map +1 -1
- package/dist/json.js +1 -0
- package/dist/json.js.map +1 -1
- package/dist/lint.js +153 -11
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +14 -7
- package/dist/logs.js.map +1 -1
- package/dist/memory.js +1 -0
- package/dist/memory.js.map +1 -1
- package/dist/mode.js +1 -0
- package/dist/mode.js.map +1 -1
- package/dist/outdated.js +16 -8
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +630 -324
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +14 -7
- package/dist/patch.js.map +1 -1
- package/dist/process-registry.d.ts +56 -2
- package/dist/process-registry.js +138 -3
- package/dist/process-registry.js.map +1 -1
- package/dist/read.js +21 -16
- package/dist/read.js.map +1 -1
- package/dist/replace.js +14 -7
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +14 -7
- package/dist/scaffold.js.map +1 -1
- package/dist/search.js +1 -0
- package/dist/search.js.map +1 -1
- package/dist/test.js +153 -11
- package/dist/test.js.map +1 -1
- package/dist/todo.js +1 -0
- package/dist/todo.js.map +1 -1
- package/dist/tool-help.js +1 -0
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-icons.d.ts +20 -0
- package/dist/tool-icons.js +130 -0
- package/dist/tool-icons.js.map +1 -0
- package/dist/tool-search.js +1 -0
- package/dist/tool-search.js.map +1 -1
- package/dist/tool-use.js +1 -0
- package/dist/tool-use.js.map +1 -1
- package/dist/tree.js +14 -7
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +153 -11
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +21 -15
- package/dist/write.js.map +1 -1
- 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
|
|
526
|
-
const needsShell = isWin && (
|
|
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
|
-
|
|
695
|
-
if (
|
|
696
|
-
|
|
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",
|