@akai-workflow-builder/cli-sdk

中文文档
0.15.1发布于 2026/10/29MITTypeScriptES Module近 30 天下载 2541

Authoring SDK for atomic, agent-executable CLIs and tools.

原始使用文档
文档目录25 节
  1. Installation
  2. Requirements
  3. Quickstart
  4. Defining a connection
  5. OAuth connections
  6. Hosted MCP server connections
  7. Core concepts
  8. Atomic tools
  9. defineCLI()
  10. Tool keys
  11. The ctx contract
  12. Secrets — declarations vs references
  13. Properties — non-sensitive tenant config
  14. Dynamic egress (multi-tenant connectors)
  15. Egress allowlist
  16. Operator-pinned private egress (host runtime only)
  17. isReadonly
  18. Output mode: JSON vs text
  19. Worked example: Zendesk
  20. Errors
  21. Typed tool errors
  22. Troubleshooting
  23. Changelog
  24. Unreleased
  25. License

@akai-workflow-builder/cli-sdk

Author atomic, agent-executable CLIs in TypeScript.

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.

Status: pre-v0.1. The authoring surface (tool, defineCLI, buildCtx, ctx.fetch, ctx.secrets, ctx.properties) is stable.


Installation

npm install @akai-workflow-builder/cli-sdk zod

Requirements

Node≥ 24
TypeScript≥ 5.0 (uses const type parameters for literal-key inference)
zod^4.4.1 (peer dependency)
tsconfig.jsonneeds Response / RequestInit / AbortSignal. Either add "DOM" to your existing lib entries (e.g. "lib": ["ES2023", "DOM"]) or install @types/node@^18+ and list it in "types" (e.g. "types": ["node"]) — TypeScript 6 no longer loads @types/* packages that types does not name

Quickstart

import { tool, defineCLI } from '@akai-workflow-builder/cli-sdk';
import { z } from 'zod';

// Define the response shape once; reuse for `jsonOutput` and runtime parsing.
const Response = z.object({
  user: z.object({ id: z.number(), email: z.string() }),
});

const usersMe = tool({
  name: 'Authenticated user',
  description: 'Return the authenticated user.',
  jsonOutput: Response,                          // input defaults to z.object({}) when omitted
  options: {
    network: { egress: 'zendesk.com' },
    secretKeys: ['ZENDESK_SUBDOMAIN', 'ZENDESK_API_TOKEN'],
    isReadonly: true,
  },
  handler: async ({ ctx, isJson }) => {
    const sub = ctx.secrets.ZENDESK_SUBDOMAIN!;
    const tok = ctx.secrets.ZENDESK_API_TOKEN!;
    // Quickstart trusts `sub` is a workspace shortname ("acme"); the worked
    // example validates it before constructing the URL.
    const res = await ctx.fetch(`https://${sub}.zendesk.com/api/v2/users/me.json`, {
      signal: ctx.signal,
      headers: { Authorization: `Bearer ${tok}` },
    });
    if (!res.ok) throw new Error(`fetch authenticated user failed: ${res.status}`);
    // Parse, don't cast — strips unknown fields, throws if the shape changes.
    const body = Response.parse(await res.json());
    return isJson ? body : `User #${body.user.id} <${body.user.email}>`;
  },
});

export default defineCLI({
  id: 'zendesk',
  name: 'Zendesk Support',
  summary: 'Read tickets, users, and search results from Zendesk Support.',
  description:
    '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.',
  secrets: [
    { key: 'ZENDESK_SUBDOMAIN', name: 'Workspace subdomain', description: '...', required: true },
    { key: 'ZENDESK_API_TOKEN', name: 'API access token',    description: '...', required: true },
  ],
  tools: {
    'users.me': usersMe,
  },
});

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).

Defining a connection

A connection is the umbrella identity that groups CLIs sharing one auth surface — e.g. google groups Gmail and Sheets. Declare it with defineConnection, symmetric to defineCLI:

import { defineConnection } from '@akai-workflow-builder/cli-sdk';

export default defineConnection({
  slug: 'google',                 // groups CLIs that set connectionSlug: 'google'
  name: 'Google Workspace',       // umbrella display name
  vendor: 'Google',
  blurb: 'Gmail, Sheets, Docs, Drive and Calendar in one connection.',
  longDesc: 'Connect once with OAuth; every Google CLI shares the authorization.',
  authKind: 'oauth2',
  oauth: { /* see below */ },
});

CLIs link to it through the existing defineCLI — set connectionSlug:

const gmail  = defineCLI({ id: 'gmail',  connectionSlug: 'google', summary: '…', tools: { … } });
const gsheet = defineCLI({ id: 'gsheet', connectionSlug: 'google', summary: '…', tools: { … } });

When to declare one: only when ≥2 CLIs share a slug and need a single umbrella identity. A single-CLI connection needs no defineConnection — its identity comes from that CLI's own metadata (pdf, sandbox and friends are unchanged).

defineConnection owns one concern: identity. It does not own tool sourcing, OAuth secret glue, or UI cosmetics — those split by source:

ConcernSource
Identity (name / blurb / longDesc / instructions / vendor / authKind)defineConnection
OAuth shape (authorize/token URLs, scopes, pkce, params, refresh, admin fields)defineConnection.oauth
OAuth secret VALUES, token storage, encryption, refresh hooksakai-app, keyed by slug
UI cosmetics (hue / mark / logo)akai-app, keyed by slug

A complete runnable example is in examples/google — defineConnection plus two CLIs (gmail, gsheet) grouped under it.

OAuth connections

When authKind is oauth2, declare an oauth block. The manifest declares shape; akai-app owns values:

oauth: {
  authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  tokenUrl: 'https://oauth2.googleapis.com/token',
  scopes: [                                             // OAuth authorize scopes (≥1); distinct from per-CLI vendorScopes
    'https://mail.google.com/',
    'https://www.googleapis.com/auth/drive',
  ],
  pkce: true,                                           // Google requires PKCE; defaults to false
  tokenEndpointAuthMethod: 'client_secret_post',        // default 'client_secret_basic'
  extraAuthorizeParams: { access_type: 'offline', prompt: 'consent' },
  refreshStrategy: 'cron',                              // 'cron' (background refresh) | 'jit' (refresh on next use); default 'cron'
  adminFields: [
    { key: 'GMAIL_CLIENT_ID',     label: 'Client ID' },
    { key: 'GMAIL_CLIENT_SECRET', label: 'Client Secret', secret: true },
  ],
}
  • scopes is the connection-level OAuth authorize scope set (≥1) the engine joins onto the authorize URL — distinct from a CLI's per-tool vendorScopes.
  • Set pkce when the provider requires it; set extraAuthorizeParams for static authorize params (Google's access_type: 'offline' to get a refresh token); leave refreshStrategy at the default cron (a background job refreshes the token before it expires) or set jit to refresh just-in-time on the next use.
  • adminFields declares which OAuth-app credentials an admin must enter (env-style key + UI label). The VALUES are stored encrypted in akai-app, never in the manifest or the per-tenant image.
  • Mark a field secret: true so akai-app masks the input in the admin form and redacts it from logs.

See the connection-identity and define-connection-oauth migration specs (akai-app app/connections/migration-specs/) for per-field guidance.

Hosted MCP server connections

A connection can stand for a vendor's hosted MCP server (Notion, Linear, Stripe, …) instead of a set of CLIs. Set mcpServer:

export default defineConnection({
  slug: 'notion-mcp',
  name: 'Notion',
  vendor: 'Notion',
  blurb: 'Search and edit Notion pages through the Notion MCP server.',
  longDesc: 'Sign in with Notion. Akai talks to the server directly; no CLI tools are involved.',
  authKind: 'oauth2',                              // or 'none' for an open server
  mcpServer: { url: 'https://mcp.notion.com/mcp' }, // must be https
});
  • The host (akai-app) turns the connection into an MCP connection. With authKind: 'oauth2' it discovers the server's OAuth metadata at sign-in and registers itself through RFC 7591 dynamic client registration, so there is no static OAuth client and no admin fields: oauth must not be set.
  • authKind must be oauth2 or none (apiKey is not supported yet). secrets, apiKeyHeader and jwtRefresh must not be set, and nativeBuiltin must not be true.
  • It is standalone: no defineCLI links to its slug, and the registry still lists it. On /tools/list it appears in connections with mcpServer: { url } (the field is omitted on every other connection).

Core concepts

Atomic tools

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.

Three consequences:

  • Per-tool permissions are meaningful — one tool = one observable side effect = one approval surface.
  • Schemas stay tight — each tool documents one operation, not a workflow.
  • No hidden side effects — what the schema describes is what executes.

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.

defineCLI()

A CLI is a named bundle of tools sharing one secret-declaration set:

defineCLI({
  id: 'zendesk',
  name: 'Zendesk Support',
  summary: 'Read tickets, users, and search results from Zendesk Support.',
  description: 'Connect to a Zendesk Support workspace via OAuth bearer token …',
  secrets: [/* ... */],
  tools: {
    'users.me':     usersMe,
    'tickets.get':  getTicket,
    'tickets.list': listTickets,
  },
});

Required

FieldRole
idMachine slug. Used for wire ids (<id>.<toolKey>) and the x-akai-cli header. Pattern: ^[a-z][a-z0-9_-]*$.
nameHuman display label.
summaryOne-sentence short label.
toolsMap of tool() results. Keys follow the kebab + dotted convention below.

Optional

FieldRole
vendorBrand/company. Defaults to name. Useful when display ("JPMorgan Access Portal") differs from vendor ("JPMorgan").
descriptionLong-form description. Consumers decide how to render when omitted.
instructionsOrdered setup steps shown to operators.
secretsArray of SecretDecl (the keys tools reference).
propertiesArray of PropertyDecl for non-sensitive tenant config (base URLs, region codes). Distinct from secrets[].

defineCLI() cross-validates every secretKeys and propertyKeys reference against the CLI-level declarations. Mismatches throw AkaiSpecError at module load — no silent failures.

Tool keys

Tool keys are kebab-case, optionally dot-namespaced for grouping:

chat-post-message
users.me
tickets.search-by-query
activations.contract-benefit.mark-enrolled-ahead-of-ingest

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>.

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 repo.

The ctx contract

Tool handlers receive a frozen ctx with these members:

interface ToolCtx<S extends string, P extends string> {
  fetch:      (url: string, init?: RequestInit) => Promise<Response>;
  secrets:    Readonly<Record<S, string | undefined>>;
  properties: Readonly<Record<P, string | undefined>>;
  logger:     ToolLogger;
  signal:     AbortSignal;
  workdir:    string;
  safePath:   (userPath: string) => string;
  readFile:   (value: string) => Promise<Buffer>;
}

What you get

  • fetch — sandboxed fetch bound to the tool's egress allowlist.
  • secrets — only the secret keys the tool declared in secretKeys. Other workspace secrets are inaccessible.
  • properties — only the property keys the tool declared in propertyKeys. Non-sensitive tenant config (base URL, region) lives here, not in secrets.
  • logger — structured logger; writes to stderr. stdout is reserved for the tool's result.
  • signal — request AbortSignal. Forward it to ctx.fetch for cancellation and timeouts.
  • workdir — writable scratch directory scoped to this invocation.
  • safePath — resolve an agent-supplied path under the tool's allowed roots (default: workdir + os.tmpdir()); strips file://, follows symlinks on every existing parent segment, and throws AkaiPathError on escape.
  • readFile — read a local path (resolved via safePath) or an http(s):// URL; pairs with filePath() from the SDK so handlers transparently support cross-pod file delivery.

What ctx deliberately omits

  • No process.env accessor — secrets flow through ctx.secrets so the runtime can scope and audit them.
  • 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.
  • No handle to invoke sibling tools — composition is the planner's job.

Secrets — declarations vs references

Secrets are declared once at the CLI level and referenced by key on each tool that needs them:

defineCLI({
  secrets: [
    { key: 'ZENDESK_API_TOKEN', name: 'API token', description: '...', required: true },
  ],
  tools: {
    getTicket: tool({
      options: {
        secretKeys: ['ZENDESK_API_TOKEN'],
        /* ... */
      },
      /* ... */
    }),
  },
});
FieldTypeRole
CLIsecretsSecretDecl[]Operator-facing declarations
Tooloptions.secretKeysstring[]Per-tool subset
Runtimectx.secretsReadonly<Record<S, string | undefined>>What the handler reads

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.

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.

Properties — non-sensitive tenant config

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.

defineCLI({
  secrets:    [{ key: 'JIRA_API_TOKEN', name: 'API token',     description: '...', required: true }],
  properties: [{ key: 'JIRA_BASE_URL',  name: 'Workspace URL', description: '...', required: true }],
  tools: {
    getIssue: tool({
      options: {
        secretKeys:   ['JIRA_API_TOKEN'],
        propertyKeys: ['JIRA_BASE_URL'],
        network:      { egress: { from: 'property:JIRA_BASE_URL' } },
        isReadonly:   true,
      },
      handler: async ({ ctx }) => {
        const base = ctx.properties.JIRA_BASE_URL!;
        const tok  = ctx.secrets.JIRA_API_TOKEN!;
        return ctx.fetch(`https://${base}/rest/api/3/issue/...`, {
          headers: { Authorization: `Bearer ${tok}` },
        });
      },
      /* ... */
    }),
  },
});
FieldTypeRole
CLIpropertiesPropertyDecl[]Operator-facing declarations
Tooloptions.propertyKeysstring[]Per-tool subset
Runtimectx.propertiesReadonly<Record<P, string | undefined>>What the handler reads

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.

Dynamic egress (multi-tenant connectors)

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:

options: {
  propertyKeys: ['JIRA_BASE_URL'],
  network:      { egress: { from: 'property:JIRA_BASE_URL' } },
}

defineCLI() cross-checks that the referenced key is declared in properties[] AND included in the tool's propertyKeys. buildCtx resolves the reference itself — extracting the hostname from the bound secret or property value — so both the in-process step-fetcher path and the executor derive the same allowlisted host. Resolution throws AkaiSpecError on an unsupported prefix, AkaiSecretError/AkaiPropertyError when the value is unset or not a valid host.

Egress allowlist

Every tool declares its allowed outbound hosts:

options: {
  network: { egress: 'api.example.com' },                  // single host (shorthand)
  // or:    { egress: ['api.example.com', 'cdn.example.com'] },
}

ctx.fetch enforces, in order:

  • Protocol — HTTPS-only in production. HTTP allowed only when NODE_ENV === 'development'.
  • Host — exact-or-suffix-with-dot. api.example.com matches itself and *.api.example.com; evil.api.example.com.attacker.tld does not.
  • 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. The one production exception is an operator pin passed by the host runtime — see Operator-pinned private egress.
  • Redirects — the SDK never follows a redirect, so a Location: to another host cannot smuggle a request past the egress check. redirect: 'error' is the default and 'follow' is refused (it is downgraded to 'error'), because following is the one mode that could leave the allowlisted host after the check has passed. redirect: 'manual' is honoured: it follows nothing, so the handler receives the 3xx itself — which is how a form-post portal reports success — and reaching the Location: costs a fresh ctx.fetch that re-runs the whole preflight.
  • 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.

Protocol, host, and DNS-preflight failures reject with AkaiEgressError before any network request is made. Under redirect: 'error' — the default, and where 'follow' is downgraded to — a redirect surfaces instead as a fetch rejection after the initial response: same outcome (no follow-through), different error type. Under 'manual' there is no failure to surface, because the 3xx is a successful result: it is returned to the handler as an ordinary response.

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.

Operator-pinned private egress (host runtime only)

The DNS preflight refuses every private address, because a tool's host can come from a connection secret that a tenant user controls. A host runtime that deploys next to one platform-owned internal service can open exactly one exception for it. It passes privateEgress to executeTool (or buildCtx, or createSafeFetch):

import { executeTool } from '@akai-workflow-builder/cli-sdk/runtime';

await executeTool({
  /* …registry, body, logger, signal, workdir… */
  privateEgress: [
    {
      cli: 'grafana',                       // the CLI id; no other CLI may use this entry
      host: 'grafana.shared.akai',          // exact hostname; no suffix match, never an IP literal
      cidrs: ['10.33.0.0/16'],              // the host must resolve inside these (each at most /16)
      port: 443,                            // optional, default 443
      caPem: '-----BEGIN CERTIFICATE-----…', // optional CA for this connection; default trust store otherwise
    },
  ],
});

It is off by default: with no privateEgress the behaviour is exactly what is described above. The value comes from the host runtime's own configuration and from nowhere else — a tool definition, its options, secrets, properties, the request body and the process environment are never read as a pin.

A pin allows a call to reach a private address only when ALL of these hold; otherwise the unchanged resolved to private address error is thrown:

  • the calling CLI id equals cli;
  • the URL host equals host exactly (case-insensitive; x.host and evilhost do not match) and is also on the tool's own egress allowlist;
  • the scheme is https: and the port equals port;
  • EVERY resolved address is inside cidrs, and none is loopback, link-local (including the cloud metadata address) or unspecified. IPv4-mapped IPv6 forms (::ffff:127.0.0.1, ::ffff:7f00:1) are judged as the IPv4 address they are.

The allowed call goes over node:https, held to what the check saw: the socket connects only to the addresses that passed (a later DNS answer cannot redirect it), the TLS name is the pinned host, caPem — when set — replaces the trust store for that connection, certificate verification stays on, and the Host header is forced to the pinned host whatever the handler set. Redirects follow the usual rule: refused unless redirect: 'manual', and following a Location: is a fresh ctx.fetch with the full check.

An entry is ignored (and logged once through logger.warn) rather than half-applied when its cli is not a CLI id, its host is an IP literal or not a fully qualified name, cidrs is empty or malformed, a CIDR is wider than /16 or overlaps loopback, link-local or unspecified space (127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, ::1, fe80::/10, the IPv6 metadata address), port is not 1–65535, or caPem is not a PEM certificate.

isReadonly

Every tool must declare whether it mutates remote state:

options: {
  isReadonly: true,   // reads, queries — no approval gate
  // isReadonly: false, // writes — runtime pauses for human approval
}

Two roles:

  1. Planner hint — exposed in the tool listing so plan UIs render write steps distinctly.
  2. Runtime gate — write tools cannot execute unless a confirmation flag is set, which the runtime flips only after the approval flow returns approved.

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.

Output mode: JSON vs text

Each handler receives an isJson: boolean flag. The caller chooses format=json or format=text; the handler returns either shape:

handler: async ({ input, ctx, isJson }) => {
  const body = await fetchTheThing(/* ... */);
  return isJson ? body : `Thing #${body.id} (${body.status})`;
}

When jsonOutput is declared, the handler return type is string | ZInfer<jsonOutput>. When jsonOutput is omitted, the handler always returns a string.


Worked example: Zendesk

The repository's examples/zendesk/ folder ships a five-tool, read-only Zendesk Support connector (not bundled in the published package):

ToolEndpointDemonstrates
users.meGET /users/meAuth probe; simplest tool
users.getGET /users/{id}Path parameter
tickets.getGET /tickets/{id}Path parameter, 404 mapped to typed error
tickets.listGET /ticketsCursor pagination
tickets.searchGET /searchQuery composition, 422 surfaced

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.


Errors

ClassWhen
AkaiSpecErrorInvalid tool() / defineCLI() spec — thrown at module load
AkaiInputErrorTool input fails the declared zod schema — thrown by the runtime when validating the request body
AkaiSecretErrorA required: true secret is missing at invocation time — thrown by buildCtx
AkaiPropertyErrorA required: true property is missing at invocation time — thrown by buildCtx
AkaiEgressErrorctx.fetch rejected the target
AkaiToolErrorA handler threw a typed, caller-facing failure — the host maps kind to an HTTP status and surfaces the message verbatim

Every error extends AkaiError; the runtime maps each to a structured error envelope.

Typed tool errors

Throw AkaiToolError from a handler to give an expected failure a stable classification and author-vetted wording. The host runtime brand-checks the thrown value by property access on its shape — { name: 'AkaiToolError', kind, message, meta? } (it reads message via property access, not JSON serialization, since Error.message is non-enumerable) — with no SDK dependency, maps kind to an HTTP status, and returns message to the caller verbatim. Keep messages secret-free.

import { AkaiToolError } from '@akai-workflow-builder/cli-sdk';

const res = await ctx.fetch(`https://api.example.com/tickets/${id}`);
if (res.status === 404) throw AkaiToolError.notFound(`No ticket ${id}`, { id });
if (res.status === 429) throw AkaiToolError.rateLimited('Slow down', { retryAfterMs: 1000 });

Factories — each sets the matching kind:

FactorykindHost status
AkaiToolError.notFound(msg, meta?)not_found404
AkaiToolError.forbidden(msg, meta?)forbidden403
AkaiToolError.authExpired(msg, meta?)auth_expired401
AkaiToolError.invalidInput(msg, meta?)invalid_input422
AkaiToolError.conflict(msg, meta?)conflict409
AkaiToolError.rateLimited(msg, meta?)rate_limited429 (retryable)
AkaiToolError.unavailable(msg, meta?)unavailable503 (retryable)

Untyped throws are classified generically (a 500 with a derived message).


Troubleshooting

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 install @types/node@^18+ and add "types": ["node"] (TypeScript 6 defaults types to [], so installing it alone is not enough).

AkaiSpecError: undeclared secret 'X' — a secretKeys entry references a key that isn't in defineCLI({ secrets: [...] }).

AkaiSpecError: undeclared property 'X' — a propertyKeys entry references a key that isn't in defineCLI({ properties: [...] }).

AkaiSpecError: invalid id "..." — CLI id must be a lowercase slug matching ^[a-z][a-z0-9_-]*$. The display label lives in name.

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.

AkaiEgressError against an unallowed host — runtime egress check failed. Add the host to options.network.egress (matched exact-or-suffix-with-dot).

Type error: handler return doesn't match jsonOutput — when jsonOutput is declared, the handler return type is string | <structured shape>. Branch on isJson.


Changelog

Unreleased

  • Add defineConnection — declare a connection's umbrella identity (slug, name, vendor, blurb, longDesc, instructions, authKind) plus its oauth shape. Returns a frozen, WeakSet-branded ConnectionDef. Symmetric to defineCLI.
  • Add the oauth block: authorizeUrl, tokenUrl, scopes, pkce, tokenEndpointAuthMethod, extraAuthorizeParams, refreshStrategy, and adminFields (the OAuth-app credentials an admin enters; values stay in akai-app). Validated for valid URLs, non-empty scopes, env-style admin keys, and adminFields presence when authKind is oauth2.
  • New exports: defineConnection, isAkaiConnection, AkaiConnectionError, and types ConnectionSpec, ConnectionDef, ConnectionOAuthConfig, OAuthAdminField, ConnectionAuthKind, RefreshStrategy, TokenEndpointAuthMethod.
  • New runnable example: examples/google (a connection grouping gmail + gsheet).

License

MIT — see LICENSE.

版本与发布记录

@akai-workflow-builder/cli-sdk 共发布 39 个版本,首次发布于 2026/05/27;过去 12 个月发布了 39 个版本。

版本发布日期距今
0.15.1latest2026/10/299 天前
0.15.02026/10/2216 天前
0.14.02026/10/1722 天前
0.13.02026/10/1325 天前
0.12.02026/09/281 个月前
0.11.12026/09/251 个月前

关于 @akai-workflow-builder/cli-sdk 的常见问题

@akai-workflow-builder/cli-sdk 是什么?

Authoring SDK for atomic, agent-executable CLIs and tools.

如何安装 @akai-workflow-builder/cli-sdk?

在项目目录运行 npm install @akai-workflow-builder/cli-sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:zod(^4.4.1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 akai,可以用 npx akai 直接运行。

@akai-workflow-builder/cli-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@akai-workflow-builder/cli-sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@akai-workflow-builder/cli-sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@akai-workflow-builder/cli-sdk 可以商用吗?

@akai-workflow-builder/cli-sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@akai-workflow-builder/cli-sdk 的下载量有多少?

根据 npm 官方下载统计,@akai-workflow-builder/cli-sdk 在 2026/10/05 – 2026/11/03 这 30 天内被下载 2,541 次,日均约 85 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@akai-workflow-builder/cli-sdk 还在维护吗?

最新版本 0.15.1 发布于 2026/10/29(9 天前);至今共发布 39 个版本,其中过去 12 个月发布了 39 个;首次发布于 2026/05/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。