@wrongstack/plugin-sdk 0.308.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @wrongstack/plugin-sdk
2
+
3
+ Authoring SDK for [WrongStack](../../README.md) plugins. Third-party plugin
4
+ authors should depend on **this package only** — it exposes the plugin
5
+ contract, the `definePlugin` helper, and the same audit-hardened runtime
6
+ helpers the built-in plugins use.
7
+
8
+ ```bash
9
+ npm install @wrongstack/plugin-sdk
10
+ ```
11
+
12
+ ## Minimal plugin
13
+
14
+ ```ts
15
+ import { definePlugin } from '@wrongstack/plugin-sdk';
16
+
17
+ export default definePlugin(
18
+ {
19
+ name: 'wstack-plugin-hello',
20
+ version: '1.0.0',
21
+ description: 'Greets the team',
22
+ capabilities: { tools: true },
23
+ defaultConfig: { greeting: 'hello' },
24
+ },
25
+ async (api, options) => {
26
+ api.tools.register({
27
+ name: 'hello_greet',
28
+ description: `Say ${options.greeting} to someone`,
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: { who: { type: 'string' } },
32
+ required: ['who'],
33
+ },
34
+ permission: 'auto',
35
+ riskTier: 'safe',
36
+ async execute(input) {
37
+ return { greeting: `${options.greeting}, ${input.who}!` };
38
+ },
39
+ });
40
+ },
41
+ );
42
+ ```
43
+
44
+ `definePlugin` injects the current `KERNEL_API_VERSION` for you. If you
45
+ write the plugin object by hand instead, declare the contract version
46
+ your plugin was built against:
47
+
48
+ ```ts
49
+ import { KERNEL_API_VERSION } from '@wrongstack/plugin-sdk';
50
+
51
+ export const plugin = {
52
+ name: 'wstack-plugin-hello',
53
+ apiVersion: '^0.1', // keep loading across additive host releases
54
+ // ...
55
+ async setup(api) {},
56
+ };
57
+ ```
58
+
59
+ ## What's in the box
60
+
61
+ | Import | Purpose |
62
+ |---|---|
63
+ | `definePlugin` | Typed authoring helper (infers options, injects `apiVersion`) |
64
+ | `KERNEL_API_VERSION` | The host's plugin contract version |
65
+ | `Plugin`, `PluginAPI`, `PluginCapabilities`, … | The full plugin contract types |
66
+ | `HookEvent`, `HookOutcome`, `InProcessHook`, … | Lifecycle hook types (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `Stop`) |
67
+ | `AgentExtension`, `ProviderRunnerWrapper`, … | Agent-loop extension point types |
68
+ | `Tool`, `EventName`, `Config`, `ConfigStore` | Registry, event, and config types |
69
+ | `@wrongstack/plugin-sdk/runtime` | Bounded collections, `releaseHandles`, sandboxed paths (`safePath`, `isInsideProject`), ReDoS guards (`withReDoSGuard`), safe runner spawning (`resolveRunnerCommand`, `runRunnerCommand`), optional-LLM helpers (`runOptionalPluginLlm`, `runOptionalPluginCouncil`) |
70
+
71
+ ## Versioning
72
+
73
+ The SDK tracks the host's **plugin contract** (`KERNEL_API_VERSION`), not the
74
+ host package version. Pin `apiVersion: '^0.1'` for additive compatibility;
75
+ the host refuses plugins whose declared range no longer satisfies the kernel
76
+ (see `docs/plugin-third-party.md` in the repository for the compatibility
77
+ policy and release checklist).
78
+
79
+ ## Loading your plugin in WrongStack
80
+
81
+ ```bash
82
+ # register an npm-installed package (installs into ~/.wrongstack/plugins,
83
+ # scripts disabled by default for supply-chain safety)
84
+ wstack plugin add wstack-plugin-hello --install
85
+
86
+ # or point at local code during development
87
+ wstack plugin add ./my-plugin # writes { name, path } into config.plugins
88
+ ```
89
+
90
+ External plugins run in-process with full host privileges. On first load the
91
+ host pins a SHA-256 of your plugin's entry file
92
+ (`~/.wrongstack/plugin-trust.json`); after an update, users re-confirm with
93
+ `wstack plugin trust <name>`. See `docs/plugin-author-guide.md` for the full
94
+ isolation and capability-enforcement model.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @wrongstack/plugin-sdk — the authoring surface for WrongStack plugins.
3
+ *
4
+ * Third-party plugins should depend on THIS package (not on
5
+ * `@wrongstack/core` or `@wrongstack/plugins`) to compile against the
6
+ * plugin contract. It re-exports:
7
+ *
8
+ * - `definePlugin` — ergonomic authoring helper that infers option types
9
+ * and injects the current `apiVersion`,
10
+ * - `KERNEL_API_VERSION` — the host contract version to pin against,
11
+ * - every type a plugin's manifest, hooks, tools, and config schema need.
12
+ *
13
+ * Runtime helpers (bounded collections, safe process spawning, sandboxed
14
+ * paths, optional-LLM plumbing) live in `@wrongstack/plugin-sdk/runtime`.
15
+ *
16
+ * Versioning: the SDK follows the host's plugin contract
17
+ * (`KERNEL_API_VERSION`). Declare `apiVersion: '^0.1'` (or use
18
+ * `definePlugin`, which does it for you) so your plugin keeps loading
19
+ * across additive host releases.
20
+ */
21
+ export { definePlugin, KERNEL_API_VERSION } from '@wrongstack/core/plugin';
22
+ export type { JSONSchema, Plugin, PluginAPI, PluginCapabilities, PluginConfigFieldMetadata, PluginConfigFieldLifecycle, PluginDependency, PluginPipelines, PluginRuntime, } from '@wrongstack/core/types';
23
+ export type { AnyHookOutcome, HookEvent, HookInput, HookInvocationContext, HookMatcher, HookOutcome, HookRegistrationOptions, InProcessHook, PreToolUseOutcome, } from '@wrongstack/core/types';
24
+ export type { AfterIterationHook, AfterRunHook, AfterToolExecutionHook, AgentExtension, BeforeIterationHook, BeforeRunHook, BeforeToolExecutionHook, OnErrorHook, ProviderRunnerFn, ProviderRunnerWrapper, } from '@wrongstack/core/extension';
25
+ export type { SystemPromptContributor } from '@wrongstack/core/types';
26
+ export type { Tool, ToolCallContext } from '@wrongstack/core/types';
27
+ export type { EventName, Listener } from '@wrongstack/core/kernel';
28
+ export type { Config, ConfigStore, PluginConfig } from '@wrongstack/core/types';
29
+ export type { Logger } from '@wrongstack/core/types';
30
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // src/index.ts
2
+ import { definePlugin, KERNEL_API_VERSION } from "@wrongstack/core/plugin";
3
+ export {
4
+ KERNEL_API_VERSION,
5
+ definePlugin
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @wrongstack/plugins — bounded key/value cache.
3
+ *
4
+ * Several plugins memoise per-path or per-model work in a module-scope
5
+ * `Map` that is only ever cleared at teardown. In a short test that is
6
+ * invisible; in a long session over a large repository it is a slow leak —
7
+ * one entry per file touched, per branch resolved, per model seen — held
8
+ * for the lifetime of the process.
9
+ *
10
+ * `BoundedMap` is a drop-in replacement for those maps: same `get`/`set`/
11
+ * `delete`/`clear`/`size` surface, plus a hard entry cap with
12
+ * least-recently-used eviction and an optional per-entry TTL. Nothing
13
+ * about the call sites has to change beyond the constructor.
14
+ *
15
+ * Why LRU rather than insertion-order eviction: the access pattern for
16
+ * these caches is "a few hot paths, a long tail of cold ones". Evicting by
17
+ * insertion order throws away the hot entries first, which is exactly
18
+ * backwards; re-reading a hot entry should keep it.
19
+ */
20
+ export interface BoundedMapOptions {
21
+ /** Hard cap on retained entries. Must be a safe integer >= 1. */
22
+ max: number;
23
+ /**
24
+ * Optional per-entry lifetime in ms. An entry older than this is treated
25
+ * as absent by `get`/`has` and dropped on access. Omit for no expiry.
26
+ */
27
+ ttlMs?: number | undefined;
28
+ /** Clock injection point — tests override this instead of faking timers. */
29
+ now?: (() => number) | undefined;
30
+ }
31
+ export declare class BoundedMap<K, V> {
32
+ private readonly map;
33
+ private readonly max;
34
+ private readonly ttlMs;
35
+ private readonly now;
36
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
37
+ private evictions;
38
+ constructor(options: BoundedMapOptions);
39
+ private expired;
40
+ get(key: K): V | undefined;
41
+ /**
42
+ * Read without promoting the key to most-recently-used. Use for
43
+ * diagnostics that must not perturb the eviction order.
44
+ */
45
+ peek(key: K): V | undefined;
46
+ has(key: K): boolean;
47
+ set(key: K, value: V): this;
48
+ delete(key: K): boolean;
49
+ clear(): void;
50
+ get size(): number;
51
+ /** How many entries have been dropped to respect `max`, since the last clear. */
52
+ get evictionCount(): number;
53
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
54
+ prune(): number;
55
+ /** Live (non-expired) entries, coldest first. */
56
+ entries(): IterableIterator<[K, V]>;
57
+ [Symbol.iterator](): IterableIterator<[K, V]>;
58
+ }
59
+ /**
60
+ * A `Set` with the same hard cap and LRU eviction as {@link BoundedMap}.
61
+ *
62
+ * Plugins use module-scope sets to remember "already reported" keys so a
63
+ * finding is surfaced once rather than on every scan. That set has to
64
+ * outlive a single scan, which is why it lives at module scope — but
65
+ * without a bound it grows one entry per distinct finding for the life of
66
+ * the process.
67
+ *
68
+ * Eviction means a very old key may eventually be reported a second time.
69
+ * That is the right trade: re-reporting a finding the user last saw
70
+ * thousands of findings ago is reasonable behaviour, whereas unbounded
71
+ * growth is not.
72
+ */
73
+ export declare class BoundedSet<T> {
74
+ private readonly inner;
75
+ constructor(options: BoundedMapOptions);
76
+ has(value: T): boolean;
77
+ add(value: T): this;
78
+ delete(value: T): boolean;
79
+ clear(): void;
80
+ get size(): number;
81
+ /** How many entries have been dropped to respect `max`, since the last clear. */
82
+ get evictionCount(): number;
83
+ values(): IterableIterator<T>;
84
+ [Symbol.iterator](): IterableIterator<T>;
85
+ }
86
+ //# sourceMappingURL=bounded-map.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @wrongstack/plugins — canonical credential-pattern table.
3
+ *
4
+ * Two plugins detect credentials, at different points in the pipeline:
5
+ *
6
+ * - `secret-scanner` gates tool *input* and inspects tool *output*.
7
+ * - `prompt-firewall` inspects the outgoing *provider request* (and the
8
+ * response) so a credential sitting in context is not shipped to a
9
+ * third party.
10
+ *
11
+ * They previously each carried their own pattern list. The lists drifted:
12
+ * one grew GitLab, npm, SendGrid and DigitalOcean coverage while the other
13
+ * did not, so which credentials were caught depended on which side of the
14
+ * pipeline they crossed. A credential is a credential — the two surfaces
15
+ * should never disagree about what one looks like.
16
+ *
17
+ * This module is the single source of truth. Adding a pattern here
18
+ * strengthens both surfaces at once.
19
+ *
20
+ * Style notes for new entries:
21
+ * - Prefer explicit lookaround boundaries — `(?<![A-Za-z0-9])` /
22
+ * `(?![A-Za-z0-9])` — over ``. `` is defined against word
23
+ * characters, so it silently fails next to the `+`, `/` and `=` that
24
+ * base64-shaped secrets routinely end with.
25
+ * - Keep every group non-capturing (`(?:…)`). The combined regex maps a
26
+ * capture-group index back to the pattern that fired; an inner group
27
+ * shifts that mapping. `secret-scanner` compensates for user-supplied
28
+ * patterns, but built-ins should not need it.
29
+ * - Avoid unbounded `.*` lookarounds: these run on every provider
30
+ * request, against the full context.
31
+ */
32
+ /** One credential shape, with a stable machine-readable id. */
33
+ export interface CredentialPattern {
34
+ /** Stable id, e.g. `github_pat`. Reported to users and in metrics. */
35
+ type: string;
36
+ /** Global-flagged matcher. */
37
+ regex: RegExp;
38
+ }
39
+ export declare const CREDENTIAL_PATTERNS: readonly CredentialPattern[];
40
+ /** Fresh, independently-stateful copies (RegExp `lastIndex` is mutable). */
41
+ export declare function cloneCredentialPatterns(): CredentialPattern[];
42
+ //# sourceMappingURL=credential-patterns.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * H1 idempotent state — single-slot plugin state with a registry of
3
+ * releasable handles that survives `setup()` reload cycles.
4
+ *
5
+ * The "H1 audit pattern" (per SAGE memory T-03) is documented across
6
+ * the plugin suite: a plugin's module-scope `state` object holds
7
+ * counters plus a `hookUnregister` (or `extensionUnregister`) slot;
8
+ * on reload, the slot MUST be released before a new one is stored.
9
+ * Every plugin implements this inline with subtle variations:
10
+ *
11
+ * - some use `releaseHandle(state.hookUnregister)` (`accessibility-auditor`)
12
+ * - some use `try { state.hookUnregister(); } catch {}` (`config-validator`)
13
+ * - some use a single inline `if (state.hookUnregister) { … }` block
14
+ *
15
+ * The drift cost: in 4 plugins the prior handle was leaked on reload
16
+ * because the inline `if` check raced with the new registration.
17
+ * This helper centralises the contract.
18
+ *
19
+ * Contract:
20
+ * `createH1State<T>(initial)` returns
21
+ * {
22
+ * state: T, // the user's mutable state
23
+ * register: (key, unregister) => void,
24
+ * release: (key) => void,
25
+ * releaseAll: () => void,
26
+ * }
27
+ *
28
+ * - `register(key, unregister)` releases any prior handle at `key`
29
+ * before storing the new one.
30
+ * - `release(key)` is a no-op if no handle is registered.
31
+ * - `releaseAll()` releases every registered handle and clears the map.
32
+ * - A throwing unregister function is swallowed (best-effort), matching
33
+ * the existing `releaseHandle` semantics at `runtime/handles.ts`.
34
+ *
35
+ * The state object itself is NOT reset by `releaseAll` — counter
36
+ * reset is the plugin's responsibility (it knows the semantics of its
37
+ * counters). This helper owns the handle lifecycle only.
38
+ */
39
+ export type Unregister = () => void;
40
+ export interface H1State<T> {
41
+ /** The plugin's mutable state. Owned by the caller; never reset by this helper. */
42
+ state: T;
43
+ /**
44
+ * Register an unregister function under `key`. Any prior handle at
45
+ * `key` is released first. Throwing unregister functions are
46
+ * swallowed.
47
+ */
48
+ register: (key: string, unregister: Unregister | null | undefined) => void;
49
+ /**
50
+ * Release the handle at `key` (if any). Idempotent. Throwing
51
+ * unregister functions are swallowed.
52
+ */
53
+ release: (key: string) => void;
54
+ /** Release every registered handle. Idempotent. */
55
+ releaseAll: () => void;
56
+ /** Number of currently registered handles. Observability for health()/status tools. */
57
+ size: () => number;
58
+ /** List the registered keys. Order is insertion order; useful for diagnostics. */
59
+ keys: () => string[];
60
+ }
61
+ export declare function createH1State<T>(initial: T): H1State<T>;
62
+ //# sourceMappingURL=h1-state.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @wrongstack/plugins — unregister-handle discipline.
3
+ *
4
+ * Plugins keep their hook/listener unregister functions in module-scope
5
+ * state, and every plugin's `setup()` is expected to be idempotent: calling
6
+ * it twice must not leave two live registrations. The house style calls
7
+ * this the "H1 pattern".
8
+ *
9
+ * The failure mode this helper exists to prevent is subtle and was present
10
+ * in ~15 plugins: `setup()` reset the handle with
11
+ *
12
+ * ```ts
13
+ * state.hookUnregister = null; // WRONG
14
+ * ```
15
+ *
16
+ * which drops the only reference to the *previous* registration without
17
+ * calling it. The old hook stays live in the registry, unreachable, and
18
+ * fires alongside the new one — so counters double, warnings appear twice,
19
+ * and a gate can block on a stale closure holding the previous config.
20
+ *
21
+ * `releaseHandle` makes the correct spelling a one-liner with the same
22
+ * shape as the wrong one, so the two are hard to confuse:
23
+ *
24
+ * ```ts
25
+ * state.hookUnregister = releaseHandle(state.hookUnregister);
26
+ * ```
27
+ */
28
+ /** An unregister function returned by `registerHook`/`onEvent`/etc. */
29
+ export type Unregister = (() => void) | null | undefined;
30
+ /**
31
+ * Invoke `off` if present, swallowing any error, and return `null` so the
32
+ * caller can assign the result straight back to the handle.
33
+ *
34
+ * Unregistering is best-effort by design: a handle whose owner has already
35
+ * been torn down may throw, and that must never prevent the rest of
36
+ * `setup()`/`teardown()` from running.
37
+ */
38
+ export declare function releaseHandle(off: Unregister): null;
39
+ /**
40
+ * Release several handles held on one state object, clearing each in place.
41
+ *
42
+ * @example
43
+ * releaseHandles(state, ['hookUnregister', 'postHookUnregister']);
44
+ */
45
+ export declare function releaseHandles<S extends object, K extends keyof S>(state: S, keys: readonly K[]): void;
46
+ //# sourceMappingURL=handles.d.ts.map
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @wrongstack/plugins — Language-agnostic runtime helper.
3
+ *
4
+ * Plugins that need to invoke a build-time or test-time tool
5
+ * (TypeScript's `tsc`, Node's `vitest`, Python's `pytest`, Rust's
6
+ * `cargo`, Go's `go test`, etc.) share this module instead of
7
+ * re-implementing argv/sandbox/allowlist logic.
8
+ *
9
+ * Why one module instead of three plugin-local copies?
10
+ * - One audit surface for security-sensitive code: arg-splitting,
11
+ * sandboxing, execFile-with-shell-false, maxBuffer, timeout.
12
+ * - Plugins stay focused on their domain (linters, type-checkers,
13
+ * test runners); the runtime helper owns the cross-cutting concern.
14
+ * - New languages opt in by adding a `LanguageRuntime` entry; the
15
+ * plugin layer keeps working unchanged.
16
+ *
17
+ * The public API is intentionally small:
18
+ * - `LanguageRuntime`: declares which language a plugin targets.
19
+ * - `resolveRunnerCommand(runtime, command, options)`: validate +
20
+ * split a user-supplied command into a safe argv array.
21
+ * - `sanitizeRunnerPath(value, options)`: reject paths outside
22
+ * the project or that start with `-` (option smuggling).
23
+ * - `runRunnerCommand(argv, options)`: spawn the resolved argv
24
+ * with `shell:false`, capture stdout/stderr/code, and apply
25
+ * timeout/abort.
26
+ * - `probeRunner(runtime, argv, options)`: cheap availability check.
27
+ *
28
+ * Anything language-specific (flag tables, default commands,
29
+ * output parsing) stays in the plugin that owns that language.
30
+ */
31
+ export { parseLlmJsonObject, runOptionalPluginCouncil, runOptionalPluginLlm, stripOuterMarkdownFence, type OptionalCouncilRequest, type OptionalLlmRequest, type OptionalLlmResult, } from './llm.js';
32
+ export { BoundedMap, BoundedSet, type BoundedMapOptions } from './bounded-map.js';
33
+ export { cloneCredentialPatterns, CREDENTIAL_PATTERNS, type CredentialPattern, } from './credential-patterns.js';
34
+ export { UNSERIALIZABLE, safeJsonStringify } from './safe-json.js';
35
+ export { releaseHandle, releaseHandles, type Unregister } from './handles.js';
36
+ export { withReDoSGuard, guardedMatcher, type ReDoSResult, type ReDoSOptions, } from './redos-guard.js';
37
+ export { safePath, isInsideProject, type SafePathOptions, } from './sandbox.js';
38
+ export { createH1State, type H1State, } from './h1-state.js';
39
+ export { clearLocalBinCache, findOnPath, resolveExecInvocation, resolveFirstNodeBin, resolveNodeBin, resolveWin32Command, type ExecInvocation, type ResolvedNodeBin, } from './local-bin.js';
40
+ export type LanguageId = 'typescript' | 'javascript' | 'python' | 'go' | 'rust' | 'shell' | 'ruby' | 'java' | 'kotlin' | 'dotnet' | 'generic';
41
+ export type PackageManagerId = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'pip' | 'poetry' | 'go' | 'cargo' | 'gem' | 'maven' | 'gradle' | 'dotnet' | 'none';
42
+ export interface LanguageRuntime {
43
+ /** Stable identifier for diagnostics and logging. */
44
+ id: LanguageId;
45
+ /**
46
+ * Default package manager launcher if the plugin must spawn a tool
47
+ * (e.g. `npx vitest` or `cargo test`). `none` means the executable
48
+ * itself is invoked directly (no launcher).
49
+ */
50
+ packageManager: PackageManagerId;
51
+ /**
52
+ * Executable token that must appear as the second argv element when
53
+ * a launcher is used (e.g. `vitest` after `pnpm exec`, `test` after
54
+ * `cargo`). When `subcommands.length === 0` and the package manager
55
+ * has no subcommand step, this is also accepted as the head token.
56
+ */
57
+ executable: string;
58
+ /**
59
+ * Allowlisted flag values, in addition to positional arguments and
60
+ * file paths. `null` means "no flag allowlist enforced" — every
61
+ * leading-dash token is still rejected, but no flag whitelist applies.
62
+ */
63
+ allowedFlags: ReadonlySet<string> | null;
64
+ /**
65
+ * Optional list of subcommand tokens that may follow the launcher,
66
+ * e.g. `['exec']` for `pnpm exec tsc`. Empty array means the runner
67
+ * executable must appear immediately as the second token (e.g.
68
+ * `cargo test`, `go test`).
69
+ */
70
+ subcommands: readonly string[];
71
+ /**
72
+ * Default command spelling for `resolveRunnerCommand` when no
73
+ * custom command is supplied. Plugins can override via config.
74
+ */
75
+ defaultCommand: string;
76
+ }
77
+ export interface ResolvedCommand {
78
+ cmd: string;
79
+ args: readonly string[];
80
+ display: string;
81
+ }
82
+ export interface ResolveOptions {
83
+ /**
84
+ * Project root used to sandbox absolute executable paths. Defaults
85
+ * to `process.cwd()`. Absolute executable paths must resolve inside
86
+ * this directory; relative basenames must match `LanguageRuntime.executable`.
87
+ */
88
+ projectRoot?: string;
89
+ }
90
+ export interface RunOptions extends ResolveOptions {
91
+ cwd: string;
92
+ timeoutMs: number;
93
+ signal?: AbortSignal;
94
+ }
95
+ export interface RunResult {
96
+ code: number | null;
97
+ stdout: string;
98
+ stderr: string;
99
+ timedOut: boolean;
100
+ /** True when the executable could not be spawned (ENOENT, EPERM, …). */
101
+ spawnError: boolean;
102
+ }
103
+ /**
104
+ * Validate `value` as a sandboxed path inside the project. Returns the
105
+ * canonical absolute path on success, `null` on rejection (empty,
106
+ * outside the project, leading-dash, or longer than 4096 bytes).
107
+ */
108
+ export declare function sanitizeRunnerPath(value: string, options?: ResolveOptions): string | null;
109
+ /**
110
+ * Resolve a user-supplied command string into an argv-style invocation
111
+ * using the language runtime's allowlist. Returns `null` if the command
112
+ * fails closed (unknown launcher, unknown subcommand, disallowed flag,
113
+ * metacharacters, leading-dash injection, absolute-path escape).
114
+ */
115
+ export declare function resolveRunnerCommand(runtime: LanguageRuntime, command: string, options?: ResolveOptions): ResolvedCommand | null;
116
+ /**
117
+ * Spawn the resolved argv with `shell:false`. Always uses
118
+ * `execFile` (no shell, argv), captures stdout/stderr with a hard
119
+ * `maxBuffer`, and respects `signal` + `timeoutMs`. Plugins can layer
120
+ * language-specific output parsing on top of `RunResult`.
121
+ */
122
+ export declare function runRunnerCommand(argv: readonly string[], options: RunOptions): Promise<RunResult>;
123
+ /**
124
+ * Cheap availability check. Calls the runner's `--version`-like command
125
+ * with a short timeout; returns true only when exit code is zero.
126
+ */
127
+ export declare function probeRunner(runtime: LanguageRuntime, probeArg: string | undefined, options: RunOptions): Promise<boolean>;
128
+ /**
129
+ * Check whether a file path is inside the project root. Uses
130
+ * `process.cwd()` as the project boundary. Returns `true` for valid
131
+ * paths inside the project, `false` for empty, too-long, outside,
132
+ * or absolute paths that escape.
133
+ *
134
+ * This is the canonical sandbox check that every file-mutating or
135
+ * file-reading plugin should call before touching a path supplied
136
+ * by tool input. It replaces 27 identical copies across plugins.
137
+ *
138
+ * Performance: caches `process.cwd()` per call to avoid redundant
139
+ * syscalls when checking multiple paths in the same tick.
140
+ */
141
+ export declare function withinProject(p: string): boolean;
142
+ /**
143
+ * Convenience: locate the runner binary on disk inside the project.
144
+ * Returns the absolute path or `null`.
145
+ */
146
+ export declare function locateRunnerEntry(runtime: LanguageRuntime, projectRoot: string): string | null;
147
+ export interface CollectOptions {
148
+ /** File extensions to include (e.g. ['.ts', '.tsx', '.js']). */
149
+ extensions: string[];
150
+ /** Directory names to skip entirely. Default skips node_modules, dist, .git, coverage. */
151
+ excludeDirs?: string[] | undefined;
152
+ /** Maximum recursion depth. Unlimited when omitted. */
153
+ maxDepth?: number | undefined;
154
+ }
155
+ /**
156
+ * Recursively collect files under `root` whose extension is in
157
+ * `opts.extensions`. Skips directories named in `opts.excludeDirs`
158
+ * (defaulting to node_modules, dist, .git, coverage) and limits depth
159
+ * when `opts.maxDepth` is set.
160
+ *
161
+ * Shared by 6+ plugin source-scan tools that previously duplicated
162
+ * this implementation identically.
163
+ *
164
+ * Determinism: returns files in sorted order (locale-aware) so
165
+ * scan results are reproducible across platforms and file systems.
166
+ */
167
+ export declare function collectSourceFiles(root: string, opts: CollectOptions): string[];
168
+ /**
169
+ * Async version of `collectSourceFiles` for non-blocking file collection.
170
+ * Uses `fs.promises` to avoid blocking the event loop on large directory trees.
171
+ *
172
+ * Performance: prefer this in hooks and tools that run on every write/edit
173
+ * (e.g., duplicate-code-detector) to keep the agent loop responsive during
174
+ * large scans.
175
+ */
176
+ export declare function collectSourceFilesAsync(root: string, opts: CollectOptions): Promise<string[]>;
177
+ /**
178
+ * Check whether `p` has one of the given extensions (case-insensitive).
179
+ * Replaces a 6-copy helper that was duplicated identically across
180
+ * source-scan plugins.
181
+ */
182
+ export declare function matchesExtension(p: string, exts: string[]): boolean;
183
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Small, shared helpers for optional plugin LLM enrichment.
3
+ *
4
+ * The host-owned `api.llm` facade keeps provider credentials and routing out
5
+ * of plugins. These helpers standardise the other half of that contract:
6
+ * bounded prompts, cancellation, defensive response parsing, and an explicit
7
+ * deterministic fallback when no provider is wired or generation fails.
8
+ */
9
+ import type { CouncilOption, PluginAPI, PluginLLMOptions } from '@wrongstack/core/types';
10
+ export interface OptionalLlmResult<T> {
11
+ used: boolean;
12
+ value: T | null;
13
+ fallbackReason: 'not-requested' | 'unavailable' | 'cancelled' | 'provider-error' | 'invalid-response' | null;
14
+ }
15
+ export interface OptionalLlmRequest<T> {
16
+ requested: boolean;
17
+ prompt: string;
18
+ options?: PluginLLMOptions | undefined;
19
+ parse(text: string): T | null;
20
+ api: Pick<PluginAPI, 'llm' | 'log'>;
21
+ label: string;
22
+ }
23
+ export interface OptionalCouncilRequest<T> extends OptionalLlmRequest<T> {
24
+ context?: string | undefined;
25
+ profile?: string | undefined;
26
+ councilOptions?: readonly CouncilOption[] | undefined;
27
+ }
28
+ /** Remove one outer Markdown fence without modifying inner code fences. */
29
+ export declare function stripOuterMarkdownFence(text: string): string;
30
+ /** Parse a JSON object from a plain or fenced provider response. */
31
+ export declare function parseLlmJsonObject(text: string): Record<string, unknown> | null;
32
+ /**
33
+ * Run optional enrichment without turning a provider outage into a tool
34
+ * failure. Abort remains observable as a fallback reason and the caller's
35
+ * deterministic result remains authoritative.
36
+ */
37
+ export declare function runOptionalPluginLlm<T>(request: OptionalLlmRequest<T>): Promise<OptionalLlmResult<T>>;
38
+ /**
39
+ * Prefer the host Council for consequential analysis, then degrade through the
40
+ * same One Shot helper and finally the caller's deterministic result. Council
41
+ * outages therefore never turn an optional enrichment into a tool failure.
42
+ */
43
+ export declare function runOptionalPluginCouncil<T>(request: OptionalCouncilRequest<T>): Promise<OptionalLlmResult<T>>;
44
+ //# sourceMappingURL=llm.d.ts.map