@akai-workflow-builder/cli-sdk 0.0.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +370 -0
  3. package/bin/akai.js +15 -0
  4. package/dist/cli.d.ts +46 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +148 -0
  7. package/dist/ctx/build-ctx.d.ts +46 -0
  8. package/dist/ctx/build-ctx.d.ts.map +1 -0
  9. package/dist/ctx/build-ctx.js +83 -0
  10. package/dist/ctx/read-file.d.ts +22 -0
  11. package/dist/ctx/read-file.d.ts.map +1 -0
  12. package/dist/ctx/read-file.js +40 -0
  13. package/dist/ctx/safe-fetch.d.ts +10 -0
  14. package/dist/ctx/safe-fetch.d.ts.map +1 -0
  15. package/dist/ctx/safe-fetch.js +183 -0
  16. package/dist/ctx/safe-path.d.ts +12 -0
  17. package/dist/ctx/safe-path.d.ts.map +1 -0
  18. package/dist/ctx/safe-path.js +64 -0
  19. package/dist/ctx/secrets.d.ts +42 -0
  20. package/dist/ctx/secrets.d.ts.map +1 -0
  21. package/dist/ctx/secrets.js +58 -0
  22. package/dist/define-cli.d.ts +48 -0
  23. package/dist/define-cli.d.ts.map +1 -0
  24. package/dist/define-cli.js +257 -0
  25. package/dist/errors.d.ts +28 -0
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/errors.js +31 -0
  28. package/dist/file-input.d.ts +31 -0
  29. package/dist/file-input.d.ts.map +1 -0
  30. package/dist/file-input.js +32 -0
  31. package/dist/index.d.ts +14 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/manifest/constants.d.ts +16 -0
  35. package/dist/manifest/constants.d.ts.map +1 -0
  36. package/dist/manifest/constants.js +14 -0
  37. package/dist/manifest/index.d.ts +6 -0
  38. package/dist/manifest/index.d.ts.map +1 -0
  39. package/dist/manifest/index.js +3 -0
  40. package/dist/manifest/json-schema.d.ts +75 -0
  41. package/dist/manifest/json-schema.d.ts.map +1 -0
  42. package/dist/manifest/json-schema.js +129 -0
  43. package/dist/manifest/to-schema.d.ts +15 -0
  44. package/dist/manifest/to-schema.d.ts.map +1 -0
  45. package/dist/manifest/to-schema.js +87 -0
  46. package/dist/manifest/types.d.ts +102 -0
  47. package/dist/manifest/types.d.ts.map +1 -0
  48. package/dist/manifest/types.js +1 -0
  49. package/dist/tool.d.ts +65 -0
  50. package/dist/tool.d.ts.map +1 -0
  51. package/dist/tool.js +214 -0
  52. package/dist/types.d.ts +215 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +1 -0
  55. package/package.json +62 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The @akai-workflow-builder/cli-sdk Authors
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,370 @@
1
+ # @akai-workflow-builder/cli-sdk
2
+
3
+ > Author atomic, agent-executable CLIs in TypeScript.
4
+
5
+ A small, opinionated SDK for defining CLIs that an agent runtime invokes. Each tool represents one atomic operation — typically a single HTTP call, but a tool can wrap any single observable operation. The SDK gives you a sandboxed `fetch`, scoped secrets, typed input + output schemas, and a per-tool egress allowlist. The host runtime handles input validation, secret scoping, network sandboxing, and the human-in-the-loop approval gate for writes.
6
+
7
+ > **Status:** pre-v0.1. The authoring surface (`tool`, `defineCLI`, `buildCtx`, `ctx.fetch`, `ctx.secrets`, `ctx.properties`) is stable.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @akai/cli zod
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ | | |
20
+ |---|---|
21
+ | Node | `≥ 24` |
22
+ | TypeScript | `≥ 5.0` (uses `const` type parameters for literal-key inference) |
23
+ | `zod` | `^4.4.1` (peer dependency) |
24
+ | `tsconfig.json` | needs `Response` / `RequestInit` / `AbortSignal`. Either add `"DOM"` to your existing `lib` entries (e.g. `"lib": ["ES2023", "DOM"]`) or include `@types/node@^18+` |
25
+
26
+ ---
27
+
28
+ ## Quickstart
29
+
30
+ ```ts
31
+ import { tool, defineCLI } from '@akai/cli';
32
+ import { z } from 'zod';
33
+
34
+ // Define the response shape once; reuse for `jsonOutput` and runtime parsing.
35
+ const Response = z.object({
36
+ user: z.object({ id: z.number(), email: z.string() }),
37
+ });
38
+
39
+ const usersMe = tool({
40
+ name: 'Authenticated user',
41
+ description: 'Return the authenticated user.',
42
+ jsonOutput: Response, // input defaults to z.object({}) when omitted
43
+ options: {
44
+ network: { egress: 'zendesk.com' },
45
+ secretKeys: ['ZENDESK_SUBDOMAIN', 'ZENDESK_API_TOKEN'],
46
+ isReadonly: true,
47
+ },
48
+ handler: async ({ ctx, isJson }) => {
49
+ const sub = ctx.secrets.ZENDESK_SUBDOMAIN!;
50
+ const tok = ctx.secrets.ZENDESK_API_TOKEN!;
51
+ // Quickstart trusts `sub` is a workspace shortname ("acme"); the worked
52
+ // example validates it before constructing the URL.
53
+ const res = await ctx.fetch(`https://${sub}.zendesk.com/api/v2/users/me.json`, {
54
+ signal: ctx.signal,
55
+ headers: { Authorization: `Bearer ${tok}` },
56
+ });
57
+ if (!res.ok) throw new Error(`fetch authenticated user failed: ${res.status}`);
58
+ // Parse, don't cast — strips unknown fields, throws if the shape changes.
59
+ const body = Response.parse(await res.json());
60
+ return isJson ? body : `User #${body.user.id} <${body.user.email}>`;
61
+ },
62
+ });
63
+
64
+ export default defineCLI({
65
+ id: 'zendesk',
66
+ name: 'Zendesk Support',
67
+ summary: 'Read tickets, users, and search results from Zendesk Support.',
68
+ description:
69
+ 'Connect to a Zendesk Support workspace via OAuth bearer token. Read-only surface: fetch the authenticated user, fetch tickets and users by id, list tickets, and run search queries.',
70
+ secrets: [
71
+ { key: 'ZENDESK_SUBDOMAIN', name: 'Workspace subdomain', description: '...', required: true },
72
+ { key: 'ZENDESK_API_TOKEN', name: 'API access token', description: '...', required: true },
73
+ ],
74
+ tools: {
75
+ 'users.me': usersMe,
76
+ },
77
+ });
78
+ ```
79
+
80
+ A full worked example with five read-only tools and a smoke runner lives in the repository under `examples/zendesk/` (not bundled in the published package; see the source tree).
81
+
82
+ ---
83
+
84
+ ## Core concepts
85
+
86
+ ### Atomic tools
87
+
88
+ Every `tool()` represents **one atomic operation** — typically a single HTTP call, but it can wrap any single observable operation (a local compute, a file read, a DB query). Composition is the planner's responsibility — agents plan over the atomic tools the runtime exposes; the SDK is intentionally not the place to chain steps.
89
+
90
+ Three consequences:
91
+
92
+ - **Per-tool permissions are meaningful** — one tool = one observable side effect = one approval surface.
93
+ - **Schemas stay tight** — each tool documents one operation, not a workflow.
94
+ - **No hidden side effects** — what the schema describes is what executes.
95
+
96
+ The SDK doesn't statically enforce step count — atomicity is a design guideline. If a logical step needs three calls, ship three tools and let the planner compose them.
97
+
98
+ ### `defineCLI()`
99
+
100
+ A CLI is a named bundle of tools sharing one secret-declaration set:
101
+
102
+ ```ts
103
+ defineCLI({
104
+ id: 'zendesk',
105
+ name: 'Zendesk Support',
106
+ summary: 'Read tickets, users, and search results from Zendesk Support.',
107
+ description: 'Connect to a Zendesk Support workspace via OAuth bearer token …',
108
+ secrets: [/* ... */],
109
+ tools: {
110
+ 'users.me': usersMe,
111
+ 'tickets.get': getTicket,
112
+ 'tickets.list': listTickets,
113
+ },
114
+ });
115
+ ```
116
+
117
+ **Required**
118
+
119
+ | Field | Role |
120
+ |---|---|
121
+ | `id` | Machine slug. Used for wire ids (`<id>.<toolKey>`) and the `x-akai-cli` header. Pattern: `^[a-z][a-z0-9_-]*$`. |
122
+ | `name` | Human display label. |
123
+ | `summary` | One-sentence short label. |
124
+ | `tools` | Map of `tool()` results. Keys follow the kebab + dotted convention below. |
125
+
126
+ **Optional**
127
+
128
+ | Field | Role |
129
+ |---|---|
130
+ | `vendor` | Brand/company. Defaults to `name`. Useful when display ("JPMorgan Access Portal") differs from vendor ("JPMorgan"). |
131
+ | `description` | Long-form description. Consumers decide how to render when omitted. |
132
+ | `instructions` | Ordered setup steps shown to operators. |
133
+ | `secrets` | Array of `SecretDecl` (the keys tools reference). |
134
+ | `properties` | Array of `PropertyDecl` for non-sensitive tenant config (base URLs, region codes). Distinct from `secrets[]`. |
135
+
136
+ `defineCLI()` cross-validates every `secretKeys` and `propertyKeys` reference against the CLI-level declarations. Mismatches throw `AkaiSpecError` at module load — no silent failures.
137
+
138
+ ### Tool keys
139
+
140
+ Tool keys are **kebab-case**, optionally dot-namespaced for grouping:
141
+
142
+ ```
143
+ chat-post-message
144
+ users.me
145
+ tickets.search-by-query
146
+ activations.contract-benefit.mark-enrolled-ahead-of-ingest
147
+ ```
148
+
149
+ Each dot-separated segment matches `[a-zA-Z][a-zA-Z0-9_]*(-[a-zA-Z0-9_]+)*` — first char a letter, hyphens must be followed by at least one word char (so `--`, leading/trailing `-`, and empty segments reject). Wire ids are `<cliId>.<toolKey>`.
150
+
151
+ Underscores and camelCase are still accepted so existing tools keep working, but new tools should be kebab to match the convention in the [`akai-cli-tools`](https://github.com/akai-workflow-builder/akai-cli-tools) repo.
152
+
153
+ ### The `ctx` contract
154
+
155
+ Tool handlers receive a frozen `ctx` with exactly six members:
156
+
157
+ ```ts
158
+ interface ToolCtx<S extends string, P extends string> {
159
+ fetch: (url: string, init?: RequestInit) => Promise<Response>;
160
+ secrets: Readonly<Record<S, string | undefined>>;
161
+ properties: Readonly<Record<P, string | undefined>>;
162
+ logger: ToolLogger;
163
+ signal: AbortSignal;
164
+ workdir: string;
165
+ }
166
+ ```
167
+
168
+ **What you get**
169
+
170
+ - `fetch` — sandboxed `fetch` bound to the tool's egress allowlist.
171
+ - `secrets` — only the secret keys the tool declared in `secretKeys`. Other workspace secrets are inaccessible.
172
+ - `properties` — only the property keys the tool declared in `propertyKeys`. Non-sensitive tenant config (base URL, region) lives here, not in `secrets`.
173
+ - `logger` — structured logger; writes to stderr. stdout is reserved for the tool's result.
174
+ - `signal` — request `AbortSignal`. Forward it to `ctx.fetch` for cancellation and timeouts.
175
+ - `workdir` — writable scratch directory scoped to this invocation.
176
+
177
+ **What `ctx` deliberately omits**
178
+
179
+ - No `process.env` accessor — secrets flow through `ctx.secrets` so the runtime can scope and audit them.
180
+ - No `fs`, `child_process`, or `worker_threads` handles — `ctx` doesn't pre-wire them. The SDK does not sandbox the Node runtime; if a handler imports those modules directly, the host is responsible for restricting that out of band.
181
+ - No handle to invoke sibling tools — composition is the planner's job.
182
+
183
+ ### Secrets — declarations vs references
184
+
185
+ Secrets are declared once at the CLI level and referenced by key on each tool that needs them:
186
+
187
+ ```ts
188
+ defineCLI({
189
+ secrets: [
190
+ { key: 'ZENDESK_API_TOKEN', name: 'API token', description: '...', required: true },
191
+ ],
192
+ tools: {
193
+ getTicket: tool({
194
+ options: {
195
+ secretKeys: ['ZENDESK_API_TOKEN'],
196
+ /* ... */
197
+ },
198
+ /* ... */
199
+ }),
200
+ },
201
+ });
202
+ ```
203
+
204
+ | | Field | Type | Role |
205
+ |---|---|---|---|
206
+ | CLI | `secrets` | `SecretDecl[]` | Operator-facing declarations |
207
+ | Tool | `options.secretKeys` | `string[]` | Per-tool subset |
208
+ | Runtime | `ctx.secrets` | `Readonly<Record<S, string \| undefined>>` | What the handler reads |
209
+
210
+ When building `ctx`, the runtime walks the tool's declared `secretKeys`. Each key must be declared in the CLI's `secrets[]`; an undeclared reference throws `AkaiSpecError` at module load. At invocation, a `required: true` secret with no value throws `AkaiSecretError`. Optional secrets that the caller didn't supply appear on `ctx.secrets` as `undefined` so handlers can branch.
211
+
212
+ `ctx.secrets[K]` is typed `string | undefined` for all `K` — optional secrets may be absent, so handlers narrow before use. Even when `required: true`, the SDK can't statically prove the runtime body shipped the value, so the type stays union.
213
+
214
+ ### Properties — non-sensitive tenant config
215
+
216
+ Tenant-specific values that aren't credentials live in `properties[]`, not `secrets[]`. Examples: a workspace subdomain, a self-hosted base URL, a region code. The host runtime stores these without the encryption-at-rest treatment applied to secrets and may surface them in admin UIs unmasked.
217
+
218
+ ```ts
219
+ defineCLI({
220
+ secrets: [{ key: 'JIRA_API_TOKEN', name: 'API token', description: '...', required: true }],
221
+ properties: [{ key: 'JIRA_BASE_URL', name: 'Workspace URL', description: '...', required: true }],
222
+ tools: {
223
+ getIssue: tool({
224
+ options: {
225
+ secretKeys: ['JIRA_API_TOKEN'],
226
+ propertyKeys: ['JIRA_BASE_URL'],
227
+ network: { egress: { from: 'property:JIRA_BASE_URL' } },
228
+ isReadonly: true,
229
+ },
230
+ handler: async ({ ctx }) => {
231
+ const base = ctx.properties.JIRA_BASE_URL!;
232
+ const tok = ctx.secrets.JIRA_API_TOKEN!;
233
+ return ctx.fetch(`https://${base}/rest/api/3/issue/...`, {
234
+ headers: { Authorization: `Bearer ${tok}` },
235
+ });
236
+ },
237
+ /* ... */
238
+ }),
239
+ },
240
+ });
241
+ ```
242
+
243
+ | | Field | Type | Role |
244
+ |---|---|---|---|
245
+ | CLI | `properties` | `PropertyDecl[]` | Operator-facing declarations |
246
+ | Tool | `options.propertyKeys` | `string[]` | Per-tool subset |
247
+ | Runtime | `ctx.properties` | `Readonly<Record<P, string \| undefined>>` | What the handler reads |
248
+
249
+ `AkaiPropertyError` is thrown when a `required: true` property is missing at invocation time. Same lifecycle as `AkaiSecretError`, distinct class because the failure source is conceptually different.
250
+
251
+ ### Dynamic egress (multi-tenant connectors)
252
+
253
+ For connectors where the host varies per tenant (Jira Cloud, Zendesk, Looker self-hosted, GHES, Salesforce), declare the host as a property and reference it from `egress`:
254
+
255
+ ```ts
256
+ options: {
257
+ propertyKeys: ['JIRA_BASE_URL'],
258
+ network: { egress: { from: 'property:JIRA_BASE_URL' } },
259
+ }
260
+ ```
261
+
262
+ `defineCLI()` cross-checks that the referenced key is declared in `properties[]` AND included in the tool's `propertyKeys`. The host runtime resolves the reference per invocation; SDK-side `buildCtx` throws `AkaiSpecError` to enforce that the host has resolved the egress before constructing `ctx`.
263
+
264
+ ### Egress allowlist
265
+
266
+ Every tool declares its allowed outbound hosts:
267
+
268
+ ```ts
269
+ options: {
270
+ network: { egress: 'api.example.com' }, // single host (shorthand)
271
+ // or: { egress: ['api.example.com', 'cdn.example.com'] },
272
+ }
273
+ ```
274
+
275
+ `ctx.fetch` enforces, in order:
276
+
277
+ - **Protocol** — HTTPS-only in production. HTTP allowed only when `NODE_ENV === 'development'`.
278
+ - **Host** — exact-or-suffix-with-dot. `api.example.com` matches itself and `*.api.example.com`; `evil.api.example.com.attacker.tld` does not.
279
+ - **DNS preflight** — hostname resolved before the request. In production, addresses in private (RFC 1918), loopback, link-local, or unique-local IPv6 ranges are refused (IPv4, IPv6, v4-mapped, and canonicalized forms). In development the private-IP refusal is skipped so handlers can target local services.
280
+ - **Redirects** — `redirect: 'error'` is pinned on every request, non-overridable. If the target responds with a 3xx, `fetch` itself rejects; the SDK never follows redirects, so a `Location:` to a public host cannot smuggle the request past the egress check.
281
+ - **Identity headers** — `user-agent`, `x-akai-cli`, `x-akai-tool`, `x-akai-request-id` are stamped on every request. `x-akai-tenant` is stamped only when the host runtime passes a tenant id to `buildCtx`; otherwise it is removed even if a handler tried to set it. Handler-supplied values for any of these are always overwritten.
282
+
283
+ Protocol, host, and DNS-preflight failures reject with `AkaiEgressError` before any network request is made. Redirect failures surface as a `fetch` rejection after the initial response — same outcome (no follow-through), different error type.
284
+
285
+ Spec validation also rejects egress hosts that don't pass a syntactic DNS-hostname check — `localhost` (single label), IP literals, hosts with spaces or slashes — so they can't be declared in the first place. The check is shape-only; it doesn't verify the name is publicly resolvable, so internal DNS zones still pass the spec and are then handled at DNS-preflight time.
286
+
287
+ ### `isReadonly`
288
+
289
+ Every tool must declare whether it mutates remote state:
290
+
291
+ ```ts
292
+ options: {
293
+ isReadonly: true, // reads, queries — no approval gate
294
+ // isReadonly: false, // writes — runtime pauses for human approval
295
+ }
296
+ ```
297
+
298
+ Two roles:
299
+
300
+ 1. **Planner hint** — exposed in the tool listing so plan UIs render write steps distinctly.
301
+ 2. **Runtime gate** — write tools cannot execute unless a confirmation flag is set, which the runtime flips only after the approval flow returns approved.
302
+
303
+ No default. The SDK throws on omission — silent defaults would risk marking writes as readonly. **When in doubt, choose `false`** — false-positives cost one approval click; false-negatives cause silent mutation.
304
+
305
+ ### Output mode: JSON vs text
306
+
307
+ Each handler receives an `isJson: boolean` flag. The caller chooses `format=json` or `format=text`; the handler returns either shape:
308
+
309
+ ```ts
310
+ handler: async ({ input, ctx, isJson }) => {
311
+ const body = await fetchTheThing(/* ... */);
312
+ return isJson ? body : `Thing #${body.id} (${body.status})`;
313
+ }
314
+ ```
315
+
316
+ When `jsonOutput` is declared, the handler return type is `string | ZInfer<jsonOutput>`. When `jsonOutput` is omitted, the handler always returns a string.
317
+
318
+ ---
319
+
320
+ ## Worked example: Zendesk
321
+
322
+ The repository's `examples/zendesk/` folder ships a five-tool, read-only Zendesk Support connector (not bundled in the published package):
323
+
324
+ | Tool | Endpoint | Demonstrates |
325
+ |---|---|---|
326
+ | `users.me` | `GET /users/me` | Auth probe; simplest tool |
327
+ | `users.get` | `GET /users/{id}` | Path parameter |
328
+ | `tickets.get` | `GET /tickets/{id}` | Path parameter, 404 mapped to typed error |
329
+ | `tickets.list` | `GET /tickets` | Cursor pagination |
330
+ | `tickets.search` | `GET /search` | Query composition, 422 surfaced |
331
+
332
+ Each tool branches on `isJson` and uses the structured logger. The folder includes a smoke runner that mirrors the runtime's per-invocation dispatch (registry → `buildCtx()` → handler) so you can hit a real workspace from your terminal.
333
+
334
+ ---
335
+
336
+ ## Errors
337
+
338
+ | Class | When |
339
+ |---|---|
340
+ | `AkaiSpecError` | Invalid `tool()` / `defineCLI()` spec — thrown at module load |
341
+ | `AkaiInputError` | Tool input fails the declared zod schema — thrown by the runtime when validating the request body |
342
+ | `AkaiSecretError` | A `required: true` secret is missing at invocation time — thrown by `buildCtx` |
343
+ | `AkaiPropertyError`| A `required: true` property is missing at invocation time — thrown by `buildCtx` |
344
+ | `AkaiEgressError` | `ctx.fetch` rejected the target |
345
+
346
+ Every error extends `AkaiError`; the runtime maps each to a structured error envelope.
347
+
348
+ ---
349
+
350
+ ## Troubleshooting
351
+
352
+ **`Cannot find name 'Response'`** — `tsconfig.json` is missing web-platform types. Add `"DOM"` to your existing `lib` entries (e.g. `"lib": ["ES2023", "DOM"]`) — setting `lib` overrides the target's defaults, so include both — or use `@types/node@^18+`.
353
+
354
+ **`AkaiSpecError: undeclared secret 'X'`** — a `secretKeys` entry references a key that isn't in `defineCLI({ secrets: [...] })`.
355
+
356
+ **`AkaiSpecError: undeclared property 'X'`** — a `propertyKeys` entry references a key that isn't in `defineCLI({ properties: [...] })`.
357
+
358
+ **`AkaiSpecError: invalid id "..."`** — CLI `id` must be a lowercase slug matching `^[a-z][a-z0-9_-]*$`. The display label lives in `name`.
359
+
360
+ **`AkaiSpecError: invalid egress host` for `localhost` / `127.0.0.1`** — `tool()` rejects IP literals and single-label hostnames (including `localhost`) at spec time, before `ctx.fetch` ever runs. For local development, use a tunnel (ngrok, Cloudflare Tunnel) that resolves to a public hostname.
361
+
362
+ **`AkaiEgressError` against an unallowed host** — runtime egress check failed. Add the host to `options.network.egress` (matched exact-or-suffix-with-dot).
363
+
364
+ **Type error: handler return doesn't match `jsonOutput`** — when `jsonOutput` is declared, the handler return type is `string | <structured shape>`. Branch on `isJson`.
365
+
366
+ ---
367
+
368
+ ## License
369
+
370
+ TBD — license will be selected before the first public release.
package/bin/akai.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ // Tiny shim: keep the shebang here (TS sources can't carry it) and forward
3
+ // to the compiled run(). Kept as JS so no build step has to prepend the
4
+ // shebang to dist/.
5
+ import { run } from '../dist/cli.js';
6
+
7
+ run(process.argv.slice(2)).then(
8
+ (code) => process.exit(code),
9
+ (err) => {
10
+ // Defensive — run() catches its own errors and returns an exit code,
11
+ // so this should never fire. Surface anything that slips through.
12
+ process.stderr.write(`akai: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
13
+ process.exit(1);
14
+ },
15
+ );
package/dist/cli.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { CLIDef } from './types.js';
2
+ type ParsedArgs = {
3
+ ok: true;
4
+ packagePath: string;
5
+ out: string | undefined;
6
+ } | {
7
+ ok: false;
8
+ error: string;
9
+ };
10
+ /**
11
+ * Parse argv (minus `node` + script path). Single-command CLI today —
12
+ * `build-manifest` is the only verb. Exits non-zero with a clear stderr
13
+ * message on any usage error.
14
+ */
15
+ export declare function parseArgs(argv: readonly string[]): ParsedArgs;
16
+ /**
17
+ * Resolve a package directory to its compiled entry, dynamic-import it,
18
+ * and verify the default export is a {@link CLIDef}. Reads `package.json`
19
+ * for `main`; falls back to `index.js`. Brand-checks `__akaiCLI: true` to
20
+ * reject hand-crafted objects that happen to share the shape.
21
+ */
22
+ export declare function loadCli(packagePath: string): Promise<CLIDef>;
23
+ /**
24
+ * Load the CLI, project it to a manifest, and write the JSON either to
25
+ * stdout (default) or the path given by `--out`.
26
+ *
27
+ * Uses `cli.toSchema()` (the instance method attached at `defineCLI`
28
+ * time) rather than this SDK's imported `toSchema(cli)`, so the target
29
+ * package's bundled SDK version wins. Per-tenant CLIs may ship with a
30
+ * different `@akai/cli` than the one running the binary — calling the
31
+ * instance method emits the manifest shape that package was authored
32
+ * against.
33
+ */
34
+ export declare function buildManifest(packagePath: string, out: string | undefined): Promise<void>;
35
+ /**
36
+ * Entry point used by the `bin/akai.js` shim. Returns the exit code so
37
+ * tests can assert on it without spawning a child process.
38
+ *
39
+ * Exit codes:
40
+ * 0 — success
41
+ * 1 — runtime error (missing/malformed package, import failure)
42
+ * 2 — usage error (bad/missing argv)
43
+ */
44
+ export declare function run(argv: readonly string[]): Promise<number>;
45
+ export {};
46
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAiBzC,KAAK,UAAU,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC1D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAyB7D;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDlE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;;;;GAQG;AACH,wBAAsB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAclE"}
package/dist/cli.js ADDED
@@ -0,0 +1,148 @@
1
+ import { resolve, join, isAbsolute } from 'node:path';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { pathToFileURL } from 'node:url';
4
+ function isFileNotFound(err) {
5
+ return (typeof err === 'object' &&
6
+ err !== null &&
7
+ 'code' in err &&
8
+ err.code === 'ENOENT');
9
+ }
10
+ function errorMessage(err) {
11
+ return err instanceof Error ? err.message : String(err);
12
+ }
13
+ const USAGE = 'usage: akai build-manifest <package-path> [--out file]';
14
+ /**
15
+ * Parse argv (minus `node` + script path). Single-command CLI today —
16
+ * `build-manifest` is the only verb. Exits non-zero with a clear stderr
17
+ * message on any usage error.
18
+ */
19
+ export function parseArgs(argv) {
20
+ if (argv.length === 0)
21
+ return { ok: false, error: USAGE };
22
+ const [command, ...rest] = argv;
23
+ if (command === '-h' || command === '--help')
24
+ return { ok: false, error: USAGE };
25
+ if (command !== 'build-manifest')
26
+ return { ok: false, error: `unknown command: ${command}\n${USAGE}` };
27
+ let packagePath;
28
+ let out;
29
+ for (let i = 0; i < rest.length; i++) {
30
+ const arg = rest[i];
31
+ if (arg === '--out') {
32
+ const value = rest[i + 1];
33
+ if (value === undefined)
34
+ return { ok: false, error: '--out requires a filename' };
35
+ out = value;
36
+ i++;
37
+ }
38
+ else if (arg === '-h' || arg === '--help') {
39
+ return { ok: false, error: USAGE };
40
+ }
41
+ else if (packagePath === undefined) {
42
+ packagePath = arg;
43
+ }
44
+ else {
45
+ return { ok: false, error: `unexpected argument: ${arg}` };
46
+ }
47
+ }
48
+ if (packagePath === undefined)
49
+ return { ok: false, error: `missing <package-path>\n${USAGE}` };
50
+ return { ok: true, packagePath, out };
51
+ }
52
+ /**
53
+ * Resolve a package directory to its compiled entry, dynamic-import it,
54
+ * and verify the default export is a {@link CLIDef}. Reads `package.json`
55
+ * for `main`; falls back to `index.js`. Brand-checks `__akaiCLI: true` to
56
+ * reject hand-crafted objects that happen to share the shape.
57
+ */
58
+ export async function loadCli(packagePath) {
59
+ const abs = isAbsolute(packagePath) ? packagePath : resolve(process.cwd(), packagePath);
60
+ const pkgJsonPath = join(abs, 'package.json');
61
+ let entry = join(abs, 'index.js');
62
+ // ENOENT is the only "fall through to the default" case. Anything else
63
+ // (permission denied, malformed JSON, missing/invalid `main`) is a hard
64
+ // error — silently emitting a manifest from the wrong entry point is worse
65
+ // than failing loud.
66
+ let pkgRaw;
67
+ try {
68
+ pkgRaw = await readFile(pkgJsonPath, 'utf8');
69
+ }
70
+ catch (err) {
71
+ if (!isFileNotFound(err)) {
72
+ throw new Error(`${packagePath}: failed to read package.json: ${errorMessage(err)}`);
73
+ }
74
+ }
75
+ if (pkgRaw !== undefined) {
76
+ let main;
77
+ try {
78
+ main = JSON.parse(pkgRaw).main;
79
+ }
80
+ catch (err) {
81
+ throw new Error(`${packagePath}/package.json: invalid JSON: ${errorMessage(err)}`);
82
+ }
83
+ if (typeof main === 'string' && main.length > 0) {
84
+ entry = join(abs, main);
85
+ }
86
+ }
87
+ let mod;
88
+ try {
89
+ mod = await import(pathToFileURL(entry).href);
90
+ }
91
+ catch (err) {
92
+ const msg = err instanceof Error ? err.message : String(err);
93
+ throw new Error(`failed to import ${entry}: ${msg}`);
94
+ }
95
+ const def = mod.default;
96
+ if (def === null ||
97
+ typeof def !== 'object' ||
98
+ def.__akaiCLI !== true) {
99
+ throw new Error(`${packagePath}: default export is not a CLIDef (missing __akaiCLI brand)`);
100
+ }
101
+ return def;
102
+ }
103
+ /**
104
+ * Load the CLI, project it to a manifest, and write the JSON either to
105
+ * stdout (default) or the path given by `--out`.
106
+ *
107
+ * Uses `cli.toSchema()` (the instance method attached at `defineCLI`
108
+ * time) rather than this SDK's imported `toSchema(cli)`, so the target
109
+ * package's bundled SDK version wins. Per-tenant CLIs may ship with a
110
+ * different `@akai/cli` than the one running the binary — calling the
111
+ * instance method emits the manifest shape that package was authored
112
+ * against.
113
+ */
114
+ export async function buildManifest(packagePath, out) {
115
+ const cli = await loadCli(packagePath);
116
+ const json = `${JSON.stringify(cli.toSchema(), null, 2)}\n`;
117
+ if (out !== undefined) {
118
+ await writeFile(out, json, 'utf8');
119
+ }
120
+ else {
121
+ process.stdout.write(json);
122
+ }
123
+ }
124
+ /**
125
+ * Entry point used by the `bin/akai.js` shim. Returns the exit code so
126
+ * tests can assert on it without spawning a child process.
127
+ *
128
+ * Exit codes:
129
+ * 0 — success
130
+ * 1 — runtime error (missing/malformed package, import failure)
131
+ * 2 — usage error (bad/missing argv)
132
+ */
133
+ export async function run(argv) {
134
+ const parsed = parseArgs(argv);
135
+ if (!parsed.ok) {
136
+ process.stderr.write(`akai: ${parsed.error}\n`);
137
+ return 2;
138
+ }
139
+ try {
140
+ await buildManifest(parsed.packagePath, parsed.out);
141
+ return 0;
142
+ }
143
+ catch (err) {
144
+ const msg = err instanceof Error ? err.message : String(err);
145
+ process.stderr.write(`akai build-manifest: ${msg}\n`);
146
+ return 1;
147
+ }
148
+ }
@@ -0,0 +1,46 @@
1
+ import { type PropertyValues, type SecretValues } from './secrets.js';
2
+ import type { CLIDef, ToolCtx, ToolLogger } from '../types.js';
3
+ /**
4
+ * Inputs to {@link buildCtx} — what the host runtime collects per
5
+ * invocation before calling a tool's handler.
6
+ */
7
+ export interface BuildCtxParams {
8
+ readonly cli: CLIDef;
9
+ readonly toolName: string;
10
+ /** Resolved secret values (one per key declared at the CLI level), sourced from the runtime request body. */
11
+ readonly secretValues: SecretValues;
12
+ /** Resolved tenant-property values, sourced alongside `secretValues`. Optional — defaults to `{}`. */
13
+ readonly propertyValues?: PropertyValues;
14
+ readonly logger: ToolLogger;
15
+ readonly signal: AbortSignal;
16
+ readonly workdir: string;
17
+ readonly sdkVersion: string;
18
+ readonly tenantId?: string;
19
+ /**
20
+ * Extra roots beyond `workdir` + `os.tmpdir()` that the runtime considers
21
+ * safe for this tool's path I/O. Worker passes its `AGENT_WORKDIR` and
22
+ * `DT_SAFE_PATH_ROOTS` here for parity with the rest of its CLI tree.
23
+ */
24
+ readonly extraSafePathRoots?: readonly string[];
25
+ }
26
+ /**
27
+ * Assemble a frozen {@link ToolCtx} for one tool invocation: resolves
28
+ * the named tool from the CLI, scopes secrets to the keys the tool
29
+ * declared, and wires a sandboxed `ctx.fetch` bound to the tool's
30
+ * egress allowlist (or a throwing stub when the tool is native).
31
+ *
32
+ * Used by the host runtime per `/tools/execute` request — handler
33
+ * authors receive the resulting `ctx` and don't call `buildCtx`
34
+ * directly.
35
+ *
36
+ * @param params - see {@link BuildCtxParams}.
37
+ * @returns frozen {@link ToolCtx} ready to hand to the tool's handler.
38
+ * @throws {Error} when `toolName` is not registered in `cli.tools`.
39
+ * @throws {AkaiSecretError} when the resolved tool's `secretKeys`
40
+ * reference a key not declared at the CLI level, or when a referenced
41
+ * key whose CLI declaration is `required: true` has no value in
42
+ * `secretValues`. CLI-level required secrets that this tool does not
43
+ * reference are not checked.
44
+ */
45
+ export declare function buildCtx(params: BuildCtxParams): ToolCtx;
46
+ //# sourceMappingURL=build-ctx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-ctx.d.ts","sourceRoot":"","sources":["../../src/ctx/build-ctx.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAc,MAAM,EAAiB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG1F;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6GAA6G;IAC7G,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,sGAAsG;IACtG,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjD;AA4CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CA4CxD"}