@vibecontrols/plugin-sdk 2026.509.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 (40) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +206 -0
  3. package/boilerplate/.github/workflows/release.template.yml +57 -0
  4. package/boilerplate/README.md +41 -0
  5. package/boilerplate/bunfig.toml +13 -0
  6. package/boilerplate/eslint.config.base.js +36 -0
  7. package/boilerplate/lefthook.base.yml +10 -0
  8. package/boilerplate/package.template.json +52 -0
  9. package/boilerplate/tsconfig.base.json +20 -0
  10. package/dist/audit/index.d.ts +17 -0
  11. package/dist/audit/index.js +19 -0
  12. package/dist/broadcast/index.d.ts +15 -0
  13. package/dist/broadcast/index.js +14 -0
  14. package/dist/cli/index.d.ts +75 -0
  15. package/dist/cli/index.js +104 -0
  16. package/dist/config/index.d.ts +34 -0
  17. package/dist/config/index.js +66 -0
  18. package/dist/contract/index.d.ts +118 -0
  19. package/dist/contract/index.js +16 -0
  20. package/dist/http/index.d.ts +35 -0
  21. package/dist/http/index.js +70 -0
  22. package/dist/index.d.ts +15 -0
  23. package/dist/index.js +595 -0
  24. package/dist/lifecycle/index.d.ts +31 -0
  25. package/dist/lifecycle/index.js +30 -0
  26. package/dist/log/index.d.ts +22 -0
  27. package/dist/log/index.js +25 -0
  28. package/dist/providers/index.d.ts +29 -0
  29. package/dist/providers/index.js +38 -0
  30. package/dist/routes/index.d.ts +37 -0
  31. package/dist/routes/index.js +77 -0
  32. package/dist/storage/index.d.ts +36 -0
  33. package/dist/storage/index.js +67 -0
  34. package/dist/subprocess/index.d.ts +37 -0
  35. package/dist/subprocess/index.js +69 -0
  36. package/dist/telemetry/index.d.ts +27 -0
  37. package/dist/telemetry/index.js +41 -0
  38. package/dist/testing/index.d.ts +31 -0
  39. package/dist/testing/index.js +97 -0
  40. package/package.json +159 -0
@@ -0,0 +1,104 @@
1
+ // src/cli/multimode.ts
2
+ function pickOutputMode(flags) {
3
+ if (flags.json) return "json";
4
+ if (flags.plain) return "plain";
5
+ if (flags.interactive) return "interactive";
6
+ return "auto";
7
+ }
8
+ function isCi() {
9
+ return !!process.env.CI || !!process.env.NO_COLOR || process.env.TERM === "dumb";
10
+ }
11
+ function stdoutIsTty() {
12
+ return Boolean(process.stdout.isTTY);
13
+ }
14
+ async function runMultimode(opts) {
15
+ const data = await opts.fetchData();
16
+ const mode = opts.mode ?? "auto";
17
+ if (mode === "json") {
18
+ const shaped = opts.json ? opts.json(data) : data;
19
+ process.stdout.write(`${JSON.stringify(shaped, null, 2)}
20
+ `);
21
+ return;
22
+ }
23
+ if (mode === "plain") {
24
+ await opts.plain(data);
25
+ return;
26
+ }
27
+ const wantInteractive = (mode === "interactive" || stdoutIsTty() && !isCi()) && !!opts.interactive;
28
+ if (wantInteractive && opts.interactive) {
29
+ try {
30
+ await opts.interactive(data);
31
+ return;
32
+ } catch {
33
+ }
34
+ }
35
+ await opts.plain(data);
36
+ }
37
+ function maybePrintJson(flags, data) {
38
+ if (!flags.json) return false;
39
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
40
+ `);
41
+ return true;
42
+ }
43
+
44
+ // src/cli/redaction.ts
45
+ var SENSITIVE_KEY_RE = /(token|secret|password|apikey|api_key|key|auth|credential|email)/i;
46
+ function redact(value) {
47
+ return redactInner(value);
48
+ }
49
+ function redactInner(value) {
50
+ if (value === null || value === void 0) return value;
51
+ if (Array.isArray(value)) return value.map(redactInner);
52
+ if (typeof value === "object") {
53
+ const out = {};
54
+ for (const [k, v] of Object.entries(value)) {
55
+ if (SENSITIVE_KEY_RE.test(k)) {
56
+ out[k] = "[redacted]";
57
+ continue;
58
+ }
59
+ out[k] = redactInner(v);
60
+ }
61
+ return out;
62
+ }
63
+ return value;
64
+ }
65
+
66
+ // src/cli/command-builder.ts
67
+ var CliCommandBuilder = class {
68
+ constructor(program) {
69
+ this.program = program;
70
+ }
71
+ program;
72
+ /**
73
+ * Register a `<name>` sub-command that fetches once and renders via
74
+ * `--json` / `--plain` / interactive (when stdout is a TTY).
75
+ */
76
+ addStatusCommand(name, spec) {
77
+ this.program.command(name).description(spec.description).option("--json", "emit JSON for scripting").option("--plain", "force plain text (no opentui)").action(async (opts) => {
78
+ const mode = pickOutputMode(opts);
79
+ await runMultimode({
80
+ mode,
81
+ fetchData: spec.fetchData,
82
+ plain: async (data) => {
83
+ const view = spec.redact ? redact(data) : data;
84
+ if (spec.format) {
85
+ await spec.format(view);
86
+ return;
87
+ }
88
+ process.stdout.write(`${JSON.stringify(view, null, 2)}
89
+ `);
90
+ },
91
+ json: (data) => spec.redact ? redact(data) : data
92
+ });
93
+ });
94
+ return this;
95
+ }
96
+ /** Escape hatch: hand back the underlying commander Command. */
97
+ command() {
98
+ return this.program;
99
+ }
100
+ };
101
+
102
+ export { CliCommandBuilder, maybePrintJson, pickOutputMode, redact, runMultimode };
103
+ //# sourceMappingURL=index.js.map
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,34 @@
1
+ import { HostServices, SdkLogger } from '../contract/index.js';
2
+
3
+ /**
4
+ * @vibecontrols/plugin-sdk/config
5
+ *
6
+ * `ConfigManager` resolves plugin config in this order:
7
+ * 1. env var `VIBE_<PLUGIN>_<KEY>` (uppercase, hyphen→underscore)
8
+ * 2. `hostServices.getConfig(<plugin>.<key>)` — plugin-section config.json
9
+ * 3. caller-supplied default
10
+ *
11
+ * Type coercion helpers (`getInt`, `getBoolean`) parse the resolved string
12
+ * with sane fallbacks; invalid values warn through the logger and return
13
+ * the default.
14
+ */
15
+
16
+ declare class ConfigManager {
17
+ private readonly pluginName;
18
+ private readonly hostServices?;
19
+ private readonly logger?;
20
+ private readonly envPrefix;
21
+ constructor(pluginName: string, hostServices?: HostServices | undefined, logger?: SdkLogger | undefined);
22
+ private envName;
23
+ /**
24
+ * Resolve a string config value. Returns `defaultValue` (or undefined)
25
+ * when neither env nor host config has it.
26
+ */
27
+ get(key: string, defaultValue?: string): Promise<string | undefined>;
28
+ /** Like `get`, but throw if the key resolves to undefined / empty. */
29
+ getRequired(key: string): Promise<string>;
30
+ getInt(key: string, defaultValue?: number): Promise<number | undefined>;
31
+ getBoolean(key: string, defaultValue?: boolean): Promise<boolean | undefined>;
32
+ }
33
+
34
+ export { ConfigManager };
@@ -0,0 +1,66 @@
1
+ // src/config/index.ts
2
+ var ConfigManager = class {
3
+ constructor(pluginName, hostServices, logger) {
4
+ this.pluginName = pluginName;
5
+ this.hostServices = hostServices;
6
+ this.logger = logger;
7
+ this.envPrefix = `VIBE_${pluginName.toUpperCase().replace(/-/g, "_")}_`;
8
+ }
9
+ pluginName;
10
+ hostServices;
11
+ logger;
12
+ envPrefix;
13
+ envName(key) {
14
+ return `${this.envPrefix}${key.toUpperCase().replace(/-/g, "_")}`;
15
+ }
16
+ /**
17
+ * Resolve a string config value. Returns `defaultValue` (or undefined)
18
+ * when neither env nor host config has it.
19
+ */
20
+ async get(key, defaultValue) {
21
+ const fromEnv = process.env[this.envName(key)];
22
+ if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
23
+ const fromHost = await this.hostServices?.getConfig?.(`${this.pluginName}.${key}`);
24
+ if (fromHost !== void 0 && fromHost !== "") return fromHost;
25
+ return defaultValue;
26
+ }
27
+ /** Like `get`, but throw if the key resolves to undefined / empty. */
28
+ async getRequired(key) {
29
+ const v = await this.get(key);
30
+ if (v === void 0 || v === "") {
31
+ throw new Error(
32
+ `[${this.pluginName}] required config '${key}' is missing (env ${this.envName(key)} or host config '${this.pluginName}.${key}')`
33
+ );
34
+ }
35
+ return v;
36
+ }
37
+ async getInt(key, defaultValue) {
38
+ const raw = await this.get(key);
39
+ if (raw === void 0) return defaultValue;
40
+ const parsed = Number.parseInt(raw, 10);
41
+ if (Number.isNaN(parsed)) {
42
+ this.logger?.warn?.(this.pluginName, "ConfigManager: invalid int", {
43
+ key,
44
+ raw
45
+ });
46
+ return defaultValue;
47
+ }
48
+ return parsed;
49
+ }
50
+ async getBoolean(key, defaultValue) {
51
+ const raw = await this.get(key);
52
+ if (raw === void 0) return defaultValue;
53
+ const v = raw.toLowerCase();
54
+ if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
55
+ if (v === "false" || v === "0" || v === "no" || v === "off") return false;
56
+ this.logger?.warn?.(this.pluginName, "ConfigManager: invalid bool", {
57
+ key,
58
+ raw
59
+ });
60
+ return defaultValue;
61
+ }
62
+ };
63
+
64
+ export { ConfigManager };
65
+ //# sourceMappingURL=index.js.map
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @vibecontrols/plugin-sdk/contract
3
+ *
4
+ * The plugin contract v2 surface. Mirrors the agent's `HostServices` /
5
+ * `ProfileContext` / `PluginCapabilities` types but every host-side field
6
+ * is **optional** so a plugin author writing against the SDK can rely on
7
+ * partial host implementations (tests, alt-hosts, future agents that drop
8
+ * obsolete services). Plugins MUST optional-chain every host-side call.
9
+ *
10
+ * Source of truth (READ-ONLY mirror): vibecontrols-agent/src/core/plugin-system.ts:155-242,
11
+ * vibecontrols-agent/src/core/profile-context.ts:62-151,
12
+ * vibecontrols-agent/src/core/plugin-capabilities.ts.
13
+ */
14
+ interface PluginCapabilities {
15
+ storage?: "none" | "read" | "rw";
16
+ secrets?: "none" | "read" | "rw";
17
+ gateway?: boolean;
18
+ broadcast?: boolean;
19
+ subprocess?: boolean;
20
+ audit?: boolean;
21
+ telemetry?: boolean;
22
+ singletonOnly?: boolean;
23
+ requiresIsolation?: boolean;
24
+ }
25
+ declare const FULL_TRUST_CAPS: Required<PluginCapabilities>;
26
+ type PluginTag = "backend" | "frontend" | "cli" | "provider" | "adapter" | "integration";
27
+ type PrerequisiteKind = "binary" | "npm" | "pip" | "cargo" | "manual";
28
+ interface Prerequisite {
29
+ name: string;
30
+ kind: PrerequisiteKind;
31
+ requiresSudo: boolean;
32
+ version?: string;
33
+ install?: string;
34
+ }
35
+ interface StorageProvider {
36
+ get<T = unknown>(namespace: string, key: string): Promise<T | null>;
37
+ set<T = unknown>(namespace: string, key: string, value: T): Promise<void>;
38
+ delete(namespace: string, key: string): Promise<boolean>;
39
+ list?(namespace: string): Promise<string[]>;
40
+ }
41
+ interface ServiceRegistry {
42
+ registerService<T>(type: string, name: string, instance: T): void;
43
+ getService<T>(type: string, name: string): T | undefined;
44
+ listProvidersForType?(type: string): string[];
45
+ }
46
+ interface SdkLogger {
47
+ debug?(source: string, message: string, metadata?: Record<string, unknown>): void;
48
+ info?(source: string, message: string, metadata?: Record<string, unknown>): void;
49
+ warn?(source: string, message: string, metadata?: Record<string, unknown>): void;
50
+ error?(source: string, message: string, metadata?: Record<string, unknown>): void;
51
+ setLevel?(level: string): void;
52
+ }
53
+ /**
54
+ * The host surface a plugin can rely on. Every member is optional —
55
+ * downstream plugins must defensively use optional chaining or check
56
+ * presence before invoking. Mirrors agent's `HostServices` (plugin-system.ts:155-242)
57
+ * but type-loosened so SDK consumers don't need to depend on the agent.
58
+ */
59
+ interface HostServices {
60
+ storage?: StorageProvider;
61
+ logger?: SdkLogger;
62
+ serviceRegistry?: ServiceRegistry;
63
+ getProvider?<T = unknown>(type: string): T | undefined;
64
+ getAgentBaseUrl?(): string;
65
+ getAgentVersion?(): string;
66
+ broadcast?(type: string, payload: unknown): void;
67
+ workspaceQuery?<T = Record<string, unknown>>(query: string, variables?: Record<string, unknown>): Promise<{
68
+ data?: T;
69
+ errors?: Array<{
70
+ message: string;
71
+ }>;
72
+ }>;
73
+ isGatewayConfigured?(): boolean;
74
+ getAgentRecordId?(): Promise<string | null>;
75
+ getWorkspaceId?(): Promise<string | null>;
76
+ getConfig?(key: string): Promise<string | undefined>;
77
+ getPluginRegistry?(): string;
78
+ getDataDir?(): string;
79
+ cliContributors?: {
80
+ addStatusSection?(section: unknown): void;
81
+ addDoctorCheck?(check: unknown): void;
82
+ };
83
+ audit?: {
84
+ emit(event: string, payload?: Record<string, unknown>): void;
85
+ };
86
+ telemetry?: {
87
+ emit(event: string, payload?: Record<string, unknown>): void;
88
+ };
89
+ os?: unknown;
90
+ }
91
+ interface ProfileContext {
92
+ name: string;
93
+ dataDir: string;
94
+ logger?: SdkLogger;
95
+ audit?: {
96
+ emit(source: string, event: string, payload?: Record<string, unknown>): void;
97
+ };
98
+ telemetry?: {
99
+ emit(event: string, payload?: Record<string, unknown>): void;
100
+ };
101
+ }
102
+ interface VibePlugin {
103
+ name: string;
104
+ version: string;
105
+ description?: string;
106
+ tags?: PluginTag[];
107
+ capabilities?: PluginCapabilities;
108
+ prerequisites?: Prerequisite[];
109
+ cliCommand?: string;
110
+ apiPrefix?: string;
111
+ createRoutes?: () => unknown;
112
+ onServerStart?: (app: unknown, hostServices: HostServices) => void | Promise<void>;
113
+ onServerStop?: (hostServices: HostServices) => void | Promise<void>;
114
+ onCliSetup?: (program: unknown, hostServices: HostServices) => void;
115
+ }
116
+ type VibePluginFactory = (ctx: ProfileContext) => VibePlugin;
117
+
118
+ export { FULL_TRUST_CAPS, type HostServices, type PluginCapabilities, type PluginTag, type Prerequisite, type PrerequisiteKind, type ProfileContext, type SdkLogger, type ServiceRegistry, type StorageProvider, type VibePlugin, type VibePluginFactory };
@@ -0,0 +1,16 @@
1
+ // src/contract/index.ts
2
+ var FULL_TRUST_CAPS = {
3
+ storage: "rw",
4
+ secrets: "rw",
5
+ gateway: true,
6
+ broadcast: true,
7
+ subprocess: true,
8
+ audit: true,
9
+ telemetry: true,
10
+ singletonOnly: false,
11
+ requiresIsolation: false
12
+ };
13
+
14
+ export { FULL_TRUST_CAPS };
15
+ //# sourceMappingURL=index.js.map
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @vibecontrols/plugin-sdk/http
3
+ *
4
+ * Minimal `HttpClient` with timeout (AbortController), retries with
5
+ * exponential backoff, and JSON parse helpers. Wraps the platform-native
6
+ * `fetch` (Bun + Node 24 both ship it). No third-party HTTP deps so this
7
+ * module stays peerless.
8
+ */
9
+ interface HttpClientOptions {
10
+ /** Per-request timeout in ms. Default 10_000. */
11
+ timeoutMs?: number;
12
+ /** Total attempts (initial + retries). Default 3. */
13
+ maxAttempts?: number;
14
+ /** Default headers merged into every request. */
15
+ defaultHeaders?: Record<string, string>;
16
+ /** Override fetch implementation (tests). */
17
+ fetchImpl?: typeof fetch;
18
+ }
19
+ interface RequestOptions {
20
+ headers?: Record<string, string>;
21
+ signal?: AbortSignal;
22
+ }
23
+ declare class HttpClient {
24
+ private readonly baseUrl;
25
+ private readonly timeoutMs;
26
+ private readonly maxAttempts;
27
+ private readonly defaultHeaders;
28
+ private readonly fetchImpl;
29
+ constructor(baseUrl: string, options?: HttpClientOptions);
30
+ get<T>(path: string, options?: RequestOptions): Promise<T>;
31
+ post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
32
+ private request;
33
+ }
34
+
35
+ export { HttpClient, type HttpClientOptions, type RequestOptions };
@@ -0,0 +1,70 @@
1
+ // src/http/index.ts
2
+ var HttpClient = class {
3
+ constructor(baseUrl, options = {}) {
4
+ this.baseUrl = baseUrl;
5
+ this.timeoutMs = options.timeoutMs ?? 1e4;
6
+ this.maxAttempts = options.maxAttempts ?? 3;
7
+ this.defaultHeaders = options.defaultHeaders ?? {};
8
+ this.fetchImpl = options.fetchImpl ?? fetch;
9
+ }
10
+ baseUrl;
11
+ timeoutMs;
12
+ maxAttempts;
13
+ defaultHeaders;
14
+ fetchImpl;
15
+ async get(path, options) {
16
+ return this.request("GET", path, void 0, options);
17
+ }
18
+ async post(path, body, options) {
19
+ return this.request("POST", path, body, options);
20
+ }
21
+ async request(method, path, body, options) {
22
+ const url = `${this.baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
23
+ const headers = {
24
+ ...this.defaultHeaders,
25
+ ...options?.headers ?? {}
26
+ };
27
+ if (body !== void 0 && headers["content-type"] === void 0) {
28
+ headers["content-type"] = "application/json";
29
+ }
30
+ let lastError;
31
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
34
+ const callerSignal = options?.signal;
35
+ const onCallerAbort = () => controller.abort();
36
+ callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
37
+ try {
38
+ const res = await this.fetchImpl(url, {
39
+ method,
40
+ headers,
41
+ body: body === void 0 ? void 0 : JSON.stringify(body),
42
+ signal: controller.signal
43
+ });
44
+ if (!res.ok) {
45
+ throw new Error(`HTTP ${res.status} ${res.statusText} for ${method} ${url}`);
46
+ }
47
+ const text = await res.text();
48
+ if (!text) return void 0;
49
+ try {
50
+ return JSON.parse(text);
51
+ } catch {
52
+ return text;
53
+ }
54
+ } catch (err) {
55
+ lastError = err;
56
+ if (attempt >= this.maxAttempts) break;
57
+ const backoff = 100 * 2 ** (attempt - 1);
58
+ await new Promise((r) => setTimeout(r, backoff));
59
+ } finally {
60
+ clearTimeout(timer);
61
+ callerSignal?.removeEventListener("abort", onCallerAbort);
62
+ }
63
+ }
64
+ throw lastError instanceof Error ? lastError : new Error(`HttpClient: request failed for ${method} ${url}`);
65
+ }
66
+ };
67
+
68
+ export { HttpClient };
69
+ //# sourceMappingURL=index.js.map
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ export { FULL_TRUST_CAPS, HostServices, PluginCapabilities, PluginTag, Prerequisite, PrerequisiteKind, ProfileContext, SdkLogger, ServiceRegistry, StorageProvider, VibePlugin, VibePluginFactory } from './contract/index.js';
2
+ export { LifecycleHooks, LifecycleSpec, createLifecycleHooks } from './lifecycle/index.js';
3
+ export { CliCommandBuilder, MultimodeOptions, OutputFlags, OutputMode, StatusCommandSpec, maybePrintJson, pickOutputMode, redact, runMultimode } from './cli/index.js';
4
+ export { ApiKeyResolver, RoutesBuilder } from './routes/index.js';
5
+ export { TelemetryEmitter } from './telemetry/index.js';
6
+ export { BoundLogger } from './log/index.js';
7
+ export { NamespaceStore, TypedStore } from './storage/index.js';
8
+ export { ConfigManager } from './config/index.js';
9
+ export { findAvailablePort, gracefulKill, isProcessAlive, sleep } from './subprocess/index.js';
10
+ export { HttpClient, HttpClientOptions, RequestOptions } from './http/index.js';
11
+ export { CliContribution, ProviderRegistry } from './providers/index.js';
12
+ export { AuditLogger } from './audit/index.js';
13
+ export { BroadcastEmitter } from './broadcast/index.js';
14
+ import 'commander';
15
+ import 'elysia';