@kindgi/cli 0.2.0 → 0.3.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.
Files changed (152) hide show
  1. package/README.md +12 -5
  2. package/dist/cli.js +4 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +75 -19
  6. package/dist/commands/agents.js.map +1 -1
  7. package/dist/commands/approval-wait.d.ts +90 -0
  8. package/dist/commands/approval-wait.d.ts.map +1 -0
  9. package/dist/commands/approval-wait.js +154 -0
  10. package/dist/commands/approval-wait.js.map +1 -0
  11. package/dist/commands/artifacts.d.ts.map +1 -1
  12. package/dist/commands/artifacts.js +131 -14
  13. package/dist/commands/artifacts.js.map +1 -1
  14. package/dist/commands/auth.d.ts.map +1 -1
  15. package/dist/commands/auth.js +48 -5
  16. package/dist/commands/auth.js.map +1 -1
  17. package/dist/commands/deploy.js +7 -3
  18. package/dist/commands/deploy.js.map +1 -1
  19. package/dist/commands/deployments.d.ts.map +1 -1
  20. package/dist/commands/deployments.js +36 -13
  21. package/dist/commands/deployments.js.map +1 -1
  22. package/dist/commands/doctor.d.ts +3 -2
  23. package/dist/commands/doctor.d.ts.map +1 -1
  24. package/dist/commands/doctor.js +47 -14
  25. package/dist/commands/doctor.js.map +1 -1
  26. package/dist/commands/eval-runs.d.ts.map +1 -1
  27. package/dist/commands/eval-runs.js +21 -9
  28. package/dist/commands/eval-runs.js.map +1 -1
  29. package/dist/commands/helpers.d.ts +31 -1
  30. package/dist/commands/helpers.d.ts.map +1 -1
  31. package/dist/commands/helpers.js +45 -2
  32. package/dist/commands/helpers.js.map +1 -1
  33. package/dist/commands/init.js +5 -4
  34. package/dist/commands/init.js.map +1 -1
  35. package/dist/commands/mail-triggers.d.ts.map +1 -1
  36. package/dist/commands/mail-triggers.js +27 -3
  37. package/dist/commands/mail-triggers.js.map +1 -1
  38. package/dist/commands/promote.d.ts.map +1 -1
  39. package/dist/commands/promote.js +133 -11
  40. package/dist/commands/promote.js.map +1 -1
  41. package/dist/commands/provenance.d.ts.map +1 -1
  42. package/dist/commands/provenance.js +13 -5
  43. package/dist/commands/provenance.js.map +1 -1
  44. package/dist/commands/providers.d.ts.map +1 -1
  45. package/dist/commands/providers.js +47 -4
  46. package/dist/commands/providers.js.map +1 -1
  47. package/dist/commands/schedules.d.ts.map +1 -1
  48. package/dist/commands/schedules.js +25 -9
  49. package/dist/commands/schedules.js.map +1 -1
  50. package/dist/commands/skills.d.ts +16 -0
  51. package/dist/commands/skills.d.ts.map +1 -1
  52. package/dist/commands/skills.js +122 -36
  53. package/dist/commands/skills.js.map +1 -1
  54. package/dist/commands/webhooks.d.ts.map +1 -1
  55. package/dist/commands/webhooks.js +21 -5
  56. package/dist/commands/webhooks.js.map +1 -1
  57. package/dist/config.d.ts +8 -0
  58. package/dist/config.d.ts.map +1 -1
  59. package/dist/config.js +11 -0
  60. package/dist/config.js.map +1 -1
  61. package/dist/context.d.ts +7 -3
  62. package/dist/context.d.ts.map +1 -1
  63. package/dist/context.js +1 -0
  64. package/dist/context.js.map +1 -1
  65. package/dist/dev/runtime-image.d.ts +1 -1
  66. package/dist/dev/runtime-image.js +1 -1
  67. package/dist/errors.d.ts.map +1 -1
  68. package/dist/errors.js +14 -1
  69. package/dist/errors.js.map +1 -1
  70. package/dist/exports/plan.d.ts.map +1 -1
  71. package/dist/exports/plan.js +50 -0
  72. package/dist/exports/plan.js.map +1 -1
  73. package/dist/init/augment-scaffolder.d.ts +1 -1
  74. package/dist/init/augment-scaffolder.js +9 -8
  75. package/dist/init/augment-scaffolder.js.map +1 -1
  76. package/dist/init/java-augment.d.ts.map +1 -1
  77. package/dist/init/java-augment.js +7 -6
  78. package/dist/init/java-augment.js.map +1 -1
  79. package/dist/init/python-augment.d.ts.map +1 -1
  80. package/dist/init/python-augment.js +7 -6
  81. package/dist/init/python-augment.js.map +1 -1
  82. package/dist/init/scala-augment.js +7 -6
  83. package/dist/init/scala-augment.js.map +1 -1
  84. package/dist/main.d.ts +3 -0
  85. package/dist/main.d.ts.map +1 -1
  86. package/dist/main.js +1 -0
  87. package/dist/main.js.map +1 -1
  88. package/dist/providers/presets/anthropic.json +15 -3
  89. package/dist/providers/presets/azure-openai.json +10 -2
  90. package/dist/providers/presets/bedrock.json +6 -2
  91. package/dist/providers/presets/gemini-api.json +6 -2
  92. package/dist/providers/presets/openai.json +15 -3
  93. package/dist/sdk-skills/kindgi-authoring-agents/SKILL.md +21 -9
  94. package/dist/sdk-skills/kindgi-authoring-flows/SKILL.md +5 -3
  95. package/dist/sdk-skills/kindgi-authoring-guardrails/SKILL.md +202 -68
  96. package/dist/sdk-skills/kindgi-authoring-mcp-servers/SKILL.md +12 -188
  97. package/dist/sdk-skills/kindgi-authoring-mcp-servers/common-mistakes.md +44 -0
  98. package/dist/sdk-skills/kindgi-authoring-mcp-servers/how-it-works.md +41 -0
  99. package/dist/sdk-skills/kindgi-authoring-mcp-servers/other-clients.md +19 -0
  100. package/dist/sdk-skills/kindgi-authoring-mcp-servers/postgres.md +45 -0
  101. package/dist/sdk-skills/kindgi-authoring-mcp-servers/several-servers.md +32 -0
  102. package/dist/sdk-skills/kindgi-authoring-providers/SKILL.md +49 -906
  103. package/dist/sdk-skills/kindgi-authoring-providers/bedrock.md +64 -0
  104. package/dist/sdk-skills/kindgi-authoring-providers/common-mistakes.md +125 -0
  105. package/dist/sdk-skills/kindgi-authoring-providers/keys-and-registration.md +58 -0
  106. package/dist/sdk-skills/kindgi-authoring-providers/kindgi-cloud.md +36 -0
  107. package/dist/sdk-skills/kindgi-authoring-providers/path-a-anthropic.md +216 -0
  108. package/dist/sdk-skills/kindgi-authoring-providers/path-b-openai-compat.md +127 -0
  109. package/dist/sdk-skills/kindgi-authoring-providers/path-c-onnx.md +97 -0
  110. package/dist/sdk-skills/kindgi-authoring-providers/path-d-vertex.md +95 -0
  111. package/dist/sdk-skills/kindgi-authoring-providers/router.md +113 -0
  112. package/dist/sdk-skills/kindgi-authoring-tools/SKILL.md +12 -387
  113. package/dist/sdk-skills/kindgi-authoring-tools/common-mistakes.md +135 -0
  114. package/dist/sdk-skills/kindgi-authoring-tools/declarative-http.md +34 -0
  115. package/dist/sdk-skills/kindgi-authoring-tools/imperative-handler.md +65 -0
  116. package/dist/sdk-skills/kindgi-authoring-tools/iterating-and-testing.md +45 -0
  117. package/dist/sdk-skills/kindgi-authoring-tools/secrets-and-connections.md +79 -0
  118. package/dist/sdk-skills/kindgi-authoring-tools/tool-flags.md +45 -0
  119. package/dist/sdk-skills/kindgi-fix-corrections/SKILL.md +13 -178
  120. package/dist/sdk-skills/kindgi-fix-corrections/make-the-change.md +25 -0
  121. package/dist/sdk-skills/kindgi-fix-corrections/publish-and-promote.md +76 -0
  122. package/dist/sdk-skills/kindgi-fix-corrections/pull-corrections.md +48 -0
  123. package/dist/sdk-skills/kindgi-fix-corrections/worked-example.md +27 -0
  124. package/dist/sdk-skills/kindgi-framework-feedback/SKILL.md +10 -180
  125. package/dist/sdk-skills/kindgi-framework-feedback/common-mistakes.md +19 -0
  126. package/dist/sdk-skills/kindgi-framework-feedback/editing-feedback-file.md +33 -0
  127. package/dist/sdk-skills/kindgi-framework-feedback/fields-and-entry-shape.md +53 -0
  128. package/dist/sdk-skills/kindgi-framework-feedback/hearing-back.md +24 -0
  129. package/dist/sdk-skills/kindgi-framework-feedback/sending.md +50 -0
  130. package/dist/sdk-skills/kindgi-getting-started/SKILL.md +24 -21
  131. package/dist/sdk-skills/kindgi-java-authoring-agents/SKILL.md +12 -4
  132. package/dist/sdk-skills/kindgi-java-authoring-flows/SKILL.md +6 -4
  133. package/dist/sdk-skills/kindgi-java-authoring-guardrails/SKILL.md +33 -23
  134. package/dist/sdk-skills/kindgi-java-authoring-tools/SKILL.md +19 -12
  135. package/dist/sdk-skills/kindgi-java-getting-started/SKILL.md +16 -16
  136. package/dist/sdk-skills/kindgi-python-authoring-agents/SKILL.md +18 -6
  137. package/dist/sdk-skills/kindgi-python-authoring-flows/SKILL.md +6 -4
  138. package/dist/sdk-skills/kindgi-python-authoring-guardrails/SKILL.md +39 -30
  139. package/dist/sdk-skills/kindgi-python-authoring-tools/SKILL.md +19 -9
  140. package/dist/sdk-skills/kindgi-python-getting-started/SKILL.md +24 -21
  141. package/dist/sdk-skills/kindgi-scala-authoring-agents/SKILL.md +12 -4
  142. package/dist/sdk-skills/kindgi-scala-authoring-flows/SKILL.md +6 -4
  143. package/dist/sdk-skills/kindgi-scala-authoring-guardrails/SKILL.md +34 -23
  144. package/dist/sdk-skills/kindgi-scala-authoring-tools/SKILL.md +15 -9
  145. package/dist/sdk-skills/kindgi-scala-getting-started/SKILL.md +16 -16
  146. package/dist/sdk-skills/kindgi-using/SKILL.md +11 -11
  147. package/dist/templates/sample/README.md.tmpl +8 -7
  148. package/dist/templates/sample/agents/echo-agent/index.ts.tmpl +1 -1
  149. package/dist/templates/sample/kindgi.config.ts.tmpl +1 -1
  150. package/dist/templates/sample/package.json.tmpl +1 -1
  151. package/dist/templates/woocommerce/README.md.tmpl +9 -3
  152. package/package.json +13 -13
@@ -12,8 +12,8 @@ description: >
12
12
  authoring agents is covered by kindgi-authoring-agents.
13
13
  type: core
14
14
  library: "@kindgi/sdk"
15
- version: "0.4.14"
16
- sdk_version: "0.2.0"
15
+ version: "0.5.1"
16
+ sdk_version: "0.3.0"
17
17
  pack_languages: [node]
18
18
  sources:
19
19
  - packages/tools/src/types.ts
@@ -60,227 +60,16 @@ logic lives:
60
60
  `handler` and `spec` are MUTUALLY EXCLUSIVE. Setting both (or neither)
61
61
  makes `defineTool` return an `invalid-tool-definition` error.
62
62
 
63
- ## Imperative handler
63
+ ## Your case: read this file
64
64
 
65
- ```ts
66
- // tools/verify-citation/index.ts
67
- import { defineTool } from '@kindgi/sdk/define';
68
- import type { ToolId } from '@kindgi/sdk/types';
69
- import { z } from 'zod';
70
-
71
- const Input = z.object({
72
- citation: z.string().min(1),
73
- jurisdiction: z.enum(['US', 'UK', 'EU']),
74
- });
75
-
76
- const Output = z.object({
77
- found: z.boolean(),
78
- canonicalCite: z.string().optional(),
79
- });
80
-
81
- const defined = defineTool({
82
- id: 'acme.verify-citation' as ToolId,
83
- description:
84
- 'Verify a legal citation against the jurisdictional citator. Returns whether the citation resolves and the canonical form.',
85
- version: '0.1.0',
86
- input: Input,
87
- output: Output,
88
- effects: [],
89
- mutating: false,
90
- handler: async (input, ctx) => {
91
- // ctx.tenantId, ctx.abortSignal, ctx.secrets (what needsSpec declares) available;
92
- // in a run, ctx.projectId and ctx.orgId (set from the run, never from the input)
93
- // Return type MUST match Output schema (validated at invoke time)
94
- return { found: true, canonicalCite: '...' };
95
- },
96
- });
97
-
98
- if (defined.kind === 'err') {
99
- throw new Error(`acme.verify-citation failed to compile: ${defined.error.message}`);
100
- }
101
-
102
- export default defined.value;
103
- ```
104
-
105
- A handler must return a Promise; one with nothing to `await` can return `Promise.resolve({ … })` instead of being `async`, which keeps lint rules like `require-await` quiet.
106
-
107
- ### What the handler receives
108
-
109
- The handler gets the **parsed** input, typed `z.infer` of `input` (Zod's output type):
110
-
111
- - **Defaults.** A `.default()` field is optional to the caller, the model included. The tool's advertised schema doesn't list it as required, and the handler always gets a value.
112
- - **Transforms and refinements.** `.transform()` results and `.refine()` checks apply before the handler runs. A failed refinement comes back as `input-validation-failed`.
113
- - **Extra keys.** A plain `z.object` accepts them and strips them. Use `z.strictObject` to reject them.
114
- - **JSON-Schema-authored tools** get each property's `default` filled in the same way.
115
- - **Unions of scalars** (`z.union([z.string(), z.number()])`, `type: ['string', 'number']` on the wire) compile and validate. A schema Kindgi's strict schema check still refuses (an open tuple, an unknown keyword) says so at `defineTool`; for a field that may hold any JSON value, `z.json()` compiles.
116
-
117
- The output side is the reverse: the advertised output schema requires every field, defaulted ones included. Return them all.
118
-
119
- ### Logging from the handler
120
-
121
- `ctx.log` is a logger bound to the call: its records carry the run's ids, the tool's id and the trace id. It has `info`, `warn`, `error`, `debug` and `trace`, each `(message, fields?)`. The pack service sets it; a context your own code builds (a test's) may not, so write `ctx.log?.`:
122
-
123
- ```ts
124
- ctx.log?.info('refund issued', { orderId, amountCents });
125
- ```
126
-
127
- Put values in `fields`, never in the message. Log ids, amounts and outcomes, never what a person typed (a refund's reason, a message, an address): the log is read by whoever operates the runtime, not only by the person the data is about. Docs: https://docs.kindgi.com/v0.2/guides/tools/write-a-tool/#log-from-a-tool
128
-
129
- ### Configuration and secrets
130
-
131
- A secret that belongs to the tenant — an API key a customer gives you — is declared, and read from `ctx.secrets`:
132
-
133
- ```ts
134
- const defined = defineTool({
135
- // …id, description, version, input, output, effects…
136
- needsSpec: { secrets: { CITATOR_KEY: { type: 'string', minLength: 20 } } },
137
- handler: async (input, ctx) => {
138
- const key = ctx.secrets?.CITATOR_KEY;
139
- // …
140
- },
141
- });
142
- ```
143
-
144
- The runtime resolves every declared secret on every call, for the call's tenant, in its env (`KINDGI_ENV`; in `kindgi dev`, `local`: the pack's `.env` and `.env.local`, then Kindgi's own `.kindgi/secrets.env`, where `kindgi secrets set` writes). It checks each value against its schema, and fails the call, naming the secret, when one is missing or doesn't match. A declared secret is required, so in a runtime call `ctx.secrets` holds it; it's optional in the type because a unit test builds its own context and passes `secrets: { CITATOR_KEY: '…' }`. **An optional secret** has a schema that names `null` (`{ type: ['string', 'null'] }`; an unconstrained `{}` stays required): one never stored, or empty, is left out of `ctx.secrets`, and the call goes on, its log line naming it. One revoked, or gone at its provider though mapped, still fails the call. Optional secrets need runtime 0.1.6 or later; an older runtime requires them.
145
-
146
- A value that differs per tenant, org or project but isn't secret (a base URL, a region, an account id) is an **env value**: declared in `needsSpec.env`, read from `ctx.env`:
147
-
148
- ```ts
149
- const defined = defineTool({
150
- // …id, description, version, input, output, effects…
151
- needsSpec: {
152
- env: {
153
- ORDERS_BASE_URL: { type: 'string', pattern: '^https://' },
154
- ORDERS_REGION: { type: 'string', enum: ['eu', 'us'], default: 'eu' },
155
- },
156
- },
157
- handler: async ({ orderId }, ctx) => ({
158
- url: `${ctx.env?.ORDERS_BASE_URL}/${ctx.env?.ORDERS_REGION}/orders/${orderId}`,
159
- }),
160
- });
161
- ```
162
-
163
- - **Which value a call gets:** its project's, else its org's, else the tenant's, in the runtime's env; a schema `default` makes a name optional. The values a call used are recorded with it, so a retry or a resume sees the same ones.
164
- - **Setting them:** `kindgi env set ORDERS_REGION us --scope=project:<project-id> --env=local` (or `--scope=tenant`, for every project). Changing a value that's already set takes `--force`.
165
- - **A declared value nobody set** stops the call before the tool runs. For a tool `acme-orders.needs-account` that declares `ACME_ACCOUNT_ID`, the message reads:
166
- ```text
167
- precondition-failed: Tool "acme-orders.needs-account" was not run: env-value-missing: tool "acme-orders.needs-account" needs env value "ACME_ACCOUNT_ID" in env "local", and none is set for project e889c1f5-eae7-45dc-8669-5bd029a5d85c, its org, or the tenant. Set it: kindgi env set ACME_ACCOUNT_ID <value> --scope=project:e889c1f5-eae7-45dc-8669-5bd029a5d85c --env=local (or --scope=tenant, for every project)
168
- ```
169
- - **Not secret:** env values are recorded with each run that uses them and shown in its journal. A credential is a secret (`needsSpec.secrets`), never an env value.
170
- - **Schemas compile strictly:** each `needsSpec` schema must compile as the runtime compiles it (an unknown keyword is refused), and an env `default` is a string; otherwise `defineTool` returns `invalid-tool-definition`, and a deploy fails with `deployment-validation-failed`.
171
- - **In a unit test:** call it with `invokeToolForTest(tool, input, { env: { … } })` (`@kindgi/sdk/define`). It decides the values as a run does: a name you don't set takes its schema's `default`, each value is checked, and the handler sees the declared names only. A missing or invalid one is `precondition-failed` (`env-value-missing`, `env-value-invalid`). Plain `invokeTool` passes `ctx.env` as given (the runtime decides a pack tool's env itself).
172
-
173
- Everything else comes from the process environment: `process.env.CITATOR_URL`. The pack service runs with the pack's env files in `kindgi dev` (less any secret stored with `kindgi secrets set`, which a tool declares and reads from `ctx.secrets`, and any model provider's key, which no tool gets), and with the container's environment in an image. Declare the names your code reads in `kindgi.config.ts`, `env: { required: ['CITATOR_URL'], optional: [...] }`: a deployment injects exactly those, a pack service missing a required one isn't ready and says which, and `kindgi dev` warns about it. In an image the pack service also drops every variable the pack doesn't declare before your code loads (`kindgi dev` keeps them), so an undeclared name works locally and is unset once deployed: declare every name the code reads. Values per environment go in `environments.<name>.env`, secrets only as references.
174
-
175
- ### Connections to other apps
176
-
177
- A tool that acts in another app as a person or as the company (sending mail, opening an issue) declares a **connection**, never a secret holding someone's token, and reads the call's short-lived token from `ctx.connections` (runtime 0.2.0 or later):
178
-
179
- ```ts
180
- import { ConnectionRejectedError, defineTool } from '@kindgi/sdk/define';
181
-
182
- const defined = defineTool({
183
- // …id, description, version, input, output, effects…
184
- needsSpec: {
185
- connections: { mail: { app: 'google', permissions: ['gmail.send'], as: 'person' } },
186
- },
187
- handler: async (input, ctx) => {
188
- const mail = ctx.connections?.mail;
189
- const res = await fetch(url, { headers: { authorization: `Bearer ${mail?.accessToken}` } });
190
- if (res.status === 401) throw new ConnectionRejectedError('mail');
191
- // …
192
- },
193
- });
194
- ```
195
-
196
- - **Whose connection:** `as: 'person'` is the connection of the person the run acts as (whoever started it, or the owner of the schedule or webhook that did); `as: 'company'` is the company's. The runtime picks it from the run, never from the input or the model. A run that acts as nobody (a service account's, an event's) fails a `'person'` call with `connection-needs-a-person`.
197
- - **The token:** `accessToken` is current for this call only. Never keep, print, log or return it; it never reaches the journal. `account.label` says whose account it is (`ana@acme.com`).
198
- - **A 401 from the app:** throw `new ConnectionRejectedError('mail')`, with the connection's name (a second argument can say what the app answered, never the token), or your own error with it as its `cause`. The runtime renews the token once and runs the call again; refused again, the call fails with `connection-rejected`.
199
- - **An HTTP tool** sends the token with no code: `authorization: { kind: 'connection', connection: 'mail' }`.
200
- - **Ready-made tools:** for Gmail, Outlook, GitHub and LinkedIn, include one from `@kindgi/connections` instead of writing it. Each is a factory, and the pack names its id; there's no default, because a tool id is the tenant's, filed under the first project that publishes it. A tool folder's `index.ts` is one line, `export default gmailSend({ id: 'acme.gmail.send' })` (`@kindgi/connections/google/tools`, with `gmailMessagesList`, `gmailMessageGet` and `gmailReply`); `githubIssuesList`, `githubLabelsList` and `githubIssuesAddLabels` come from `@kindgi/connections/github/tools`, `outlookSend`, `outlookMessagesList`, `outlookMessageGet` and `outlookReply` from `@kindgi/connections/microsoft/tools`, and `linkedinPost` from `@kindgi/connections/linkedin/tools`. Agents and approval settings use the pack's id. A mail tool acts in the person's own mailbox; `as: 'company'` makes it use the company's. `as` is part of what the tool declares, so an id you've published keeps its `as`: give a company mailbox its own tool id.
201
- - **Your own calls to an app's API:** `appFetch(ctx, { entry, connection, toolId, url, method, headers, body })` from `@kindgi/connections/app-fetch` sends the connection's token as the only `Authorization`, paces calls by the entry's `rateLimits` (`toolId` is what its `costs` charge; 1 when not listed), tries a busy answer again, and throws a 401 as `ConnectionRejectedError`.
202
- - **Unattended runs:** in a run no person started, a tool that uses an `as: 'person'` connection waits for that person's yes, reads included: `mutating: false` doesn't lift it. The agent's `conversationPolicy.hitl` can't lift it; only a trigger's `toolsAllowedUnattended` can, which a project admin sets, as for `needsApprovalWhenUnattended` (below). In a run a new mail started, the mail is a stranger's text, so two more calls wait: a call of a tool that isn't `mutating: false`, and, when the trigger reads the company's mailbox, a read with the company's connection to that app. The mail trigger's `toolsAllowedUnattended` lifts these too.
203
- - **In a unit test:** `ctx.connections` is optional in the type; build a context with `toolContextForTest({ connections: { mail: { accessToken: 'test', account: { id: '1', label: 'ana@acme.com' }, … } } })` (`@kindgi/sdk/define`).
204
-
205
- Docs: https://docs.kindgi.com/v0.2/guides/connections/use-a-connection/
206
-
207
- ## Declarative HTTP spec
208
-
209
- ```ts
210
- // tools/fetch-order/index.ts
211
- import { defineTool } from '@kindgi/sdk/define';
212
- import type { ToolId } from '@kindgi/sdk/types';
213
- import { z } from 'zod';
214
-
215
- const defined = defineTool({
216
- id: 'shop.fetch-order' as ToolId,
217
- description: 'Fetch an order by id from the storefront API.',
218
- version: '0.1.0',
219
- input: z.object({ orderId: z.string() }),
220
- output: z.object({ id: z.string(), status: z.string(), total: z.number() }),
221
- effects: [],
222
- mutating: false,
223
- spec: {
224
- kind: 'http',
225
- method: 'GET',
226
- urlTemplate: 'https://api.shop.example/orders/{orderId}',
227
- // Every {placeholder} MUST be a key on the input schema
228
- authorization: {
229
- kind: 'bearer',
230
- secretRef: { envName: 'production', name: 'shop-api-token' },
231
- },
232
- successStatus: { min: 200, max: 299 },
233
- },
234
- });
235
-
236
- if (defined.kind === 'err') throw new Error(defined.error.message);
237
- export default defined.value;
238
- ```
239
-
240
- `timeoutMs` (default 30 seconds) covers the whole call, reading the response included, and so does a run's cancel. A response body over 10 MiB, counted after it's decompressed, fails the call. So point a tool at an endpoint that pages or filters, not one that returns everything.
241
-
242
- ## Read-only tools: `mutating`
243
-
244
- `mutating: false` declares that the tool changes nothing outside Kindgi; it only reads. Leaving `mutating` out counts as mutating, the same as `true`. It decides three things, in both authoring modes:
245
-
246
- - **Dry runs.** `kindgi runs start --dry-run` runs a tool only if it's `mutating: false` and its `effects` declare no `writes`, `deletes`, `spawns-run`, `emits-event` or `external-side-effect`. The first other tool stops the run with `dry-run-effectful-tool`.
247
- - **Approval gates.** When an agent turns tool gates on (`conversationPolicy.hitl.tools`) and sets neither an override for the tool nor a `default`, a `mutating: false` tool runs straight through, and any other tool asks for approval on its first use.
248
- - **Runs a new mail started.** A call of a tool that isn't `mutating: false` waits for a person's yes, unless the mail trigger's `toolsAllowedUnattended` lists it.
249
-
250
- So declare `mutating: false` on every tool that only reads, and never on one that writes.
251
-
252
- A tool that writes also says what it writes, in `effects`, beside `mutating: true`:
253
-
254
- ```ts
255
- effects: [{ kind: 'writes', resource: 'acme:refunds' }],
256
- mutating: true,
257
- ```
258
-
259
- The kinds are `reads`, `writes`, `deletes`, `network`, `spawns-run`, `emits-event`, `external-side-effect` and `sensitive-data-egress` (`EFFECT_KINDS` in `@kindgi/tools`); `defineTool` refuses any other. `resource` is free text naming what the tool touches. A read-only tool keeps `effects: []`.
260
-
261
- ## A person's yes when no one is watching: `needsApprovalWhenUnattended`
262
-
263
- `needsApprovalWhenUnattended: true` declares that a person must say yes to every call of the tool in a run no person started: one a schedule, a webhook, a new mail, an event or a service account's key started. In such a run:
264
-
265
- - **An agent's call waits** for a person's yes, whatever the agent's `conversationPolicy.hitl.tools` says. The agent's rules can't lift it: agents are reused, and the tool's author knows what it does.
266
- - **A flow's tool step that calls it is refused.** Put the tool behind an agent step instead, whose call waits.
267
- - **A trigger can lift both, for its own runs only.** A project admin names the tool in a webhook trigger's, a schedule's or a mail trigger's `toolsAllowedUnattended` (`--allow-unattended=<tool-id>` on `kindgi webhooks`, `kindgi schedules` and `kindgi mail-triggers`). In that trigger's runs, the tool is then as in a run a person started. The tenant's approval rules still apply.
268
-
269
- In a run a person started (signed in, or with their own API key), the agent's rules decide, as for any tool. Absent means `false`. Declare it on a tool that moves money or changes something people rely on:
270
-
271
- ```ts
272
- effects: [{ kind: 'writes', resource: 'acme:refunds' }],
273
- mutating: true,
274
- needsApprovalWhenUnattended: true,
275
- ```
276
-
277
- Docs: https://docs.kindgi.com/v0.2/guides/approvals/ask-before-a-tool-runs/#when-no-person-started-the-run
278
-
279
- A step can run more than once (resumed after an approval, retried after a failure, run again after a crash), so a tool that writes uses `ctx.idempotencyKey`: the same every time this call runs, different for every other call. Pass it to the system you write to (an `Idempotency-Key` header, a client reference, a unique column) or look for it there first, and a refund never goes out twice. Not `ctx.requestId`: a model's call id is only unique within one of its answers. The key is absent outside a run and from a runtime before 0.1.6. Docs: https://docs.kindgi.com/v0.2/guides/tools/write-a-tool/#make-a-side-effect-happen-once
280
-
281
- ## Results that are text from outside: `resultIsOutsideData`
282
-
283
- `resultIsOutsideData: true` says the tool's results are text from outside that nobody checked, such as a mail someone sent or a page a search found. An agent reads each result of the tool as data, not instructions, in that turn and in every later turn's history. It changes what the model is told, not what the tool may do: pair it with approvals for anything an agent could do with what it read. Absent means `false`.
65
+ | Your case | Read |
66
+ |---|---|
67
+ | A `handler`: what it gets (`input`, `ctx`), and logging | [imperative-handler.md](imperative-handler.md) |
68
+ | An env value, a tenant's secret, or acting in another app (a connection) | [secrets-and-connections.md](secrets-and-connections.md) |
69
+ | A plain remote call: `spec: {kind: 'http'}` | [declarative-http.md](declarative-http.md) |
70
+ | A read-only tool, a person's yes in unattended runs, results that are outside text | [tool-flags.md](tool-flags.md) |
71
+ | Changing a tool, its `version`, its tests, code several tools share | [iterating-and-testing.md](iterating-and-testing.md) |
72
+ | An error, or a tool that won't index or load | [common-mistakes.md](common-mistakes.md) |
284
73
 
285
74
  ## Tool id convention
286
75
 
@@ -289,50 +78,6 @@ prefix scopes the tool to its pack; `<tool-name>` names the operation.
289
78
  Enforced by `defineTool` at author time. Examples:
290
79
  `acme.verify-citation`, `shop.fetch-order`, `demo.echo`.
291
80
 
292
- ## Iterating on a tool
293
-
294
- Edit the source file (`tools/<tool>/index.ts`), save. The next
295
- `kindgi runs start` sees the change — new input/output schema, new
296
- description, new handler behavior. No version bump, no restart, no
297
- re-registration ceremony. Source is truth in dev.
298
-
299
- The `version` field is a **semver contract for humans** — it declares
300
- what callers can rely on. Bump it because the *contract with
301
- downstream callers* changed (removed a field, tightened a type,
302
- narrowed enum values), not because you saved the file. If you're
303
- iterating in dev and the shape isn't finalized, leave `version`
304
- alone.
305
-
306
- **When version matters:** `kindgi deploy` publishes to a durable
307
- production registry that enforces the immutable `(id, version)`
308
- contract — a re-publish of the same version with different bytes
309
- gets rejected. That's when semver discipline kicks in. The deploy
310
- tooling surfaces the check; you don't have to think about it while
311
- authoring.
312
-
313
- **Downstream callers still pin ranges.** An agent's `tools: [{id,
314
- version: '^0.1.0'}]` picks the highest active version matching the
315
- range at run start. Compatible tool updates (patch, minor) reach the
316
- agent without editing agent source; breaking updates (major) require
317
- the agent-author to opt in.
318
-
319
- ## Testing a tool
320
-
321
- Put a tool's tests beside it, `tools/<tool>/index.test.ts`. Discovery skips `*.test.*` and `*.spec.*` files (`.ts`, `.js`, `.mjs`, `.cjs`), so the indexer never loads a test as a primitive: don't move tests elsewhere to keep them out. `invokeTool(tool, input, ctx)` from `@kindgi/sdk/define` calls the tool the way Kindgi does, schemas included, and returns a `Result`. `toolContextForTest()` builds its `ctx` (tenant `tenant-test`, run `run-test`, an abort signal) and takes what to change: `env`, `secrets`, `projectId`.
322
-
323
- ```ts
324
- import { invokeTool, toolContextForTest } from '@kindgi/sdk/define';
325
-
326
- const result = await invokeTool(lookupOrder, { orderId: 'ord_1001' }, toolContextForTest());
327
- // a tool with env values: invokeToolForTest(tool, input, { env: { STORE_URL: '…' } })
328
- ```
329
-
330
- `kindgi test` runs the pack's tests with vitest (`vitest run`; `--watch` keeps watching). In an app Kindgi was added to, `kindgi init` wrote the vitest config (`vitest.config.mts`, or `.ts` in a `"type": "module"` app): it runs `kindgi/**/*.test.ts`, and its `resolve.alias` copies `tsconfig.json`'s `paths`, which vitest doesn't read. A test that can't resolve an alias the app's code uses (`@/…`) means the two differ: update the config's alias, not the import.
331
-
332
- ## Shared code
333
-
334
- Code several tools share (schemas, a client, helpers) goes outside `tools/`, for example in `lib/` beside it. Discovery loads every `.ts`, `.js` and `.mjs` file under `tools/` (tests aside) as a primitive, so a helper there fails the index. Don't put it in the app's own source either: import the app's functions from where they are (unless their module imports `server-only`: see mistake 12; for a CommonJS package's default import, see mistake 13), and keep what's Kindgi's in the pack's folder.
335
-
336
81
  ## Wiring the tool onto an agent
337
82
 
338
83
  Agents reference tools via `ToolRef[]`, NOT `string[]`. Each entry is
@@ -349,132 +94,12 @@ tools: [
349
94
  The resolver picks the highest active version matching the range at
350
95
  run start via `semver.maxSatisfying`. No implicit `:latest`.
351
96
 
352
- ## Common mistakes
353
-
354
- 1. **Copying another tool's shape without user intent.** The pack may
355
- ship `tools/echo/` as a starter example. Copying its skeleton to
356
- make `tools/lookup/` produces plausible-looking code that solves
357
- the wrong problem. Ask the user what the new tool should DO first.
358
-
359
- 2. **Passing bare strings to `agent.tools`.** `tools: ['acme.verify-citation']`
360
- doesn't type-check (`tools` is `ToolRef[]`), and `defineAgent`
361
- returns `invalid-agent` for a bare string that slips through. Use
362
- `[{id: 'acme.verify-citation', version: '^0.1.0'}]`.
363
-
364
- 3. **Forgetting the `Result` unwrap.** `defineTool` returns
365
- `Result<DefinedTool, ToolError>` — a bad schema doesn't throw at import
366
- time unless you check `defined.kind === 'err'`. Always unwrap at
367
- module load so a broken pack fails LOUDLY, not on first invocation.
368
-
369
- 4. **`handler` and `spec` together.** Mutually exclusive.
370
- `defineTool` returns `invalid-tool-definition` with both set.
371
-
372
- 5. **`{placeholder}` in `urlTemplate` without a matching input key.**
373
- HTTP tools substitute placeholders from the input at invoke time.
374
- Missing keys throw a clear error at invoke time — check both sides
375
- line up.
376
-
377
- 6. **An HTTP tool's `secretRef` in the wrong env.** The runtime
378
- resolves `authorization.secretRef` itself, on every call, in the env
379
- the ref names: `{ envName: 'local', … }` is the pack's `.env` /
380
- `.env.local` under `kindgi dev`, while `'production'` reads
381
- `.env.production`. A secret it can't find fails the call with
382
- `secret-unavailable`, naming it. Only a harness of your own that
383
- calls `invokeTool` directly has to pass `ctx.resolveSecret`.
384
- 7. **Reading a secret from `process.env` in a tool that runs for many
385
- tenants.** The pack service's environment is one for all of them.
386
- Declare the secret in `needsSpec.secrets` and read `ctx.secrets`.
387
-
388
- 8. **Bumping `version` on every dev save.** Old habit from
389
- frameworks that stored manifests immutably by `(id, version)`.
390
- Kindgi's dev mode reads tool manifests directly from source —
391
- version doesn't gate iteration. Bump it when the *contract*
392
- changes (breaking schema shape, semantic behavior), not when you
393
- save. See the "Iterating on a tool" section above.
394
-
395
- 9. **A package a tool imports, listed only in `devDependencies`.** The
396
- deployed pack installs the app's production dependencies only, so the
397
- import works under `kindgi dev` and fails in the image. When a tool
398
- imports a new package (an ORM client such as `@prisma/client`, an API
399
- SDK), check that the app's `package.json` lists it under
400
- `dependencies`. Build-time tools (the `prisma` CLI, `typescript`) stay
401
- in `devDependencies`. `kindgi dev` warns as soon as a tool imports one
402
- ("⚠ The pack imports @prisma/client (in kindgi/tools/…), which
403
- package.json lists only in devDependencies: …"), and `kindgi build`
404
- refuses the pack until it moves.
405
- 10. **A tool that needs the app's install scripts in the image.** The
406
- image installs with scripts off, so the app's `postinstall` /
407
- `prepare` (`prisma generate`, husky) don't run there; `kindgi build`
408
- lists them ("✓ The app's own install scripts don't run in the image:
409
- …"). A tool that uses Prisma's client then fails the build ("@prisma/client
410
- did not initialize yet"). Add `prisma({ schema: 'prisma/schema.prisma' })`
411
- (from `@kindgi/sdk/build`; add `config: 'prisma.config.ts'` when the app
412
- has one) to `image.extensions` in `kindgi.config.ts`. Other generate
413
- steps: `defineBuildExtension({ name, contextFiles, postInstall: [{ bin, args }] })`.
414
- Debian packages: `image.systemPackages`. Placeholder env for those steps:
415
- `image.buildEnv` (never secrets).
416
-
417
- 11. **Turning the dev sandbox off for a file the tool needs.** Under `kindgi dev`
418
- the tool's code runs sandboxed: it can't read outside the app (the home
419
- folder, `~/.aws`) or any `.env*` file, and gets `EPERM` there. A tool that
420
- needs a path names it in `kindgi.config.ts`: `dev: { sandbox: { allowRead: ['~/.aws'] } }`.
421
- Never set `KINDGI_DEV_SANDBOX=off` or `dev.sandbox: false` to get past it
422
- (https://docs.kindgi.com/v0.2/guides/secrets/dev-sandbox/).
423
-
424
- 12. **Importing a Next.js module that has `import 'server-only'`.** Outside
425
- Next.js that line stops the module from loading, so the tool fails
426
- `kindgi test`, `kindgi dev` and `kindgi build`. When the app doesn't
427
- install the package (Next.js doesn't need it), `kindgi test` fails with
428
- `Cannot find package 'server-only'`, and `kindgi dev` and `kindgi build`
429
- with `Could not resolve "server-only"`. When it does, all three fail
430
- with the package's own "This module cannot be imported from a Client
431
- Component module". Don't remove the line, and don't alias `server-only`
432
- away in the vitest config: the build still fails. Move the code the tool
433
- needs, as it is, to a module without the line (`lib/orders-core.ts`).
434
- The guarded module keeps the line and re-exports it
435
- (`import 'server-only'; export * from './orders-core';`), so the app's
436
- own imports don't change, and the tool imports the new module. Nothing
437
- guards it, so only the app's server code and the pack should import it.
438
-
439
- 13. **A default import of a package that ships only CommonJS.** A pack's
440
- code runs as ES modules: `kindgi build` bundles it into `.mjs` files,
441
- leaving out the packages from `node_modules`, and `kindgi dev` and the
442
- deployed pack load it with Node's own `import()`. Node gives
443
- `import OrdersClient from '@acme/orders-client'` the package's whole
444
- `module.exports`; when the package puts its class on `exports.default`
445
- (TypeScript's CommonJS output for `export default`), the class is on its
446
- `default`. Vitest, Next.js, and tsx in an app without `"type": "module"`
447
- give the class itself. So the tool passes `kindgi test`, and app code it
448
- imports works in the app, but under `kindgi dev` and once deployed it
449
- fails with `TypeError: OrdersClient is not a constructor`. With
450
- `"moduleResolution": "NodeNext"` (a new pack's `tsconfig.json`),
451
- TypeScript already refuses `new OrdersClient()`; with `"bundler"`
452
- (Next.js apps) it compiles. This is Node's own behavior, and Kindgi keeps
453
- it: take the class either way.
454
-
455
- ```ts
456
- import OrdersClientExport from '@acme/orders-client';
457
-
458
- type DefaultOf<T> = T extends { default: infer C } ? C : T;
459
-
460
- function defaultOf<T>(loaded: T): DefaultOf<T> {
461
- const value = typeof loaded === 'function' ? loaded : (loaded as { default: unknown }).default;
462
- return value as DefaultOf<T>;
463
- }
464
-
465
- const OrdersClient = defaultOf(OrdersClientExport);
466
- ```
467
-
468
- A dynamic import is the same: `(await import('@acme/orders-client')).default`
469
- is the class under Vitest and the whole `module.exports` under Node, so
470
- take it with `defaultOf((await import('@acme/orders-client')).default)`.
471
-
472
97
  ## References
473
98
 
474
99
  - Type surface: `hover any @kindgi/sdk/define export` in your editor
475
100
  for full JSDoc — every field on `DefineToolSpec` / `ToolManifest`
476
101
  documents purpose, when to set it, and gotchas.
477
- - API reference: https://docs.kindgi.com/v0.2/reference/typescript/sdk/kindgi/sdk/define/ (every `define*` spec, field by field).
102
+ - API reference: https://docs.kindgi.com/v0.3/reference/typescript/sdk/kindgi/sdk/define/ (every `define*` spec, field by field).
478
103
  - Common patterns: check the `sample` template (`kindgi init
479
104
  --template=sample`) for working examples of both authoring modes.
480
105
 
@@ -0,0 +1,135 @@
1
+ # Common mistakes
2
+
3
+ ## Contents
4
+
5
+ - 1. Copying another tool's shape without user intent.
6
+ - 2. Passing bare strings to `agent.tools`.
7
+ - 3. Forgetting the `Result` unwrap.
8
+ - 4. `handler` and `spec` together.
9
+ - 5. `{placeholder}` in `urlTemplate` without a matching input key.
10
+ - 6. An HTTP tool's `secretRef` in the wrong env.
11
+ - 7. Reading a secret from `process.env` in a tool that runs for many tenants.
12
+ - 8. Bumping `version` on every dev save.
13
+ - 9. A package a tool imports, listed only in `devDependencies`.
14
+ - 10. A tool that needs the app's install scripts in the image.
15
+ - 11. Turning the dev sandbox off for a file the tool needs.
16
+ - 12. Importing a Next.js module that has `import 'server-only'`.
17
+ - 13. A default import of a package that ships only CommonJS.
18
+
19
+ 1. **Copying another tool's shape without user intent.** The pack may
20
+ ship `tools/echo/` as a starter example. Copying its skeleton to
21
+ make `tools/lookup/` produces plausible-looking code that solves
22
+ the wrong problem. Ask the user what the new tool should DO first.
23
+
24
+ 2. **Passing bare strings to `agent.tools`.** `tools: ['acme.verify-citation']`
25
+ doesn't type-check (`tools` is `ToolRef[]`), and `defineAgent`
26
+ returns `invalid-agent` for a bare string that slips through. Use
27
+ `[{id: 'acme.verify-citation', version: '^0.1.0'}]`.
28
+
29
+ 3. **Forgetting the `Result` unwrap.** `defineTool` returns
30
+ `Result<DefinedTool, ToolError>` — a bad schema doesn't throw at import
31
+ time unless you check `defined.kind === 'err'`. Always unwrap at
32
+ module load so a broken pack fails LOUDLY, not on first invocation.
33
+
34
+ 4. **`handler` and `spec` together.** Mutually exclusive.
35
+ `defineTool` returns `invalid-tool-definition` with both set.
36
+
37
+ 5. **`{placeholder}` in `urlTemplate` without a matching input key.**
38
+ HTTP tools substitute placeholders from the input at invoke time.
39
+ Missing keys throw a clear error at invoke time — check both sides
40
+ line up.
41
+
42
+ 6. **An HTTP tool's `secretRef` in the wrong env.** The runtime
43
+ resolves `authorization.secretRef` itself, on every call, in the env
44
+ the ref names: `{ envName: 'local', … }` is the pack's `.env` /
45
+ `.env.local` under `kindgi dev`, while `'production'` reads
46
+ `.env.production`. A secret it can't find fails the call with
47
+ `secret-unavailable`, naming it. Only a harness of your own that
48
+ calls `invokeTool` directly has to pass `ctx.resolveSecret`.
49
+ 7. **Reading a secret from `process.env` in a tool that runs for many
50
+ tenants.** The pack service's environment is one for all of them.
51
+ Declare the secret in `needsSpec.secrets` and read `ctx.secrets`.
52
+
53
+ 8. **Bumping `version` on every dev save.** Old habit from
54
+ frameworks that stored manifests immutably by `(id, version)`.
55
+ Kindgi's dev mode reads tool manifests directly from source —
56
+ version doesn't gate iteration. Bump it when the *contract*
57
+ changes (breaking schema shape, semantic behavior), not when you
58
+ save. See `iterating-and-testing.md`.
59
+
60
+ 9. **A package a tool imports, listed only in `devDependencies`.** The
61
+ deployed pack installs the app's production dependencies only, so the
62
+ import works under `kindgi dev` and fails in the image. When a tool
63
+ imports a new package (an ORM client such as `@prisma/client`, an API
64
+ SDK), check that the app's `package.json` lists it under
65
+ `dependencies`. Build-time tools (the `prisma` CLI, `typescript`) stay
66
+ in `devDependencies`. `kindgi dev` warns as soon as a tool imports one
67
+ ("⚠ The pack imports @prisma/client (in kindgi/tools/…), which
68
+ package.json lists only in devDependencies: …"), and `kindgi build`
69
+ refuses the pack until it moves.
70
+ 10. **A tool that needs the app's install scripts in the image.** The
71
+ image installs with scripts off, so the app's `postinstall` /
72
+ `prepare` (`prisma generate`, husky) don't run there; `kindgi build`
73
+ lists them ("✓ The app's own install scripts don't run in the image:
74
+ …"). A tool that uses Prisma's client then fails the build ("@prisma/client
75
+ did not initialize yet"). Add `prisma({ schema: 'prisma/schema.prisma' })`
76
+ (from `@kindgi/sdk/build`; add `config: 'prisma.config.ts'` when the app
77
+ has one) to `image.extensions` in `kindgi.config.ts`. Other generate
78
+ steps: `defineBuildExtension({ name, contextFiles, postInstall: [{ bin, args }] })`.
79
+ Debian packages: `image.systemPackages`. Placeholder env for those steps:
80
+ `image.buildEnv` (never secrets).
81
+
82
+ 11. **Turning the dev sandbox off for a file the tool needs.** Under `kindgi dev`
83
+ the tool's code runs sandboxed: it can't read outside the app (the home
84
+ folder, `~/.aws`) or any `.env*` file, and gets `EPERM` there. A tool that
85
+ needs a path names it in `kindgi.config.ts`: `dev: { sandbox: { allowRead: ['~/.aws'] } }`.
86
+ Never set `KINDGI_DEV_SANDBOX=off` or `dev.sandbox: false` to get past it
87
+ (https://docs.kindgi.com/v0.3/guides/secrets/dev-sandbox/).
88
+
89
+ 12. **Importing a Next.js module that has `import 'server-only'`.** Outside
90
+ Next.js that line stops the module from loading, so the tool fails
91
+ `kindgi test`, `kindgi dev` and `kindgi build`. When the app doesn't
92
+ install the package (Next.js doesn't need it), `kindgi test` fails with
93
+ `Cannot find package 'server-only'`, and `kindgi dev` and `kindgi build`
94
+ with `Could not resolve "server-only"`. When it does, all three fail
95
+ with the package's own "This module cannot be imported from a Client
96
+ Component module". Don't remove the line, and don't alias `server-only`
97
+ away in the vitest config: the build still fails. Move the code the tool
98
+ needs, as it is, to a module without the line (`lib/orders-core.ts`).
99
+ The guarded module keeps the line and re-exports it
100
+ (`import 'server-only'; export * from './orders-core';`), so the app's
101
+ own imports don't change, and the tool imports the new module. Nothing
102
+ guards it, so only the app's server code and the pack should import it.
103
+
104
+ 13. **A default import of a package that ships only CommonJS.** A pack's
105
+ code runs as ES modules: `kindgi build` bundles it into `.mjs` files,
106
+ leaving out the packages from `node_modules`, and `kindgi dev` and the
107
+ deployed pack load it with Node's own `import()`. Node gives
108
+ `import OrdersClient from '@acme/orders-client'` the package's whole
109
+ `module.exports`; when the package puts its class on `exports.default`
110
+ (TypeScript's CommonJS output for `export default`), the class is on its
111
+ `default`. Vitest, Next.js, and tsx in an app without `"type": "module"`
112
+ give the class itself. So the tool passes `kindgi test`, and app code it
113
+ imports works in the app, but under `kindgi dev` and once deployed it
114
+ fails with `TypeError: OrdersClient is not a constructor`. With
115
+ `"moduleResolution": "NodeNext"` (a new pack's `tsconfig.json`),
116
+ TypeScript already refuses `new OrdersClient()`; with `"bundler"`
117
+ (Next.js apps) it compiles. This is Node's own behavior, and Kindgi keeps
118
+ it: take the class either way.
119
+
120
+ ```ts
121
+ import OrdersClientExport from '@acme/orders-client';
122
+
123
+ type DefaultOf<T> = T extends { default: infer C } ? C : T;
124
+
125
+ function defaultOf<T>(loaded: T): DefaultOf<T> {
126
+ const value = typeof loaded === 'function' ? loaded : (loaded as { default: unknown }).default;
127
+ return value as DefaultOf<T>;
128
+ }
129
+
130
+ const OrdersClient = defaultOf(OrdersClientExport);
131
+ ```
132
+
133
+ A dynamic import is the same: `(await import('@acme/orders-client')).default`
134
+ is the class under Vitest and the whole `module.exports` under Node, so
135
+ take it with `defaultOf((await import('@acme/orders-client')).default)`.
@@ -0,0 +1,34 @@
1
+ # Declarative HTTP spec
2
+
3
+ ```ts
4
+ // tools/fetch-order/index.ts
5
+ import { defineTool } from '@kindgi/sdk/define';
6
+ import type { ToolId } from '@kindgi/sdk/types';
7
+ import { z } from 'zod';
8
+
9
+ const defined = defineTool({
10
+ id: 'shop.fetch-order' as ToolId,
11
+ description: 'Fetch an order by id from the storefront API.',
12
+ version: '0.1.0',
13
+ input: z.object({ orderId: z.string() }),
14
+ output: z.object({ id: z.string(), status: z.string(), total: z.number() }),
15
+ effects: [],
16
+ mutating: false,
17
+ spec: {
18
+ kind: 'http',
19
+ method: 'GET',
20
+ urlTemplate: 'https://api.shop.example/orders/{orderId}',
21
+ // Every {placeholder} MUST be a key on the input schema
22
+ authorization: {
23
+ kind: 'bearer',
24
+ secretRef: { envName: 'production', name: 'shop-api-token' },
25
+ },
26
+ successStatus: { min: 200, max: 299 },
27
+ },
28
+ });
29
+
30
+ if (defined.kind === 'err') throw new Error(defined.error.message);
31
+ export default defined.value;
32
+ ```
33
+
34
+ `timeoutMs` (default 30 seconds) covers the whole call, reading the response included, and so does a run's cancel. A response body over 10 MiB, counted after it's decompressed, fails the call. So point a tool at an endpoint that pages or filters, not one that returns everything.