@ekanos/cli 0.3.0 → 0.3.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.
- package/package.json +2 -3
- package/templates/AGENTS.md.tmpl +97 -9
- package/templates/package.json.tmpl +2 -2
- package/templates/skills/build-integration/SKILL.md +7 -3
- package/templates/skills/build-integration/references/api-wiring.md +14 -1
- package/templates/skills/build-integration/references/widget-design.md +12 -2
- package/templates/src/integration.ts.tmpl +16 -0
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/cli",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"private": false,
|
|
3
|
+
"version": "0.3.1",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"description": "The Ekanos partner toolchain CLI: scaffold, validate, and test a Fusion integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
|
|
7
6
|
"license": "MIT",
|
|
@@ -34,7 +33,7 @@
|
|
|
34
33
|
},
|
|
35
34
|
"dependencies": {
|
|
36
35
|
"@clack/prompts": "1.7.0",
|
|
37
|
-
"@ekanos/integration-schema": "0.
|
|
36
|
+
"@ekanos/integration-schema": "0.3.0",
|
|
38
37
|
"esbuild": "0.28.1",
|
|
39
38
|
"picocolors": "1.1.1"
|
|
40
39
|
},
|
package/templates/AGENTS.md.tmpl
CHANGED
|
@@ -27,6 +27,7 @@ export const integration = defineIntegration<MyStorage>({
|
|
|
27
27
|
permissions: [ /* disclosure of what you read/write */ ],
|
|
28
28
|
components: { widgets: [ /* … */ ] },
|
|
29
29
|
tools: [ /* assistant-callable functions */ ],
|
|
30
|
+
actions: { /* deterministic, widget-invoked server handlers */ },
|
|
30
31
|
webhooks: [ /* inbound deliveries */ ],
|
|
31
32
|
schedules: [ /* cron runs */ ],
|
|
32
33
|
onActivate: async (ctx) => { /* seed caches, sanity-check credentials */ },
|
|
@@ -172,6 +173,76 @@ failure). Schedule results: `{ status: 'completed' }` / `{ status: 'skipped' }`
|
|
|
172
173
|
(`invocation.trigger` distinguishes them). Cron is 5-field numeric only — no
|
|
173
174
|
names, no `@daily`, no seconds.
|
|
174
175
|
|
|
176
|
+
### Actions
|
|
177
|
+
|
|
178
|
+
`actions` is a record — keyed by kebab-case action name (`'create-record'`,
|
|
179
|
+
not `tools`' `snake_case`) — of deterministic, widget-invoked server
|
|
180
|
+
handlers. It is a **parallel surface to `tools`, not a flag on it**: a
|
|
181
|
+
widget's Save button already has fully-typed, user-entered values, so it
|
|
182
|
+
calls your handler directly instead of routing a natural-language prompt
|
|
183
|
+
through the assistant and hoping the model picks the right tool with the
|
|
184
|
+
right arguments.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
actions: {
|
|
188
|
+
'create-record': {
|
|
189
|
+
description: 'Create a record in the base.',
|
|
190
|
+
input: CreateRecordInput, // a real zod schema
|
|
191
|
+
output: CreateRecordOutput, // optional — validates what handler resolves to
|
|
192
|
+
handler: async (ctx, input) => {
|
|
193
|
+
// Same ctx every tool/webhook/schedule handler gets: ctx.fetch,
|
|
194
|
+
// ctx.secrets, ctx.storage — runs server-side, never in the browser.
|
|
195
|
+
return createRecordViaApi(ctx, input);
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`input` (and the optional `output`) are real `z.ZodTypeAny` instances, not
|
|
202
|
+
the JSON-schema-shaped `parameters` a tool declares for LLM argument
|
|
203
|
+
synthesis — an action is only ever parsed in-process, never serialized to a
|
|
204
|
+
model. Export the inferred types alongside the declaration
|
|
205
|
+
(`export type CreateRecordInput = z.infer<typeof CreateRecordInput>`) so the
|
|
206
|
+
widget importing them gets real autocomplete, per the SDK typing note below.
|
|
207
|
+
|
|
208
|
+
**Invoke it from a widget with `useIntegrationAction()`** (`@ekanos/sdk/hooks`)
|
|
209
|
+
— never `useAskAssistant()` for this. `useAskAssistant()` sends a
|
|
210
|
+
conversational question to the assistant dock (`Widget.AskFooter`'s "ask
|
|
211
|
+
anything about this widget"); it is not a write path and does not call your
|
|
212
|
+
handler deterministically. A form's Save button calls the handler you
|
|
213
|
+
declared, with the exact values the user typed:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
|
|
217
|
+
'__SLUG__',
|
|
218
|
+
'create-record',
|
|
219
|
+
{ accountId },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
<Button onClick={() => createRecord.mutate(formValues)} disabled={createRecord.isPending}>
|
|
223
|
+
Save
|
|
224
|
+
</Button>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
A rejected mutation's `error` is always an `IntegrationActionError`
|
|
228
|
+
(`status`, `code`, and — for a 400 — your own declared zod `issues`), never a
|
|
229
|
+
raw fetch exception. The route answers a fixed set of `code`s your widget can
|
|
230
|
+
branch on without string-matching: `action_not_found`, `invalid_input`,
|
|
231
|
+
`rate_limited`, `handler_error` (plus the client-assigned `network_error` /
|
|
232
|
+
`invalid_response` for a request that never reached a valid envelope).
|
|
233
|
+
|
|
234
|
+
A handler that wants its OWN message shown to the end user verbatim (e.g.
|
|
235
|
+
"That record already exists") throws `PublicActionError`
|
|
236
|
+
(`@ekanos/sdk/integration`) — the one escape hatch from the route's default
|
|
237
|
+
opaque `handler_error` message. Every other throw collapses to a generic
|
|
238
|
+
"Action failed." on the wire, though the real error is always logged
|
|
239
|
+
server-side.
|
|
240
|
+
|
|
241
|
+
Reuse one handler as both an action and a tool without double-declaring
|
|
242
|
+
logic — export the handler once and reference it from both declarations
|
|
243
|
+
(`run: (ctx, args) => createRecord.handler(ctx, CreateRecordInput.parse(args))`)
|
|
244
|
+
if you also want the assistant able to call it.
|
|
245
|
+
|
|
175
246
|
### `onActivate`
|
|
176
247
|
|
|
177
248
|
Runs once after an activation is first persisted, and again after
|
|
@@ -222,6 +293,12 @@ harness and in production. A widget reads declared `clientReadable`
|
|
|
222
293
|
account-scope storage back with `fetchIntegrationStorage()` from
|
|
223
294
|
`@ekanos/sdk/hooks`.
|
|
224
295
|
|
|
296
|
+
For a widget that WRITES — a form's Save button, not a read — reach for
|
|
297
|
+
`useIntegrationAction()` (see "Actions" above), not `useAskAssistant()`.
|
|
298
|
+
`useAskAssistant()` sends a conversational question to the assistant dock;
|
|
299
|
+
it is not a deterministic write path and gives no guarantee the model calls
|
|
300
|
+
your handler with the values the user actually typed.
|
|
301
|
+
|
|
225
302
|
Build widget UI from `@ekanos/ui` — the published slice of the host design
|
|
226
303
|
system. Available modules (import individually, e.g. `@ekanos/ui/button`):
|
|
227
304
|
`alert`, `badge`, `button`, `card`, `dialog`, `dropdown-menu`, `form`, `icon`,
|
|
@@ -307,14 +384,18 @@ what the live context is for.
|
|
|
307
384
|
|
|
308
385
|
Two different fetches, two different guarantees:
|
|
309
386
|
|
|
310
|
-
- **`ctx.fetch` (handlers — `onActivate`/webhooks/schedules/tools).**
|
|
311
|
-
enforcement, in the harness AND in production: your `egress`
|
|
312
|
-
redirect re-vetting, IP pinning. Locally it depends on the
|
|
313
|
-
fixtures mode handlers run in the browser against the mock
|
|
314
|
-
`ctx.fetch` answers from your fixtures, never the network; in
|
|
315
|
-
they run on the dev server's live context and `ctx.fetch` reaches
|
|
316
|
-
vendor, egress-checked first — which is how the OAuth code
|
|
317
|
-
post-connect sync work locally at all.
|
|
387
|
+
- **`ctx.fetch` (handlers — `onActivate`/webhooks/schedules/tools/actions).**
|
|
388
|
+
Real enforcement, in the harness AND in production: your `egress`
|
|
389
|
+
allowlist, redirect re-vetting, IP pinning. Locally it depends on the
|
|
390
|
+
toolbar: in fixtures mode handlers run in the browser against the mock
|
|
391
|
+
context and `ctx.fetch` answers from your fixtures, never the network; in
|
|
392
|
+
live mode they run on the dev server's live context and `ctx.fetch` reaches
|
|
393
|
+
the real vendor, egress-checked first — which is how the OAuth code
|
|
394
|
+
exchange and a post-connect sync work locally at all. An `actions[name]`
|
|
395
|
+
handler is executed exactly the same way as a tool or webhook handler — a
|
|
396
|
+
widget's `useIntegrationAction()` call never reaches the vendor directly;
|
|
397
|
+
it always crosses through your server-side handler and `ctx.fetch` first,
|
|
398
|
+
in the harness and in production alike.
|
|
318
399
|
- **`useIntegrationFetch()` (widgets, browser-side).** In the harness, live
|
|
319
400
|
mode routes it through the same egress check as a convenience, enforced
|
|
320
401
|
centrally rather than something you configure per widget. **That check
|
|
@@ -328,7 +409,14 @@ Two different fetches, two different guarantees:
|
|
|
328
409
|
In short: widgets get real network in Live API mode, gated by convenience in
|
|
329
410
|
the harness and by nothing in production; handlers get fixtures in fixtures
|
|
330
411
|
mode and the real, enforced thing in live mode — locally on the dev server,
|
|
331
|
-
then on the host after publish.
|
|
412
|
+
then on the host after publish. `actions` handlers follow the handler rule:
|
|
413
|
+
`useIntegrationAction()` in the harness runs your handler against fixtures
|
|
414
|
+
by default, same as `invokeWebhook`/`invokeSchedule` — live execution is an
|
|
415
|
+
explicit, per-session opt-in from the dev toolbar (a confirmation the first
|
|
416
|
+
time a call would touch the real vendor that session, not a per-call
|
|
417
|
+
prompt), never the default. In production there is only one mode: the
|
|
418
|
+
handler always runs server-side with the real `IntegrationContext` and real
|
|
419
|
+
`ctx.fetch` egress enforcement.
|
|
332
420
|
|
|
333
421
|
## Testing
|
|
334
422
|
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"@ekanos/ui": "^0.2.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@ekanos/cli": "^0.
|
|
19
|
-
"@ekanos/harness": "^0.
|
|
18
|
+
"@ekanos/cli": "^0.3.0",
|
|
19
|
+
"@ekanos/harness": "^0.2.0",
|
|
20
20
|
"@hookform/resolvers": "^5.2.2",
|
|
21
21
|
"@tailwindcss/postcss": "^4.3.3",
|
|
22
22
|
"@tanstack/react-query": "^5.101.4",
|
|
@@ -52,8 +52,10 @@ otherwise ask in plain prose with lettered options.
|
|
|
52
52
|
3. **The outcomes** (free text): "What should a team see on their dashboard,
|
|
53
53
|
and what should the integration be able to do?" Push for 1–3 concrete
|
|
54
54
|
outcomes ("show open invoices", "alert on new disputes", "let the
|
|
55
|
-
assistant create a customer"
|
|
56
|
-
|
|
55
|
+
assistant create a customer", "let someone create a record from a
|
|
56
|
+
dashboard form"). Each SEE-outcome becomes a widget; each DO-outcome
|
|
57
|
+
becomes a tool (assistant-invoked), a webhook/schedule (event-driven), or
|
|
58
|
+
an action (a widget's own deterministic write — a form's Save button).
|
|
57
59
|
4. **Auth** (choice): API key / Bearer / OAuth 2.0 / Basic / not sure
|
|
58
60
|
(research will settle it). This decides the activation-form fields — and
|
|
59
61
|
every string field name on that form IS a secret name (see AGENTS.md).
|
|
@@ -134,7 +136,9 @@ notes, update `docs/api-notes.md` in the same commit as the code fix.
|
|
|
134
136
|
|
|
135
137
|
Order: activation form (field names = secret names, keep them in exported
|
|
136
138
|
constants) → server handlers using `ctx.fetch` / `ctx.storage` /
|
|
137
|
-
`ctx.secrets` only → tools → webhooks/schedules
|
|
139
|
+
`ctx.secrets` only → tools → webhooks/schedules → actions (deterministic
|
|
140
|
+
widget writes — a form's Save button, invoked with `useIntegrationAction()`,
|
|
141
|
+
never routed through the assistant). For an OAuth integration,
|
|
138
142
|
also read `references/oauth-setup.md`: it tells you what to put on the
|
|
139
143
|
harness entry so the harness walks the user through their provider's console,
|
|
140
144
|
and how to hand the user off to the Local OAuth panel — the user's secrets
|
|
@@ -30,6 +30,15 @@ code. This file is the order of work and the traps.
|
|
|
30
30
|
`fetchIntegrationStorage()` from `@ekanos/sdk/hooks`. Cache freshness in
|
|
31
31
|
the stored value (`fetchedAt`) and show it in the UI when staleness
|
|
32
32
|
matters.
|
|
33
|
+
6. **Widget writes** — the read path's mirror. A form's Save button declares
|
|
34
|
+
an `actions[name]` handler (zod `input`, optional `output`, `handler(ctx,
|
|
35
|
+
input)`) and calls it with `useIntegrationAction()` from
|
|
36
|
+
`@ekanos/sdk/hooks` — deterministic, with the values the user actually
|
|
37
|
+
typed, never a natural-language hand-off to the assistant. A handler that
|
|
38
|
+
wants its own message shown to the user verbatim throws
|
|
39
|
+
`PublicActionError` from `@ekanos/sdk/integration`; every other throw
|
|
40
|
+
collapses to a generic error on the wire (still logged in full
|
|
41
|
+
server-side). See "Actions" in AGENTS.md.
|
|
33
42
|
|
|
34
43
|
## Traps, each one earned
|
|
35
44
|
|
|
@@ -94,4 +103,8 @@ on its `harness.config.ts` entry — see AGENTS.md "Live mode") and the
|
|
|
94
103
|
toolbar's fixtures ⇄ live switch runs real widget queries against the real
|
|
95
104
|
API, gated only by the declared `egress`. Use fixtures for the Fusion-side
|
|
96
105
|
state widgets need first, and for webhook/schedule handlers (the Triggers
|
|
97
|
-
surface runs those against the mock context by design).
|
|
106
|
+
surface runs those against the mock context by design). `actions[name]`
|
|
107
|
+
handlers follow the same rule: `useIntegrationAction()` calls run against
|
|
108
|
+
fixtures by default, and only reach the real vendor after an explicit,
|
|
109
|
+
per-session live-mode confirmation from the toolbar — never on the first
|
|
110
|
+
call by accident.
|
|
@@ -35,6 +35,14 @@ name the question for is a widget you cannot design.
|
|
|
35
35
|
| Chart | "Which way is it going?" | The chart + current value; axes and legends muted |
|
|
36
36
|
| Action / connection | "What can I do / who's connected?" | One clear CTA or the identity + status |
|
|
37
37
|
|
|
38
|
+
A widget whose CTA WRITES something (a form, a "Create record" button) calls
|
|
39
|
+
a declared `actions[name]` handler via `useIntegrationAction()` — a
|
|
40
|
+
deterministic RPC with the values the user actually typed, not a
|
|
41
|
+
conversational hand-off. Reach for `useAskAssistant()` only for an actual
|
|
42
|
+
"ask about this" affordance (`Widget.AskFooter`); it is not a write path and
|
|
43
|
+
gives no guarantee the assistant calls anything with the right arguments.
|
|
44
|
+
See "Actions" in `AGENTS.md`.
|
|
45
|
+
|
|
38
46
|
The default failure is uniform weight: everything `text-sm`, values dressed
|
|
39
47
|
like labels. Squint at the render — if nothing reads first, there is no
|
|
40
48
|
hierarchy, and no amount of padding will save it.
|
|
@@ -74,8 +82,10 @@ Every widget: `'use client'`, `Widget.DataState` wrapping `Widget.Card` →
|
|
|
74
82
|
fetching on `ctx?.state.state === 'active'` from `WidgetContext`, and fetch
|
|
75
83
|
with `useIntegrationFetch()` from `@ekanos/sdk/hooks` — never global
|
|
76
84
|
`fetch` — reaching for `useActivationData()` (same module) if the widget
|
|
77
|
-
needs a client-visible credential.
|
|
78
|
-
|
|
85
|
+
needs a client-visible credential. For a write (a form submit), call
|
|
86
|
+
`useIntegrationAction()` (same module) instead — it posts to your declared
|
|
87
|
+
`actions[name]` handler, not the vendor directly. Copy the canonical example
|
|
88
|
+
from the SDK README rather than improvising the tree.
|
|
79
89
|
|
|
80
90
|
`Widget.Active` itself renders flush (no padding) so full-bleed charts and
|
|
81
91
|
tables can run edge-to-edge. For ordinary content — text, rows, a KPI
|
|
@@ -80,4 +80,20 @@ export const integration = defineIntegration({
|
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
82
|
],
|
|
83
|
+
|
|
84
|
+
// `actions` is the deterministic counterpart to `tools`: a widget's own
|
|
85
|
+
// Save button calls a declared handler directly (via `useIntegrationAction()`
|
|
86
|
+
// from `@ekanos/sdk/hooks`), with the exact values the user typed — never a
|
|
87
|
+
// natural-language hand-off to the assistant. Uncomment and adapt once a
|
|
88
|
+
// widget needs to write something; see "Actions" in AGENTS.md.
|
|
89
|
+
//
|
|
90
|
+
// actions: {
|
|
91
|
+
// 'create-record': {
|
|
92
|
+
// description: 'Create a record in the base.',
|
|
93
|
+
// input: z.object({ name: z.string().min(1) }),
|
|
94
|
+
// handler: async (_ctx, input) => {
|
|
95
|
+
// return { id: 'rec_123', name: input.name };
|
|
96
|
+
// },
|
|
97
|
+
// },
|
|
98
|
+
// },
|
|
83
99
|
});
|