@code-first-agents/tool 0.1.0

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 Juan Gipponi
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,76 @@
1
+ # @code-first-agents/tool
2
+
3
+ ![CI](https://github.com/beogip/code-first-agents-tool/actions/workflows/ci.yml/badge.svg)
4
+ [![npm](https://img.shields.io/npm/v/@code-first-agents/tool)](https://www.npmjs.com/package/@code-first-agents/tool)
5
+
6
+ Code-first agent tool definitions with Zod schemas.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ bun add @code-first-agents/tool
12
+ # or
13
+ npm install @code-first-agents/tool
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { Tool, l1Output } from "@code-first-agents/tool";
20
+ import { z } from "zod";
21
+
22
+ const greet = new Tool({
23
+ name: "greet",
24
+ description: "Say hello",
25
+ input: z.object({ name: z.string() }),
26
+ handler: async ({ input }) => l1Output("pass", `Hello, ${input.name}!`),
27
+ });
28
+ ```
29
+
30
+ ## Development
31
+
32
+ **Prerequisites:** [Bun](https://bun.sh) >= 1.0
33
+
34
+ ```bash
35
+ git clone https://github.com/beogip/code-first-agents-tool.git
36
+ cd code-first-agents-tool
37
+ bun install
38
+ ```
39
+
40
+ | Command | Description |
41
+ | ---------------- | ------------------------------- |
42
+ | `bun run dev` | Run with file watcher |
43
+ | `bun run build` | Compile to `dist/` (bun + tsc) |
44
+ | `bun test` | Run tests |
45
+ | `bun run check` | Lint + format (Biome, auto-fix) |
46
+ | `bun run lint` | Lint only |
47
+ | `bun run format` | Format only |
48
+
49
+ ## Project Structure
50
+
51
+ ```
52
+ src/ # Source code
53
+ tests/ # Test files (*.test.ts)
54
+ dist/ # Build output (git-ignored)
55
+ ```
56
+
57
+ ## Git Hooks
58
+
59
+ [Lefthook](https://github.com/evilmartians/lefthook) runs automatically after `bun install` (via the `prepare` script):
60
+
61
+ - **pre-commit** — Biome checks and auto-fixes staged files
62
+ - **commit-msg** — Validates [Conventional Commits](https://www.conventionalcommits.org/) format
63
+
64
+ ## Releases
65
+
66
+ Releases are automated via [semantic-release](https://semantic-release.gitbook.io/) on every push to `main`:
67
+
68
+ - `feat:` → minor release
69
+ - `fix:` → patch release
70
+ - `feat!:` or `BREAKING CHANGE:` → major release
71
+
72
+ The CI workflow handles changelog generation, npm publishing, GitHub releases, and version bumping automatically.
73
+
74
+ ## License
75
+
76
+ [MIT](LICENSE)
package/dist/args.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * args.ts — CLI argv parsing and input validation for the Tool base class.
3
+ *
4
+ * Contains only the pre-dispatch concerns: turning `process.argv.slice(2)`
5
+ * into a structured `{subcommand, ParsedArgs}` pair and validating the
6
+ * parsed args against a subcommand's Zod input schema.
7
+ *
8
+ * @module code-first-agents-tool/args
9
+ */
10
+ import type { z } from "zod";
11
+ import type { ParsedArgs } from "./types.ts";
12
+ /**
13
+ * Subcommand names reserved by the base class (`schema`, `help`).
14
+ * Module-private: not re-exported from `./index.ts`.
15
+ */
16
+ export declare const RESERVED_SUBCOMMANDS: ReadonlySet<string>;
17
+ /**
18
+ * Parse a raw argv slice into subcommand + typed flags + positional args.
19
+ *
20
+ * Rules:
21
+ * - `argv[0]` is the subcommand. A leading `--` prefix is stripped so that
22
+ * `--help` and `help` both resolve to `"help"`. The bare `--` sentinel
23
+ * yields an empty subcommand.
24
+ * - When `argv[0]` is NOT already a reserved builtin, a `--help` or
25
+ * `--schema` flag in the remaining tokens (up to the `--` sentinel)
26
+ * promotes to the subcommand (global-flag override).
27
+ * - Tokens starting with `--` are flag keys; the next token (if not also a
28
+ * flag) is the value. A bare `--flag` at end-of-argv or followed by
29
+ * another `--flag` resolves to `true`.
30
+ * - Repeated `--flag v1 --flag v2` → last-one-wins (`v2`).
31
+ * - The bare `--` token is the end-of-options sentinel (POSIX convention):
32
+ * all subsequent tokens become positional, even if they start with `--`.
33
+ * - Non-flag tokens become positional args in order.
34
+ *
35
+ * Pure function — no I/O, no side effects.
36
+ *
37
+ * @param argv - Array of CLI tokens, typically `process.argv.slice(2)`.
38
+ * @returns Parsed subcommand + {@link ParsedArgs}.
39
+ */
40
+ export declare function parseArgs(argv: string[]): {
41
+ subcommand: string;
42
+ parsed: ParsedArgs;
43
+ };
44
+ /**
45
+ * Validate raw {@link ParsedArgs} against a subcommand's input Zod schema.
46
+ * Flags are merged as-is; positional args are attached under a reserved `_`
47
+ * key only when present (so strict schemas without `_` stay happy when no
48
+ * positional args are passed).
49
+ *
50
+ * @param parsed - Raw parsed args from {@link parseArgs}.
51
+ * @param inputSchema - The subcommand's input Zod schema.
52
+ * @returns The Zod `safeParse` result carrying either validated data or a structured error.
53
+ */
54
+ export declare function validateInput<I extends z.ZodTypeAny>(parsed: ParsedArgs, inputSchema: I): z.ZodSafeParseResult<z.core.output<I>>;
55
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAA+B,CAAC;AAkBrF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,CA6CpF;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,0CAMvF"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * envelopes.ts — Typed error-envelope factories for the Tool base class.
3
+ *
4
+ * The four error codes (`unknown_subcommand`, `input_validation_error`,
5
+ * `schema_violation`, `unexpected_error`) are modeled as a discriminated
6
+ * union so extras (`subcommands`, `input_schema`, `detail`) stay type-safe
7
+ * per-code. Each builder owns the exact message template for its code,
8
+ * which previously lived inline in `Tool#run`.
9
+ *
10
+ * @module code-first-agents-tool/envelopes
11
+ */
12
+ import type { z } from "zod";
13
+ import { type HelpPayload } from "./introspection.ts";
14
+ import type { SubcommandSpec } from "./types.ts";
15
+ /**
16
+ * Discriminated union of every error envelope the base class can emit.
17
+ * Each variant is assignable to `ToolOutput` from `./utils.ts` via its
18
+ * index signature, so `jsonOutput` consumes them without casts.
19
+ *
20
+ * The final variant (`error: string`) covers handler-emitted business
21
+ * errors thrown via {@link ToolError} — the `error` code is whatever the
22
+ * handler chose.
23
+ */
24
+ export type ErrorEnvelope = {
25
+ ok: false;
26
+ error: "unknown_subcommand";
27
+ message: string;
28
+ subcommands: HelpPayload;
29
+ } | {
30
+ ok: false;
31
+ error: "input_validation_error";
32
+ message: string;
33
+ detail: string;
34
+ input_schema: unknown;
35
+ } | {
36
+ ok: false;
37
+ error: "schema_violation";
38
+ message: string;
39
+ detail: string;
40
+ } | {
41
+ ok: false;
42
+ error: "non_object_return";
43
+ message: string;
44
+ } | {
45
+ ok: false;
46
+ error: "unexpected_error";
47
+ message: string;
48
+ detail?: string | Record<string, unknown>;
49
+ } | {
50
+ ok: false;
51
+ error: string;
52
+ message: string;
53
+ detail?: string | Record<string, unknown>;
54
+ };
55
+ /**
56
+ * Error class a handler can throw to emit a structured error envelope with
57
+ * a business-specific `error` code (e.g. `"path_not_found"`, `"rate_limited"`).
58
+ * The base class catches instances of this class and produces the envelope
59
+ * via {@link toolErrorEnvelope}; anything else thrown becomes an
60
+ * `unexpected_error` envelope.
61
+ *
62
+ * @example
63
+ * handler: ({ path }) => {
64
+ * if (!existsSync(path)) {
65
+ * throw new ToolError("path_not_found", `Path does not exist: ${path}`);
66
+ * }
67
+ * return { message: "ok", ... };
68
+ * }
69
+ */
70
+ export declare class ToolError extends Error {
71
+ /** Machine-readable error code emitted in the envelope's `error` field. */
72
+ readonly code: string;
73
+ /** Optional extra context included in the envelope's `detail` field. */
74
+ readonly detail?: string | Record<string, unknown>;
75
+ /**
76
+ * @param code - Machine-readable error code (emitted verbatim). Conventionally snake_case.
77
+ * @param message - Human-readable message.
78
+ * @param detail - Optional extra context (string or structured object).
79
+ */
80
+ constructor(code: string, message: string, detail?: string | Record<string, unknown>);
81
+ }
82
+ /**
83
+ * Build the `unknown_subcommand` envelope. When `name === ""` (empty argv),
84
+ * the message notes the absence; otherwise it names the unrecognized input.
85
+ * Always includes the full `subcommands` listing so an LLM caller can
86
+ * self-correct on retry.
87
+ *
88
+ * @param name - The unrecognized subcommand (or empty string for empty argv).
89
+ * @param subs - The Tool's registered-subcommands map.
90
+ * @returns A typed `unknown_subcommand` envelope.
91
+ */
92
+ export declare function unknownSubcommandEnvelope(name: string, subs: ReadonlyMap<string, SubcommandSpec<z.ZodTypeAny, z.ZodTypeAny>>): ErrorEnvelope;
93
+ /**
94
+ * Build the `input_validation_error` envelope. Attaches the input JSON
95
+ * Schema (or `$error` fallback) so the LLM can correct the flags and retry.
96
+ *
97
+ * @param name - The subcommand whose input failed validation.
98
+ * @param zerr - The Zod error produced by `safeParse`.
99
+ * @param inputSchema - The subcommand's declared input Zod schema.
100
+ * @returns A typed `input_validation_error` envelope.
101
+ */
102
+ export declare function inputValidationErrorEnvelope(name: string, zerr: z.ZodError, inputSchema: z.ZodTypeAny): ErrorEnvelope;
103
+ /**
104
+ * Build the `schema_violation` envelope. Fires when a handler returns a
105
+ * value that fails the subcommand's output Zod schema.
106
+ *
107
+ * @param name - The subcommand whose handler return failed validation.
108
+ * @param zerr - The Zod error produced by output `safeParse`.
109
+ * @returns A typed `schema_violation` envelope.
110
+ */
111
+ export declare function schemaViolationEnvelope(name: string, zerr: z.ZodError): ErrorEnvelope;
112
+ /**
113
+ * Build the `unexpected_error` envelope. Catches anything the handler (or
114
+ * the base class plumbing itself) throws that is NOT a {@link ToolError}.
115
+ * The error's stack is included in `detail` when available.
116
+ *
117
+ * @param err - The thrown value (or rejected Promise reason).
118
+ * @returns A typed `unexpected_error` envelope.
119
+ */
120
+ export declare function unexpectedErrorEnvelope(err: unknown): ErrorEnvelope;
121
+ /**
122
+ * Build a handler-emitted error envelope from a {@link ToolError}. Preserves
123
+ * the custom `code`, `message`, and optional `detail` the handler declared,
124
+ * so consumers see a business-specific error envelope instead of a generic
125
+ * `unexpected_error`.
126
+ *
127
+ * @param err - The {@link ToolError} the handler threw.
128
+ * @returns An envelope carrying the handler's custom `error` code.
129
+ */
130
+ export declare function toolErrorEnvelope(err: ToolError): ErrorEnvelope;
131
+ /**
132
+ * Build the `non_object_return` envelope. Fires when a handler returns a
133
+ * non-plain-object value (null, string, array, etc.) that cannot be spread
134
+ * into the output envelope.
135
+ *
136
+ * @param name - The subcommand whose handler returned a non-object.
137
+ * @param value - The actual value the handler returned (used to describe the type).
138
+ * @returns A typed `non_object_return` envelope.
139
+ */
140
+ export declare function nonObjectReturnEnvelope(name: string, value: unknown): ErrorEnvelope;
141
+ //# sourceMappingURL=envelopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelopes.d.ts","sourceRoot":"","sources":["../src/envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,oBAAoB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,WAAW,CAAC;CAC1B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,wBAAwB,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;CACvB,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,kBAAkB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,kBAAkB,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C,CAAC;AAEN;;;;;;;;;;;;;;GAcG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,2EAA2E;IAC3E,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,SAAgB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE1D;;;;OAIG;gBACS,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAQrF;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,GACpE,aAAa,CAWf;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,CAAC,CAAC,QAAQ,EAChB,WAAW,EAAE,CAAC,CAAC,UAAU,GACxB,aAAa,CAUf;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,GAAG,aAAa,CAOrF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAMnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,SAAS,GAAG,aAAa,CAI/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,aAAa,CAOnF"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * index.ts — Barrel for the code-first-agents Tool base class.
3
+ *
4
+ * Implementation lives in `src/*.ts`. This file re-exports the public API
5
+ * so consumers import from the package root:
6
+ *
7
+ * import { Tool, l1Output, parseArgs, validateInput } from "@code-first-agents/tool";
8
+ *
9
+ * @module @code-first-agents/tool
10
+ */
11
+ export { parseArgs, validateInput } from "./args.ts";
12
+ export type { ErrorEnvelope } from "./envelopes.ts";
13
+ export { ToolError } from "./envelopes.ts";
14
+ export type { HelpPayload, HelpPayloadEntry, SchemaOutputEntry, } from "./introspection.ts";
15
+ export { buildHelpPayload, buildSchemaOutput } from "./introspection.ts";
16
+ export type { JSONSchemaResult } from "./json-schema.ts";
17
+ export { l1Output, l2Output, l3Output } from "./output-helpers.ts";
18
+ export { Tool } from "./tool-class.ts";
19
+ export type { HandlerReturn, ParsedArgs, SubcommandSpec, ToolMeta, } from "./types.ts";
20
+ export type { ToolOutput } from "./utils.ts";
21
+ export { jsonOutput, stringifyError } from "./utils.ts";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACzE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,QAAQ,GACT,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,316 @@
1
+ // src/args.ts
2
+ var RESERVED_SUBCOMMANDS = new Set(["schema", "help"]);
3
+ function normalizeSubcommand(raw) {
4
+ if (raw === undefined || raw === "--")
5
+ return "";
6
+ return raw.startsWith("--") ? raw.slice(2) : raw;
7
+ }
8
+ function parseArgs(argv) {
9
+ let subcommand = normalizeSubcommand(argv[0]);
10
+ const flags = {};
11
+ const positional = [];
12
+ let i = 1;
13
+ while (i < argv.length) {
14
+ const tok = argv[i];
15
+ if (tok === "--") {
16
+ positional.push(...argv.slice(i + 1));
17
+ break;
18
+ }
19
+ if (!tok.startsWith("--")) {
20
+ positional.push(tok);
21
+ i += 1;
22
+ continue;
23
+ }
24
+ const key = tok.slice(2);
25
+ if (RESERVED_SUBCOMMANDS.has(key) && !RESERVED_SUBCOMMANDS.has(subcommand)) {
26
+ subcommand = key;
27
+ i += 1;
28
+ continue;
29
+ }
30
+ const next = argv[i + 1];
31
+ if (next === undefined || next.startsWith("--")) {
32
+ flags[key] = true;
33
+ i += 1;
34
+ continue;
35
+ }
36
+ flags[key] = next;
37
+ i += 2;
38
+ }
39
+ return { subcommand, parsed: { flags, positional } };
40
+ }
41
+ function validateInput(parsed, inputSchema) {
42
+ const toValidate = { ...parsed.flags };
43
+ if (parsed.positional.length > 0) {
44
+ toValidate._ = parsed.positional;
45
+ }
46
+ return inputSchema.safeParse(toValidate);
47
+ }
48
+ // src/json-schema.ts
49
+ import { z } from "zod";
50
+ function safeToJSONSchema(schema) {
51
+ try {
52
+ return { ok: true, schema: z.toJSONSchema(schema, { target: "draft-2020-12" }) };
53
+ } catch (err) {
54
+ return { ok: false, $error: err instanceof Error ? err.message : String(err) };
55
+ }
56
+ }
57
+
58
+ // src/introspection.ts
59
+ function buildSchemaOutput(subs) {
60
+ const result = {};
61
+ for (const [name, spec] of subs) {
62
+ const inputResult = safeToJSONSchema(spec.input);
63
+ const outputResult = safeToJSONSchema(spec.output);
64
+ if (!inputResult.ok) {
65
+ result[name] = { $error: inputResult.$error };
66
+ } else if (!outputResult.ok) {
67
+ result[name] = { $error: outputResult.$error };
68
+ } else {
69
+ result[name] = { input: inputResult.schema, output: outputResult.schema };
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ function buildHelpPayload(subs) {
75
+ const result = [];
76
+ for (const [name, spec] of subs) {
77
+ const conv = safeToJSONSchema(spec.input);
78
+ const input_schema = conv.ok ? conv.schema : { $error: conv.$error };
79
+ result.push({ name, description: spec.description, input_schema });
80
+ }
81
+ return result;
82
+ }
83
+
84
+ // src/utils.ts
85
+ function jsonOutput(data) {
86
+ console.log(JSON.stringify(data));
87
+ process.exit(0);
88
+ }
89
+ function stringifyError(err) {
90
+ return err instanceof Error ? err.message : String(err);
91
+ }
92
+
93
+ // src/envelopes.ts
94
+ class ToolError extends Error {
95
+ code;
96
+ detail;
97
+ constructor(code, message, detail) {
98
+ super(message);
99
+ this.name = "ToolError";
100
+ this.code = code;
101
+ if (detail !== undefined) {
102
+ this.detail = detail;
103
+ }
104
+ }
105
+ }
106
+ function unknownSubcommandEnvelope(name, subs) {
107
+ const message = name === "" ? "No subcommand provided — see 'subcommands' for available options" : `Unknown subcommand '${name}' — see 'subcommands' for available options`;
108
+ return {
109
+ ok: false,
110
+ error: "unknown_subcommand",
111
+ message,
112
+ subcommands: buildHelpPayload(subs)
113
+ };
114
+ }
115
+ function inputValidationErrorEnvelope(name, zerr, inputSchema) {
116
+ const conv = safeToJSONSchema(inputSchema);
117
+ const input_schema = conv.ok ? conv.schema : { $error: conv.$error };
118
+ return {
119
+ ok: false,
120
+ error: "input_validation_error",
121
+ message: `Input validation failed for subcommand '${name}'`,
122
+ detail: zerr.message,
123
+ input_schema
124
+ };
125
+ }
126
+ function schemaViolationEnvelope(name, zerr) {
127
+ return {
128
+ ok: false,
129
+ error: "schema_violation",
130
+ message: `Handler output failed schema validation for subcommand '${name}'`,
131
+ detail: zerr.message
132
+ };
133
+ }
134
+ function unexpectedErrorEnvelope(err) {
135
+ const message = stringifyError(err);
136
+ const stack = err instanceof Error ? err.stack : undefined;
137
+ return stack !== undefined ? { ok: false, error: "unexpected_error", message, detail: stack } : { ok: false, error: "unexpected_error", message };
138
+ }
139
+ function toolErrorEnvelope(err) {
140
+ return err.detail !== undefined ? { ok: false, error: err.code, message: err.message, detail: err.detail } : { ok: false, error: err.code, message: err.message };
141
+ }
142
+ function nonObjectReturnEnvelope(name, value) {
143
+ const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
144
+ return {
145
+ ok: false,
146
+ error: "non_object_return",
147
+ message: `Handler for '${name}' must return a plain object, got ${type}`
148
+ };
149
+ }
150
+ // src/output-helpers.ts
151
+ import { z as z2 } from "zod";
152
+ function l1Output(fields) {
153
+ return z2.object({
154
+ ok: z2.literal(true),
155
+ message: z2.string(),
156
+ ...fields
157
+ });
158
+ }
159
+ function l2Output(classification, fields) {
160
+ return z2.object({
161
+ ok: z2.literal(true),
162
+ message: z2.string(),
163
+ classification,
164
+ ...fields ?? {}
165
+ });
166
+ }
167
+ function l3Output(fields) {
168
+ return z2.object({
169
+ ok: z2.literal(true),
170
+ message: z2.string(),
171
+ instructions: z2.string(),
172
+ ...fields ?? {}
173
+ });
174
+ }
175
+ // src/tool-class.ts
176
+ import { z as z3 } from "zod";
177
+ function isPlainObject(value) {
178
+ return typeof value === "object" && value !== null && !Array.isArray(value);
179
+ }
180
+
181
+ class Tool {
182
+ meta;
183
+ subs = new Map;
184
+ constructor(meta) {
185
+ this.meta = meta;
186
+ }
187
+ subcommand(spec) {
188
+ this.validateRegistration(spec);
189
+ this.subs.set(spec.name, spec);
190
+ return this;
191
+ }
192
+ async run(argv) {
193
+ try {
194
+ const { subcommand, parsed } = parseArgs(argv);
195
+ const builtin = this.dispatchBuiltin(subcommand);
196
+ if (builtin)
197
+ return jsonOutput(builtin);
198
+ const miss = this.rejectUnknown(subcommand);
199
+ if (miss)
200
+ return jsonOutput(miss);
201
+ return jsonOutput(await this.runSubcommand(subcommand, parsed));
202
+ } catch (err) {
203
+ if (err instanceof ToolError)
204
+ return jsonOutput(toolErrorEnvelope(err));
205
+ return jsonOutput(unexpectedErrorEnvelope(err));
206
+ }
207
+ }
208
+ validateRegistration(spec) {
209
+ if (RESERVED_SUBCOMMANDS.has(spec.name)) {
210
+ throw new RangeError(`Subcommand name "${spec.name}" is reserved — the base class auto-registers 'schema' and 'help'`);
211
+ }
212
+ if (this.subs.has(spec.name)) {
213
+ throw new RangeError(`Subcommand "${spec.name}" is already registered`);
214
+ }
215
+ if (spec.input === undefined || spec.input === null) {
216
+ throw new TypeError(`Subcommand "${spec.name}" is missing required 'input' Zod schema (use z.object({}).strict() for argless subs)`);
217
+ }
218
+ if (spec.output === undefined || spec.output === null) {
219
+ throw new TypeError(`Subcommand "${spec.name}" is missing required 'output' Zod schema (use l1Output({}) for minimal output)`);
220
+ }
221
+ }
222
+ dispatchBuiltin(name) {
223
+ if (name === "schema") {
224
+ return {
225
+ ok: true,
226
+ message: this.subs.size === 0 ? `Tool "${this.meta.name}" has no subcommands registered` : `JSON Schemas for ${this.subs.size} subcommand(s)`,
227
+ schemas: buildSchemaOutput(this.subs)
228
+ };
229
+ }
230
+ if (name === "help") {
231
+ return {
232
+ ok: true,
233
+ message: this.meta.description || `Help for ${this.meta.name}`,
234
+ tool: { name: this.meta.name },
235
+ subcommands: buildHelpPayload(this.subs)
236
+ };
237
+ }
238
+ return null;
239
+ }
240
+ rejectUnknown(name) {
241
+ if (name === "" || !this.subs.has(name)) {
242
+ return unknownSubcommandEnvelope(name, this.subs);
243
+ }
244
+ return null;
245
+ }
246
+ async runSubcommand(name, parsed) {
247
+ const spec = this.subs.get(name);
248
+ if (!spec) {
249
+ return unexpectedErrorEnvelope(new Error(`Subcommand '${name}' vanished between rejectUnknown and runSubcommand`));
250
+ }
251
+ const inputResult = validateInput(parsed, spec.input);
252
+ if (!inputResult.success) {
253
+ return inputValidationErrorEnvelope(name, inputResult.error, spec.input);
254
+ }
255
+ const handlerResult = await spec.handler(inputResult.data);
256
+ return this.validateOutput(name, handlerResult, spec);
257
+ }
258
+ async invoke(subcommand, args = {}) {
259
+ try {
260
+ const builtin = this.dispatchBuiltin(subcommand);
261
+ if (builtin)
262
+ return builtin;
263
+ const miss = this.rejectUnknown(subcommand);
264
+ if (miss)
265
+ return miss;
266
+ const spec = this.subs.get(subcommand);
267
+ if (!spec) {
268
+ return unexpectedErrorEnvelope(new Error(`Subcommand '${subcommand}' vanished between rejectUnknown and invoke`));
269
+ }
270
+ const inputResult = spec.input.safeParse(args);
271
+ if (!inputResult.success) {
272
+ return inputValidationErrorEnvelope(subcommand, inputResult.error, spec.input);
273
+ }
274
+ const handlerResult = await spec.handler(inputResult.data);
275
+ return this.validateOutput(subcommand, handlerResult, spec);
276
+ } catch (err) {
277
+ if (err instanceof ToolError)
278
+ return toolErrorEnvelope(err);
279
+ return unexpectedErrorEnvelope(err);
280
+ }
281
+ }
282
+ validateOutput(name, handlerResult, spec) {
283
+ if (!isPlainObject(handlerResult)) {
284
+ return nonObjectReturnEnvelope(name, handlerResult);
285
+ }
286
+ const fullResult = { ...handlerResult, ok: true };
287
+ const outputResult = spec.output.safeParse(fullResult);
288
+ if (!outputResult.success) {
289
+ return schemaViolationEnvelope(name, outputResult.error);
290
+ }
291
+ const data = outputResult.data;
292
+ if (!("ok" in data)) {
293
+ return schemaViolationEnvelope(name, new z3.ZodError([
294
+ {
295
+ code: "custom",
296
+ path: ["ok"],
297
+ message: "Output schema is missing 'ok' field — add ok: z.literal(true) or use l1Output/l2Output/l3Output helpers"
298
+ }
299
+ ]));
300
+ }
301
+ return data;
302
+ }
303
+ }
304
+ export {
305
+ validateInput,
306
+ stringifyError,
307
+ parseArgs,
308
+ l3Output,
309
+ l2Output,
310
+ l1Output,
311
+ jsonOutput,
312
+ buildSchemaOutput,
313
+ buildHelpPayload,
314
+ ToolError,
315
+ Tool
316
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * introspection.ts — Builders for the auto-registered `schema` and `help`
3
+ * subcommand payloads, plus the help listing carried inside the
4
+ * `unknown_subcommand` error envelope.
5
+ *
6
+ * Both builders delegate to `safeToJSONSchema` so exotic Zod constructs
7
+ * fail soft (one subcommand emits `{$error}` instead of crashing the whole
8
+ * response).
9
+ *
10
+ * @module code-first-agents-tool/introspection
11
+ */
12
+ import type { z } from "zod";
13
+ import type { SubcommandSpec } from "./types.ts";
14
+ /** One entry in the `subcommands` help listing. */
15
+ export interface HelpPayloadEntry {
16
+ /** Subcommand name as registered. */
17
+ name: string;
18
+ /** One-line description from the spec. */
19
+ description: string;
20
+ /** Input JSON Schema (draft-2020-12) or `{$error}` fallback. */
21
+ input_schema: unknown;
22
+ }
23
+ /** The shape returned by {@link buildHelpPayload} — an array of entries. */
24
+ export type HelpPayload = HelpPayloadEntry[];
25
+ /** One entry in the `schemas` map emitted by the `schema` subcommand. */
26
+ export type SchemaOutputEntry = {
27
+ input: unknown;
28
+ output: unknown;
29
+ } | {
30
+ $error: string;
31
+ };
32
+ /**
33
+ * Convert a registered subcommand map into a `{name: {input, output}}`
34
+ * record of JSON Schemas (draft-2020-12). Per-subcommand conversion is
35
+ * fail-soft via {@link safeToJSONSchema}: a failing sub gets a `$error`
36
+ * entry instead of crashing the whole `schema` response.
37
+ *
38
+ * @param subs - Map from subcommand name to its spec.
39
+ * @returns Record keyed by subcommand name, each value either `{input, output}` or `{$error}`.
40
+ */
41
+ export declare function buildSchemaOutput(subs: ReadonlyMap<string, SubcommandSpec<z.ZodTypeAny, z.ZodTypeAny>>): Record<string, SchemaOutputEntry>;
42
+ /**
43
+ * Build the `subcommands` array used by `help` output and the
44
+ * `unknown_subcommand` error envelope. Each entry carries the input JSON
45
+ * Schema so an LLM caller can discover which flags a subcommand expects.
46
+ *
47
+ * @param subs - Map from subcommand name to its spec.
48
+ * @returns Array of `{name, description, input_schema}` entries.
49
+ */
50
+ export declare function buildHelpPayload(subs: ReadonlyMap<string, SubcommandSpec<z.ZodTypeAny, z.ZodTypeAny>>): HelpPayload;
51
+ //# sourceMappingURL=introspection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,4EAA4E;AAC5E,MAAM,MAAM,WAAW,GAAG,gBAAgB,EAAE,CAAC;AAE7C,yEAAyE;AACzE,MAAM,MAAM,iBAAiB,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,GACpE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAcnC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,GACpE,WAAW,CAQb"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * json-schema.ts — Safe wrapper around `z.toJSONSchema`.
3
+ *
4
+ * `z.toJSONSchema` throws on exotic Zod constructs (e.g. `.transform()`).
5
+ * This helper centralizes the try/catch and the target version
6
+ * (`draft-2020-12`) so the three call sites — `buildSchemaOutput`,
7
+ * `buildHelpPayload`, and the `input_validation_error` envelope builder —
8
+ * share a single fail-soft path.
9
+ *
10
+ * **Known limitation:** Zod v4's `toJSONSchema` emits `required` for fields
11
+ * that have `.default(...)`. The emitted JSON Schema says the field is
12
+ * mandatory even though Zod's runtime validation accepts its absence and
13
+ * fills the default. This can mislead LLM callers reading `input_schema`
14
+ * for self-correction (they may conclude a defaulted flag is required).
15
+ * No workaround applied — consumers should test with the actual tool, not
16
+ * rely on the schema's `required` array for defaulted fields.
17
+ *
18
+ * @module code-first-agents-tool/json-schema
19
+ */
20
+ import { z } from "zod";
21
+ /** Result of converting a Zod schema to JSON Schema — success carries the schema; failure carries a message. */
22
+ export type JSONSchemaResult = {
23
+ ok: true;
24
+ schema: unknown;
25
+ } | {
26
+ ok: false;
27
+ $error: string;
28
+ };
29
+ /**
30
+ * Convert a Zod schema to JSON Schema (draft-2020-12) without throwing.
31
+ * On success returns `{ ok: true, schema }`; on failure returns
32
+ * `{ ok: false, $error }` carrying the thrown message.
33
+ *
34
+ * @param schema - Any Zod schema.
35
+ * @returns A discriminated result — never throws.
36
+ */
37
+ export declare function safeToJSONSchema(schema: z.ZodTypeAny): JSONSchemaResult;
38
+ //# sourceMappingURL=json-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,gHAAgH;AAChH,MAAM,MAAM,gBAAgB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7F;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,gBAAgB,CAMvE"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * output-helpers.ts — Level-specific output-schema composers for the
3
+ * code-first-agents Tool contract.
4
+ *
5
+ * Every tool output includes the `ok: z.literal(true)` + `message: z.string()`
6
+ * envelope. These helpers bake that in and add the fields the spec requires
7
+ * for each level (none for L1; required `classification` enum for L2;
8
+ * required `instructions` string for L3), so the caller can't forget them
9
+ * and VS Code shows a fully-resolved handler return type.
10
+ *
11
+ * Opt-in: tools with shapes that don't fit a level may pass a raw
12
+ * `z.object({...})` to `output`. The base class doesn't require a helper.
13
+ *
14
+ * @module code-first-agents-tool/output-helpers
15
+ */
16
+ import { z } from "zod";
17
+ /**
18
+ * Compose an **L1 (data)** output schema: envelope + arbitrary raw fields.
19
+ * Use when the tool returns facts for the LLM to interpret.
20
+ *
21
+ * @example
22
+ * const schema = l1Output({ checkboxes: z.number(), file_paths: z.number() });
23
+ *
24
+ * @param fields - Raw data fields to include alongside the envelope.
25
+ * @returns A `z.object` carrying `ok`, `message`, and the caller's fields.
26
+ */
27
+ export declare function l1Output<T extends z.ZodRawShape>(fields: T): z.ZodObject<{
28
+ ok: z.ZodLiteral<true>;
29
+ message: z.ZodString;
30
+ } & T extends infer T_1 ? { -readonly [P in keyof T_1]: T_1[P]; } : never, z.core.$strip>;
31
+ /**
32
+ * Compose an **L2 (classification)** output schema: envelope + a required
33
+ * `classification` enum + optional extras. Use when the tool returns a
34
+ * discrete category the skill can branch on.
35
+ *
36
+ * @example
37
+ * const schema = l2Output(
38
+ * z.enum(["lean", "standard", "full"]),
39
+ * { score: z.number(), signals: z.object({ checkboxes: z.number() }) },
40
+ * );
41
+ *
42
+ * @param classification - Zod enum describing the discrete classification result.
43
+ * @param fields - Optional extras (e.g. score, raw signals) merged into the output.
44
+ * @returns A `z.object` carrying `ok`, `message`, `classification`, and extras.
45
+ */
46
+ export declare function l2Output<C extends z.ZodTypeAny, T extends z.ZodRawShape>(classification: C, fields: T): z.ZodObject<{
47
+ ok: z.ZodLiteral<true>;
48
+ message: z.ZodString;
49
+ classification: C;
50
+ } & T>;
51
+ export declare function l2Output<C extends z.ZodTypeAny>(classification: C): z.ZodObject<{
52
+ ok: z.ZodLiteral<true>;
53
+ message: z.ZodString;
54
+ classification: C;
55
+ }>;
56
+ /**
57
+ * Compose an **L3 (instructions)** output schema: envelope + a required
58
+ * `instructions` string + optional extras. Use when the tool builds a
59
+ * verbatim procedure for the LLM to execute.
60
+ *
61
+ * @example
62
+ * const schema = l3Output({ plan_level: z.enum(["lean", "standard", "full"]) });
63
+ *
64
+ * @param fields - Optional extras (e.g. classification alongside instructions) merged into the output.
65
+ * @returns A `z.object` carrying `ok`, `message`, `instructions`, and extras.
66
+ */
67
+ export declare function l3Output<T extends z.ZodRawShape>(fields: T): z.ZodObject<{
68
+ ok: z.ZodLiteral<true>;
69
+ message: z.ZodString;
70
+ instructions: z.ZodString;
71
+ } & T>;
72
+ export declare function l3Output(): z.ZodObject<{
73
+ ok: z.ZodLiteral<true>;
74
+ message: z.ZodString;
75
+ instructions: z.ZodString;
76
+ }>;
77
+ //# sourceMappingURL=output-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output-helpers.d.ts","sourceRoot":"","sources":["../src/output-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;;;0FAM1D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,CAAC,SAAS,CAAC,CAAC,WAAW,EACtE,cAAc,EAAE,CAAC,EACjB,MAAM,EAAE,CAAC,GACR,CAAC,CAAC,SAAS,CAAC;IAAE,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC;IAAC,cAAc,EAAE,CAAC,CAAA;CAAE,GAAG,CAAC,CAAC,CAAC;AAExF,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAC7C,cAAc,EAAE,CAAC,GAChB,CAAC,CAAC,SAAS,CAAC;IAAE,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC;IAAC,cAAc,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAUpF;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAC9C,MAAM,EAAE,CAAC,GACR,CAAC,CAAC,SAAS,CAAC;IAAE,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC;IAAC,YAAY,EAAE,CAAC,CAAC,SAAS,CAAA;CAAE,GAAG,CAAC,CAAC,CAAC;AAEhG,wBAAgB,QAAQ,IAAI,CAAC,CAAC,SAAS,CAAC;IACtC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC;CAC3B,CAAC,CAAC"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * tool-class.ts — The `Tool` orchestrator for code-first-agents deterministic tools.
3
+ *
4
+ * Implements the tool contract defined in the code-first agents pattern:
5
+ * - Subcommand dispatch with typed input/output Zod schemas
6
+ * - Validated JSON output (the shape IS the validator)
7
+ * - Self-describing `schema` subcommand (auto-registered)
8
+ * - `help` subcommand + unknown-subcommand fallback that exposes input schemas
9
+ * so LLM callers can self-correct
10
+ * - Always exits with code 0; errors communicated via `ok: false` envelope
11
+ *
12
+ * Spec: https://beogip.github.io/code-first-agents/patterns/deterministic-tools.html
13
+ *
14
+ * ## Usage
15
+ *
16
+ * ```ts
17
+ * import { z } from "zod";
18
+ * import { Tool, l2Output } from "@code-first-agents/tool";
19
+ *
20
+ * new Tool({ name: "level-classifier", description: "..." })
21
+ * .subcommand({
22
+ * name: "classify",
23
+ * description: "Classify a SKILL.md by code-first level",
24
+ * input: z.object({ path: z.string() }).strict(),
25
+ * output: l2Output(z.enum(["L1", "L2", "L3"])),
26
+ * // Handler returns message + data. `ok: true` is framework-added.
27
+ * handler: ({ path }) => ({
28
+ * message: `classified ${path}`,
29
+ * classification: "L2",
30
+ * }),
31
+ * })
32
+ * .run(process.argv.slice(2));
33
+ * ```
34
+ *
35
+ * ## Output envelope contract
36
+ *
37
+ * Handlers return the output shape **without `ok`** — the base class stamps
38
+ * `ok: true` onto the handler's result before validating against the output
39
+ * schema. This keeps handlers focused on `message` + business data and avoids
40
+ * TypeScript widening `ok: true` to `boolean` in object-literal returns.
41
+ *
42
+ * Use the level helpers (`l1Output`, `l2Output`, `l3Output` from
43
+ * `./output-helpers.ts`) to get the envelope + level-specific required
44
+ * fields baked in; fall back to raw `z.object({...})` when the tool's shape
45
+ * doesn't fit a level (remember to include `ok: z.literal(true)` and
46
+ * `message: z.string()` in the raw schema so validation covers the whole
47
+ * envelope).
48
+ *
49
+ * Error envelopes (`schema_violation`, `input_validation_error`,
50
+ * `unknown_subcommand`, `unexpected_error`) are class-authored — see
51
+ * `./envelopes.ts`.
52
+ *
53
+ * @module code-first-agents-tool/tool-class
54
+ */
55
+ import { z } from "zod";
56
+ import type { SubcommandSpec, ToolMeta } from "./types.ts";
57
+ import { type ToolOutput } from "./utils.ts";
58
+ /**
59
+ * Orchestrator for a code-first deterministic tool. Register subcommands via
60
+ * {@link Tool.subcommand}, then call {@link Tool.run} with `process.argv.slice(2)`.
61
+ *
62
+ * The class always terminates the process with exit code 0 via `jsonOutput`;
63
+ * errors are communicated through the JSON envelope's `ok: false` + `error`
64
+ * fields.
65
+ *
66
+ * Reserved subcommand names `schema` and `help` are auto-registered by the
67
+ * class — attempting to register them manually throws `RangeError`.
68
+ */
69
+ export declare class Tool {
70
+ private readonly meta;
71
+ private readonly subs;
72
+ /**
73
+ * @param meta - Tool metadata (name + description).
74
+ */
75
+ constructor(meta: ToolMeta);
76
+ /**
77
+ * Register a subcommand. Chainable (returns `this`).
78
+ *
79
+ * Throws synchronously on:
80
+ * - reserved name collision (`schema`, `help`) → `RangeError`
81
+ * - duplicate name → `RangeError`
82
+ * - missing `input` schema → `TypeError`
83
+ * - missing `output` schema → `TypeError`
84
+ *
85
+ * @param spec - {@link SubcommandSpec} carrying name, description, input/output Zod schemas, and handler.
86
+ * @returns This tool instance, for chaining.
87
+ */
88
+ subcommand<I extends z.ZodTypeAny, O extends z.ZodTypeAny>(spec: SubcommandSpec<I, O>): this;
89
+ /**
90
+ * Dispatch a subcommand from parsed `argv`. Always terminates the process
91
+ * via `jsonOutput` with exit code 0.
92
+ *
93
+ * Pure dispatch — each branch delegates to a single-responsibility helper:
94
+ * 1. {@link Tool.dispatchBuiltin} handles `schema` / `help`.
95
+ * 2. {@link Tool.rejectUnknown} handles empty or unregistered subcommands.
96
+ * 3. {@link Tool.runSubcommand} runs the validate → handler → validate pipeline.
97
+ * 4. Outer try/catch: {@link ToolError} → handler-emitted envelope with the
98
+ * custom code; anything else → `unexpected_error`.
99
+ *
100
+ * @param argv - CLI tokens, typically `process.argv.slice(2)`.
101
+ * @returns Never — the process exits via `jsonOutput`.
102
+ */
103
+ run(argv: string[]): Promise<never>;
104
+ /**
105
+ * Pre-flight checks for a subcommand registration. Called by
106
+ * {@link Tool.subcommand}; throws synchronously on any violation.
107
+ *
108
+ * @param spec - The {@link SubcommandSpec} to validate.
109
+ * @throws `RangeError` for reserved-name collision or duplicate registration.
110
+ * @throws `TypeError` when `input` is missing.
111
+ */
112
+ private validateRegistration;
113
+ /**
114
+ * Handle the auto-registered `schema` and `help` subcommands. Returns the
115
+ * success envelope for those names, or `null` to signal the caller should
116
+ * fall through to user-registered dispatch.
117
+ *
118
+ * @param name - The parsed subcommand name.
119
+ * @returns A success envelope for `schema`/`help`, or `null` otherwise.
120
+ */
121
+ private dispatchBuiltin;
122
+ /**
123
+ * Handle empty argv and unregistered-subcommand cases. Returns the
124
+ * `unknown_subcommand` envelope (carrying the help listing for LLM
125
+ * self-correction) when applicable, or `null` when the subcommand is
126
+ * registered and should proceed.
127
+ *
128
+ * @param name - The parsed subcommand name.
129
+ * @returns An `unknown_subcommand` envelope, or `null` if the name is registered.
130
+ */
131
+ private rejectUnknown;
132
+ /**
133
+ * Execute a registered subcommand: validate input → `await` handler →
134
+ * stamp `ok: true` → validate output. Returns whichever envelope the
135
+ * pipeline produced (success or one of the validation errors).
136
+ *
137
+ * Preconditions: `name` must be a registered subcommand name. Callers
138
+ * should run {@link Tool.rejectUnknown} first.
139
+ *
140
+ * @param name - The subcommand to execute.
141
+ * @param parsed - Raw parsed args from {@link parseArgs}.
142
+ * @returns A success envelope or a typed error envelope.
143
+ */
144
+ private runSubcommand;
145
+ /**
146
+ * Programmatic entry point — runs a subcommand in-process without CLI
147
+ * arg parsing or `process.exit`. Identical validation pipeline to
148
+ * {@link Tool.run}, but accepts a plain object instead of argv tokens.
149
+ *
150
+ * @param subcommand - The subcommand name to dispatch.
151
+ * @param args - Input data as a plain object (bypasses CLI parsing).
152
+ * @returns The output envelope (success or error).
153
+ *
154
+ * Unlike `run()`, this method does NOT promote global flags (e.g. `--path`)
155
+ * into subcommand args. Callers must pass the full args object directly.
156
+ * Also, stderr output from handlers is not capturable — spawn the tool
157
+ * as a subprocess in tests that need to assert on stderr content.
158
+ */
159
+ invoke(subcommand: string, args?: Record<string, unknown>): Promise<ToolOutput>;
160
+ private validateOutput;
161
+ }
162
+ //# sourceMappingURL=tool-class.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-class.d.ts","sourceRoot":"","sources":["../src/tool-class.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,OAAO,KAAK,EAAc,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AASzD;;;;;;;;;;GAUG;AACH,qBAAa,IAAI;IACf,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAW;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmC;IAExD;;OAEG;gBACS,IAAI,EAAE,QAAQ;IAI1B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI;IAM5F;;;;;;;;;;;;;OAaG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC;IAczC;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IAuB5B;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAsBvB;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAOrB;;;;;;;;;;;OAWG;YACW,aAAa;IAkB3B;;;;;;;;;;;;;OAaG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA2BzF,OAAO,CAAC,cAAc;CA4BvB"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * types.ts — Public interfaces for the code-first-agents Tool base class.
3
+ *
4
+ * Kept pure: no imports beyond `zod`, no runtime logic. Consumers of the
5
+ * `Tool` class type their code against these.
6
+ *
7
+ * @module code-first-agents-tool/types
8
+ */
9
+ import type { z } from "zod";
10
+ /** Metadata describing the tool itself. Used in help output. */
11
+ export interface ToolMeta {
12
+ /** Tool identifier (usually matches the filename stem). */
13
+ name: string;
14
+ /** One-line human description shown in help output. */
15
+ description: string;
16
+ }
17
+ /**
18
+ * Raw CLI args after parsing. Flags are `--key value` pairs; bare `--flag`
19
+ * (with no following value) resolves to `true`. Positional args are any
20
+ * tokens that don't start with `--`.
21
+ */
22
+ export interface ParsedArgs {
23
+ /** Flag key → value mapping. Last-one-wins on repeated keys. */
24
+ flags: Record<string, string | true>;
25
+ /** Positional (non-flag) tokens in order. */
26
+ positional: string[];
27
+ }
28
+ /**
29
+ * What a handler is expected to return: the output shape **without** `ok`.
30
+ * The base class stamps `ok: true` onto the result before output validation,
31
+ * so handlers stay focused on `message` + business data. This also avoids
32
+ * TypeScript widening `ok: true` to `boolean` in object-literal returns.
33
+ */
34
+ export type HandlerReturn<O extends z.ZodTypeAny> = Omit<z.infer<O>, "ok">;
35
+ /**
36
+ * A registered subcommand spec. Both `input` and `output` Zod schemas are
37
+ * required — they validate CLI args before the handler runs and the
38
+ * handler's return value before emit.
39
+ *
40
+ * @typeParam I - Input Zod schema type
41
+ * @typeParam O - Output Zod schema type
42
+ */
43
+ export interface SubcommandSpec<I extends z.ZodTypeAny, O extends z.ZodTypeAny> {
44
+ /** Subcommand name as it appears on the CLI (e.g. "classify"). */
45
+ name: string;
46
+ /** One-line description shown in help output. */
47
+ description: string;
48
+ /**
49
+ * Zod schema for the validated flags + positional args. Positional args
50
+ * are exposed under a reserved `_` key when present. Use `.strict()` to
51
+ * reject unknown flags loudly.
52
+ *
53
+ * **Sync only:** the base class uses `safeParse` (not `safeParseAsync`).
54
+ * Schemas with `.refine(async ...)` or `.transform(async ...)` will cause
55
+ * a hard runtime throw at dispatch time (caught by the outer `unexpected_error`
56
+ * envelope, but with a cryptic message). Use synchronous validators only.
57
+ */
58
+ input: I;
59
+ /**
60
+ * Zod schema for the full output envelope (including `ok: z.literal(true)`
61
+ * and `message: z.string()`). Use the level helpers `l1Output` / `l2Output`
62
+ * / `l3Output` to get the envelope baked in, or pass a raw `z.object`.
63
+ * The handler returns everything **except** `ok`; the base class adds it.
64
+ *
65
+ * **Sync only:** same constraint as `input` — no async transforms or refinements.
66
+ */
67
+ output: O;
68
+ /**
69
+ * Business logic. Receives the validated, typed input. Returns the output
70
+ * shape **without** `ok` — the base class adds `ok: true` before validating
71
+ * against the output schema. May return a plain value or a Promise.
72
+ */
73
+ handler: (args: z.infer<I>) => HandlerReturn<O> | Promise<HandlerReturn<O>>;
74
+ }
75
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACrC,6CAA6C;IAC7C,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAE3E;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,CAAC,SAAS,CAAC,CAAC,UAAU;IAC5E,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;;;OASG;IACH,KAAK,EAAE,CAAC,CAAC;IACT;;;;;;;OAOG;IACH,MAAM,EAAE,CAAC,CAAC;IACV;;;;OAIG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7E"}
@@ -0,0 +1,20 @@
1
+ /** Standard JSON envelope for all tool stdout output. */
2
+ export type ToolOutput = {
3
+ ok: boolean;
4
+ message: string;
5
+ [key: string]: unknown;
6
+ };
7
+ /**
8
+ * Print a {@link ToolOutput} as JSON to stdout and terminate with exit code 0.
9
+ * @param data - The tool output envelope to serialize.
10
+ * @returns Never — the process exits.
11
+ */
12
+ export declare function jsonOutput(data: ToolOutput): never;
13
+ /**
14
+ * Extract a human-readable message from an unknown thrown value. Preserves
15
+ * the `Error.message` when available; otherwise coerces via `String(...)`.
16
+ * @param err - The caught value (Error instance, string, null, etc.).
17
+ * @returns A non-undefined string suitable for envelope `message` / log output.
18
+ */
19
+ export declare function stringifyError(err: unknown): string;
20
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAElF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAIlD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEnD"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@code-first-agents/tool",
3
+ "version": "0.1.0",
4
+ "description": "Code-first agent tool definitions with Zod schemas",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/beogip/code-first-agents-tool.git"
24
+ },
25
+ "scripts": {
26
+ "dev": "bun run --watch src/index.ts",
27
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --format esm --target node --packages external && tsc -p tsconfig.build.json",
28
+ "test": "bun test",
29
+ "lint": "biome lint .",
30
+ "format": "biome format --write .",
31
+ "check": "biome check --write .",
32
+ "prepare": "test -d .git && lefthook install || true",
33
+ "prepublishOnly": "bun run build"
34
+ },
35
+ "peerDependencies": {
36
+ "zod": "^4.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "zod": "^4.0.0",
40
+ "@biomejs/biome": "latest",
41
+ "bun-types": "latest",
42
+ "lefthook": "latest",
43
+ "semantic-release": "latest",
44
+ "@semantic-release/changelog": "latest",
45
+ "@semantic-release/git": "latest",
46
+ "@semantic-release/github": "latest",
47
+ "@semantic-release/npm": "latest",
48
+ "typescript": "latest"
49
+ }
50
+ }