@ekanos/cli 0.1.5 → 0.1.7
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/README.md +81 -2
- package/dist/bin.js +14 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/dev.d.ts +4 -0
- package/dist/commands/dev.js +6 -20
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts +61 -0
- package/dist/commands/init.js +145 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/publish.d.ts +13 -0
- package/dist/commands/publish.js +38 -15
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.js +9 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts +47 -0
- package/dist/commands/upgrade.js +546 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/validate.d.ts +5 -0
- package/dist/commands/validate.js +20 -6
- package/dist/commands/validate.js.map +1 -1
- package/dist/context.d.ts +9 -0
- package/dist/context.js +9 -0
- package/dist/context.js.map +1 -1
- package/dist/copy-tree.d.ts +48 -0
- package/dist/copy-tree.js +55 -0
- package/dist/copy-tree.js.map +1 -0
- package/dist/delegate.d.ts +28 -0
- package/dist/delegate.js +136 -0
- package/dist/delegate.js.map +1 -0
- package/dist/harness-scaffold.d.ts +21 -6
- package/dist/harness-scaffold.js +8 -5
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.js +40 -3
- package/dist/index.js.map +1 -1
- package/dist/package-manager.d.ts +10 -0
- package/dist/package-manager.js +32 -0
- package/dist/package-manager.js.map +1 -1
- package/dist/parse-argv.d.ts +7 -1
- package/dist/parse-argv.js +10 -2
- package/dist/parse-argv.js.map +1 -1
- package/dist/project-checks.d.ts +9 -0
- package/dist/project-checks.js +29 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +23 -0
- package/dist/project.js +66 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +1 -1
- package/dist/publish-api.js +2 -2
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.d.ts +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/schema-skew.d.ts +77 -0
- package/dist/schema-skew.js +163 -0
- package/dist/schema-skew.js.map +1 -0
- package/dist/sources-api.d.ts +1 -1
- package/dist/sources-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +54 -0
- package/dist/toolchain-api.js +58 -0
- package/dist/toolchain-api.js.map +1 -0
- package/dist/toolchain-resolve.d.ts +26 -0
- package/dist/toolchain-resolve.js +100 -0
- package/dist/toolchain-resolve.js.map +1 -0
- package/dist/toolchain.d.ts +21 -0
- package/dist/toolchain.js +16 -0
- package/dist/toolchain.js.map +1 -0
- package/dist/update-notice.d.ts +32 -0
- package/dist/update-notice.js +180 -0
- package/dist/update-notice.js.map +1 -0
- package/dist/validate-findings.d.ts +12 -0
- package/dist/validate-findings.js +14 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +1 -1
- package/templates/AGENTS.md.tmpl +81 -0
- package/templates/CLAUDE.md.tmpl +2 -1
- package/templates/claude-skill.md.tmpl +20 -2
- package/templates/docs/api-notes.md.tmpl +21 -0
- package/templates/mcp.json.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +178 -0
- package/templates/skills/build-integration/references/api-docs-research.md +93 -0
- package/templates/skills/build-integration/references/api-wiring.md +94 -0
- package/templates/skills/build-integration/references/widget-design.md +132 -0
- package/templates/skills/polish-widgets/SKILL.md +139 -0
- package/templates/skills/polish-widgets/references/patterns.md +269 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-integration
|
|
3
|
+
description: Interview-driven workflow for building and publishing a Fusion integration with the Ekanos SDK. Use when building a new integration from scratch ("build an integration for X", "connect a service like Stripe or Slack", a fresh ekanos scaffold with no substance yet), when adding or changing API wiring ("add an endpoint", "wire this to the API", "add a webhook/tool/schedule"), when researching the target service's API ("how does this API paginate", "what auth does it use", "where are the docs"), or when publishing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a Fusion Integration
|
|
7
|
+
|
|
8
|
+
Guide the user from intent to a published integration. The user supplies
|
|
9
|
+
judgment (what service, what it should show and do); the CLI supplies
|
|
10
|
+
determinism (scaffold, validate, test, publish). Never hand-roll what a verb
|
|
11
|
+
does; never guess at platform APIs — the contract is in `AGENTS.md` at the
|
|
12
|
+
project root. Read it before writing any integration code. The integration's
|
|
13
|
+
identity is the `slug` in `ekanos.json`; the definition is one
|
|
14
|
+
`defineIntegration()` in `src/integration.ts`.
|
|
15
|
+
|
|
16
|
+
Every `ekanos` verb supports `--json` and exits nonzero on failure. Validate
|
|
17
|
+
findings carry a `hint` that is a remediation instruction: follow it.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
1. **Interview** — establish the plan and the docs, get approval
|
|
22
|
+
2. **Research** — ground the target API in its real docs (see `references/api-docs-research.md`)
|
|
23
|
+
3. **Declare** — write the `defineIntegration` skeleton, validate clean
|
|
24
|
+
4. **Widgets** — design-first, all four states (see `references/widget-design.md`)
|
|
25
|
+
5. **Wire** — activation → secrets → `ctx.fetch`/storage → tools/webhooks/schedules (see `references/api-wiring.md`)
|
|
26
|
+
6. **Verify** — validate + test + human look at the harness
|
|
27
|
+
7. **Publish** — checklist, explicit human approval, `ekanos publish`
|
|
28
|
+
|
|
29
|
+
Work one phase at a time. After each phase, state in one line what was done
|
|
30
|
+
and what is next.
|
|
31
|
+
|
|
32
|
+
## Phase 1: Interview
|
|
33
|
+
|
|
34
|
+
Ask in order, ONE message per step, skipping anything the user already said.
|
|
35
|
+
Use AskUserQuestion for the multiple-choice steps when the tool is available;
|
|
36
|
+
otherwise ask in plain prose with lettered options.
|
|
37
|
+
|
|
38
|
+
1. **The service** (free text): which target service or API? Whether they
|
|
39
|
+
already have an account / API key / sandbox.
|
|
40
|
+
2. **The docs** (free text): where does the target API's documentation live?
|
|
41
|
+
Prefer, in this order:
|
|
42
|
+
- (a) an **OpenAPI/Swagger spec URL** — the gold standard: exact paths,
|
|
43
|
+
params, and auth schemes with no prose ambiguity;
|
|
44
|
+
- (b) an official docs URL;
|
|
45
|
+
- (c) a local file the developer drops into `docs/`.
|
|
46
|
+
|
|
47
|
+
If `ekanos.json` already has a `docs` array (set via `ekanos init
|
|
48
|
+
--docs`), confirm those entries instead of re-asking. Whatever the
|
|
49
|
+
developer supplies, WRITE it into `ekanos.json`'s `docs` array (http(s)
|
|
50
|
+
URLs or repo-relative paths) so it is versioned with the project.
|
|
51
|
+
|
|
52
|
+
3. **The outcomes** (free text): "What should a team see on their dashboard,
|
|
53
|
+
and what should the integration be able to do?" Push for 1–3 concrete
|
|
54
|
+
outcomes ("show open invoices", "alert on new disputes", "let the
|
|
55
|
+
assistant create a customer"). Each SEE-outcome becomes a widget; each
|
|
56
|
+
DO-outcome becomes a tool, webhook, or schedule.
|
|
57
|
+
4. **Auth** (choice): API key / Bearer / OAuth 2.0 / Basic / not sure
|
|
58
|
+
(research will settle it). This decides the activation-form fields — and
|
|
59
|
+
every string field name on that form IS a secret name (see AGENTS.md).
|
|
60
|
+
5. **Freshness** (choice): live fetch on view / cached with a refresh
|
|
61
|
+
schedule / pushed by webhooks. This decides storage keys and schedules.
|
|
62
|
+
|
|
63
|
+
Then present **the plan** as a short table — widgets (name, what it shows),
|
|
64
|
+
activation fields (exact names), egress origins, storage keys (with data-type
|
|
65
|
+
prefixes), tools/webhooks/schedules — and get an explicit yes before writing
|
|
66
|
+
code. The plan is the contract for the rest of the session; note any change
|
|
67
|
+
the user makes to it later.
|
|
68
|
+
|
|
69
|
+
## Phase 2: Research
|
|
70
|
+
|
|
71
|
+
Read `references/api-docs-research.md` before this phase — it carries the
|
|
72
|
+
mechanics: how to read a spec without ingesting it whole, what to extract
|
|
73
|
+
from prose, when Context7 helps, and the notes-file skeleton.
|
|
74
|
+
|
|
75
|
+
Resolve documentation down this ladder — stop at the first rung that answers
|
|
76
|
+
the question at hand:
|
|
77
|
+
|
|
78
|
+
1. **Local files** listed in `ekanos.json`'s `docs` array — read them.
|
|
79
|
+
2. **OpenAPI/Swagger spec URL** — fetch it; prefer it over prose docs.
|
|
80
|
+
3. **Official docs URL** — fetch and extract.
|
|
81
|
+
4. **Context7 MCP** (available via this project's `.mcp.json`) — best for
|
|
82
|
+
questions about the service's official client libraries and SDKs, not
|
|
83
|
+
for raw REST surfaces.
|
|
84
|
+
5. **Web search** — last resort, and SAY you are falling back to search.
|
|
85
|
+
|
|
86
|
+
**Never invent an API surface from memory alone.** If nothing on the ladder
|
|
87
|
+
resolves, stop and ask the developer for a file or URL rather than
|
|
88
|
+
proceeding.
|
|
89
|
+
|
|
90
|
+
Write findings into `docs/api-notes.md` (the scaffold ships a stub): auth
|
|
91
|
+
model, base URL(s), the endpoints each planned outcome needs (method, path,
|
|
92
|
+
key params), pagination scheme, rate limits, error shapes, webhook/event
|
|
93
|
+
surfaces if planned. One reason it is a file and not a comment: it is
|
|
94
|
+
versioned, reviewable, and survives docs-site rot — later sessions read it
|
|
95
|
+
instead of re-researching. Correct the plan if research contradicts it and
|
|
96
|
+
tell the user what changed.
|
|
97
|
+
|
|
98
|
+
## Phase 3: Declare
|
|
99
|
+
|
|
100
|
+
Write the `defineIntegration` skeleton per AGENTS.md: metadata, `egress`
|
|
101
|
+
(only the origins from the plan), `storage` schemas (declared keys only,
|
|
102
|
+
data-type prefixes from the closed vocabulary), empty-but-typed widgets/tools
|
|
103
|
+
lists. Then:
|
|
104
|
+
|
|
105
|
+
npx ekanos validate
|
|
106
|
+
|
|
107
|
+
Zero findings before moving on. Re-run after EVERY change to the declaration
|
|
108
|
+
for the rest of the session — it is fast and its findings are instructions.
|
|
109
|
+
|
|
110
|
+
## Phase 4: Widgets — design first
|
|
111
|
+
|
|
112
|
+
Read `references/widget-design.md` before the first widget. Build each widget
|
|
113
|
+
with realistic mock data first, make it distinctive and polished, implement
|
|
114
|
+
all four states (loading / empty / error / success), THEN wire real data.
|
|
115
|
+
Check rendering in the harness:
|
|
116
|
+
|
|
117
|
+
npx ekanos dev
|
|
118
|
+
|
|
119
|
+
and ask the user to look at http://localhost:3100 at each significant visual
|
|
120
|
+
change — do not claim a widget looks good without either a screenshot or the
|
|
121
|
+
user's confirmation. A review-and-fix pass over widgets that already exist
|
|
122
|
+
(flat rows, no hierarchy, overflow, a bare tile) is the `polish-widgets`
|
|
123
|
+
skill — use it here as the final styling pass and again before publish.
|
|
124
|
+
|
|
125
|
+
## Phase 5: Wire
|
|
126
|
+
|
|
127
|
+
Read `references/api-wiring.md`. Work from `docs/api-notes.md`, not
|
|
128
|
+
conversational memory — every endpoint, header, and param in client code
|
|
129
|
+
must trace to a line in the notes. When the live API disagrees with the
|
|
130
|
+
notes, update `docs/api-notes.md` in the same commit as the code fix.
|
|
131
|
+
|
|
132
|
+
Order: activation form (field names = secret names, keep them in exported
|
|
133
|
+
constants) → server handlers using `ctx.fetch` / `ctx.storage` /
|
|
134
|
+
`ctx.secrets` only → tools → webhooks/schedules. Write tests alongside each
|
|
135
|
+
handler with `createMockContext` / `invokeWebhook` / `invokeSchedule` from
|
|
136
|
+
`@ekanos/sdk/testing`, and run:
|
|
137
|
+
|
|
138
|
+
npx ekanos test
|
|
139
|
+
|
|
140
|
+
## Phase 6: Verify
|
|
141
|
+
|
|
142
|
+
All of these, in order, all green:
|
|
143
|
+
|
|
144
|
+
1. `npx ekanos validate` — zero findings
|
|
145
|
+
2. `npx ekanos test` — all passing, including at least one test per handler
|
|
146
|
+
and one failure-path test per external call
|
|
147
|
+
3. Harness check — every widget renders all four states (mock the states if
|
|
148
|
+
needed); user has seen and approved the visuals
|
|
149
|
+
4. Grep your own diff for leaks: no credential in `ctx.storage`, `configData`,
|
|
150
|
+
logs, or tool results; no global `fetch`/`process.env`/`node:fs`
|
|
151
|
+
|
|
152
|
+
## Phase 7: Publish — gated
|
|
153
|
+
|
|
154
|
+
Publishing sends the project to the Fusion host and is versioned immutably.
|
|
155
|
+
NEVER run it on your own judgment. Show the user:
|
|
156
|
+
|
|
157
|
+
- the verify results (1–4 above)
|
|
158
|
+
- `package.json` version (bump it if this slug+version was already published)
|
|
159
|
+
- the target host and source (from ekanos.json, or ask)
|
|
160
|
+
|
|
161
|
+
and ask plainly: "Ready to publish?" Only on an explicit yes:
|
|
162
|
+
|
|
163
|
+
npx ekanos publish
|
|
164
|
+
|
|
165
|
+
If it exits 4 (auth required), the user must run `npx ekanos login` — the
|
|
166
|
+
device flow needs their browser; wait for them. On success, report the
|
|
167
|
+
submission id and state, and mention `npx ekanos status` shows it from now
|
|
168
|
+
on. On `invalid_state` (exit 7), the version exists — bump `package.json`
|
|
169
|
+
and re-confirm before retrying.
|
|
170
|
+
|
|
171
|
+
## Failure etiquette
|
|
172
|
+
|
|
173
|
+
- A validate finding, test failure, or nonzero verb exit is never something
|
|
174
|
+
to narrate past — fix it or surface it.
|
|
175
|
+
- If the same error survives two fix attempts, stop and show the user the
|
|
176
|
+
exact output.
|
|
177
|
+
- Never edit files under `.ekanos/` (generated) or weaken a test to get to
|
|
178
|
+
green.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Researching the target API
|
|
2
|
+
|
|
3
|
+
Read this at Phase 2 (Research), before fetching anything. The ladder and the
|
|
4
|
+
hard rule live in SKILL.md; this file is the mechanics — how to work each
|
|
5
|
+
rung and what to leave behind in `docs/api-notes.md`.
|
|
6
|
+
|
|
7
|
+
## Reading an OpenAPI/Swagger spec efficiently
|
|
8
|
+
|
|
9
|
+
Do not ingest the whole spec — production specs run to megabytes and most of
|
|
10
|
+
it is endpoints the plan will never touch. Read in this order:
|
|
11
|
+
|
|
12
|
+
1. `servers` (or Swagger 2.0 `host` + `basePath`) — the real base URL(s),
|
|
13
|
+
including environment variants (sandbox vs production).
|
|
14
|
+
2. `components.securitySchemes` (2.0: `securityDefinitions`) plus top-level
|
|
15
|
+
`security` — the auth model and the EXACT header/query shape. This settles
|
|
16
|
+
the interview's "not sure" auth answer.
|
|
17
|
+
3. Only the paths the plan needs: search the spec for the resource nouns from
|
|
18
|
+
the plan's outcomes, then read those operations fully — method, path
|
|
19
|
+
template, required params, request/response schemas, error responses.
|
|
20
|
+
4. Pagination: look at the list operations' params (`cursor`, `page`,
|
|
21
|
+
`limit`, `starting_after`) and response envelopes (`next`, `has_more`,
|
|
22
|
+
`Link` header) — pagination is per-operation, never assume one scheme.
|
|
23
|
+
|
|
24
|
+
If the spec is JSON, `jq` the sections above rather than reading linearly.
|
|
25
|
+
Note the spec's own `info.version` and the URL it was fetched from in the
|
|
26
|
+
notes — that is the line future sessions use to detect drift.
|
|
27
|
+
|
|
28
|
+
## Extracting from prose docs
|
|
29
|
+
|
|
30
|
+
Capture: base URL, auth header shape (copy a verbatim example request),
|
|
31
|
+
endpoint tables (method, path, required params), pagination scheme, documented
|
|
32
|
+
rate limits and the 429 behavior, error response body shape, webhook event
|
|
33
|
+
names and signature scheme. Copy exact strings — paths, header names, event
|
|
34
|
+
names — never paraphrase them.
|
|
35
|
+
|
|
36
|
+
Skip: marketing pages, quickstart tutorials, SDK installation guides,
|
|
37
|
+
changelog prose. Watch for versioned base URLs (`/v1/` vs `/v2/`) — docs
|
|
38
|
+
sites routinely show both, and mixing them is a classic silent 404.
|
|
39
|
+
|
|
40
|
+
## Using Context7
|
|
41
|
+
|
|
42
|
+
Context7 (via this project's `.mcp.json`) serves indexed documentation for
|
|
43
|
+
libraries and SDKs. It is good for: the target service's OFFICIAL client
|
|
44
|
+
library — method signatures, initialization, idiomatic usage — and for
|
|
45
|
+
questions about packages the integration depends on. It is NOT an authority
|
|
46
|
+
on raw REST surfaces: when Context7's account of an endpoint disagrees with
|
|
47
|
+
the OpenAPI spec or official docs, the spec wins. If the integration calls
|
|
48
|
+
the REST API directly (the common case — `ctx.fetch` means no vendor SDK),
|
|
49
|
+
Context7 is a supplement, not a source.
|
|
50
|
+
|
|
51
|
+
## The `docs/api-notes.md` structure
|
|
52
|
+
|
|
53
|
+
The scaffold ships a stub; grow it into this shape (drop sections that do not
|
|
54
|
+
apply, keep the order):
|
|
55
|
+
|
|
56
|
+
# API notes: <service>
|
|
57
|
+
Sources: <spec/docs URLs + fetch date>
|
|
58
|
+
|
|
59
|
+
## Auth
|
|
60
|
+
<scheme; exact header shape, e.g. `Authorization: Bearer <key>`>
|
|
61
|
+
|
|
62
|
+
## Base URLs
|
|
63
|
+
<production; sandbox if any>
|
|
64
|
+
|
|
65
|
+
## Endpoints
|
|
66
|
+
| Outcome | Method | Path | Key params |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| open invoices widget | GET | /v1/invoices | status, limit, cursor |
|
|
69
|
+
|
|
70
|
+
## Pagination
|
|
71
|
+
<scheme, per-operation exceptions>
|
|
72
|
+
|
|
73
|
+
## Rate limits
|
|
74
|
+
<documented limits; 429 behavior, Retry-After>
|
|
75
|
+
|
|
76
|
+
## Errors
|
|
77
|
+
<error body shape; which statuses are retryable>
|
|
78
|
+
|
|
79
|
+
## Webhooks
|
|
80
|
+
<events used, signature header + scheme, retry behavior>
|
|
81
|
+
|
|
82
|
+
Every endpoint the Wire phase touches must have a row here first. Keep it a
|
|
83
|
+
snapshot, not a mirror — only what THIS integration uses.
|
|
84
|
+
|
|
85
|
+
## When the live API contradicts the notes
|
|
86
|
+
|
|
87
|
+
A 404 on a noted path, an auth header that bounces, a response missing a
|
|
88
|
+
noted field: believe the live API. Update `docs/api-notes.md` in the SAME
|
|
89
|
+
commit as the code fix — a note that disagrees with shipped code is worse
|
|
90
|
+
than no note — and record what was observed and when, so the correction is
|
|
91
|
+
not "fixed back" from the stale docs site later. If the contradiction changes
|
|
92
|
+
the plan (an endpoint gone, auth scheme different), surface it to the user
|
|
93
|
+
before wiring around it.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Wiring an integration to its target API
|
|
2
|
+
|
|
3
|
+
Read this at Phase 5 (Wire). The binding contract (ctx surface, storage
|
|
4
|
+
vocabulary, secret naming) is `AGENTS.md`; the target API's contract is
|
|
5
|
+
`docs/api-notes.md` — write client code from the notes, not from memory, and
|
|
6
|
+
when the live API contradicts them, fix the notes in the same commit as the
|
|
7
|
+
code. This file is the order of work and the traps.
|
|
8
|
+
|
|
9
|
+
## Order of work
|
|
10
|
+
|
|
11
|
+
1. **Constants first** — `src/config.ts` exporting every name that crosses a
|
|
12
|
+
boundary: secret names (= activation field names), storage keys, webhook
|
|
13
|
+
ids, egress origins. Both the form and every handler import from here;
|
|
14
|
+
a renamed form field silently orphans its secret otherwise.
|
|
15
|
+
2. **Activation form** — zod schema + react-hook-form inside
|
|
16
|
+
`BaseActivationForm` (see AGENTS.md). Every string field the user submits
|
|
17
|
+
becomes `ctx.secrets.get('<field name>')`. Validate the credential shape
|
|
18
|
+
in the schema (prefix, length) so a paste error fails at the form, not in
|
|
19
|
+
a handler.
|
|
20
|
+
3. **A thin API client** — one module owning base URL, auth header assembly,
|
|
21
|
+
response parsing, and error mapping, taking `ctx` (or just
|
|
22
|
+
`{ fetch, secrets }`) as an argument. Handlers never call `ctx.fetch`
|
|
23
|
+
directly for endpoints the client covers.
|
|
24
|
+
4. **Handlers** — tools, then webhooks, then schedules, each with tests as
|
|
25
|
+
they are written (`createMockContext`, `invokeWebhook`, `invokeSchedule`
|
|
26
|
+
from `@ekanos/sdk/testing`).
|
|
27
|
+
5. **Widget data** — widgets cannot call the target API (no client-side
|
|
28
|
+
`ctx`). The pattern: a schedule (or webhook) fetches and writes a
|
|
29
|
+
`clientReadable` storage key; the widget reads it back with
|
|
30
|
+
`fetchIntegrationStorage()` from `@ekanos/sdk/hooks`. Cache freshness in
|
|
31
|
+
the stored value (`fetchedAt`) and show it in the UI when staleness
|
|
32
|
+
matters.
|
|
33
|
+
|
|
34
|
+
## Traps, each one earned
|
|
35
|
+
|
|
36
|
+
- **Egress is an allowlist, checked synchronously.** Every origin the client
|
|
37
|
+
touches must be in `egress` — including the OAuth token host and any host a
|
|
38
|
+
redirect lands on. A missing origin is a thrown error, not a warning.
|
|
39
|
+
- **Secrets never leave the secrets surface.** Not into `ctx.storage`, not
|
|
40
|
+
into logs, not into a tool result, not into a widget's stored value. Token
|
|
41
|
+
rotation: `ctx.secrets.set()` (account tier only).
|
|
42
|
+
- **`ctx.storage.user` throws for machine actors** (workflow runs). A tool
|
|
43
|
+
that touches user storage must branch on `ctx.actor.kind` or stick to
|
|
44
|
+
account storage.
|
|
45
|
+
- **Webhook signatures are declared, not hand-verified** — `signature:
|
|
46
|
+
{ header, secretName }` in the declaration; the transport verifies before
|
|
47
|
+
the handler runs. Return `{ status: 'ignored' }` for valid-but-irrelevant
|
|
48
|
+
deliveries so the sender stops retrying; throw only for retryable faults.
|
|
49
|
+
- **Handlers run under redelivery and "Run now".** Make writes idempotent
|
|
50
|
+
(stable keys, upsert semantics); `invocation.trigger` distinguishes cron
|
|
51
|
+
from manual.
|
|
52
|
+
- **Schedules are 5-field numeric cron** — no names, no `@daily`.
|
|
53
|
+
- **Rate limits**: respect `Retry-After` on 429 in the client; a schedule
|
|
54
|
+
that hammers a free-tier API takes every tenant down with it.
|
|
55
|
+
- **Storage schema evolution**: a stored value that no longer parses throws
|
|
56
|
+
on read — version the key or widen with a union; never edit a schema to a
|
|
57
|
+
shape old rows can't satisfy.
|
|
58
|
+
- **Cache rows must know what they were built from.** A cache keyed by
|
|
59
|
+
nothing and invalidated only by time serves the PREVIOUS configuration
|
|
60
|
+
after the user changes activation data (old city, old account, old list)
|
|
61
|
+
until the next schedule tick — confidently wrong answers. Embed a config
|
|
62
|
+
fingerprint in the stored value (the activation fields the fetch depended
|
|
63
|
+
on) and treat a mismatch on read as a cache miss. Tools reading the cache
|
|
64
|
+
must check the fingerprint, not just row freshness. Three details that are
|
|
65
|
+
each easy to get wrong:
|
|
66
|
+
- Include the fields that change the VALUES, not just the ones that select
|
|
67
|
+
the data — units, currency, locale, timezone. They leave the identity
|
|
68
|
+
byte-identical while making every number wrong.
|
|
69
|
+
- Stabilize noisy members (round coordinates, normalize case) so the
|
|
70
|
+
fingerprint doesn't churn under inputs the answer doesn't depend on.
|
|
71
|
+
- Declare the fingerprint field `.optional()` and treat ABSENT as a miss.
|
|
72
|
+
A required field makes every pre-existing row throw on read (see the
|
|
73
|
+
schema-evolution trap above) — and for a `clientReadable` key that throw
|
|
74
|
+
reaches the widgets, turning a cache improvement into a dashboard
|
|
75
|
+
outage. Optional-plus-absent-is-stale degrades to a refetch instead.
|
|
76
|
+
|
|
77
|
+
## Test floor per handler
|
|
78
|
+
|
|
79
|
+
- happy path against `createMockContext` with seeded secrets + programmed
|
|
80
|
+
fetch responses
|
|
81
|
+
- one failure path (API 500/timeout) proving the handler returns/throws the
|
|
82
|
+
designed way rather than leaking the raw error
|
|
83
|
+
- for webhooks: one `ignored` case; for schedules: one idempotency case
|
|
84
|
+
(same invocation twice, no duplicate writes)
|
|
85
|
+
|
|
86
|
+
## Verifying against the real API — live mode
|
|
87
|
+
|
|
88
|
+
The harness mocks Fusion, never the vendor. Do not tell the developer that
|
|
89
|
+
local work is mock-only: opt the integration into live mode (a `live` block
|
|
90
|
+
on its `harness.config.ts` entry — see AGENTS.md "Live mode") and the
|
|
91
|
+
toolbar's fixtures ⇄ live switch runs real widget queries against the real
|
|
92
|
+
API, gated only by the declared `egress`. Use fixtures for the Fusion-side
|
|
93
|
+
state widgets need first, and for webhook/schedule handlers (the Triggers
|
|
94
|
+
surface runs those against the mock context by design).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Widget design for Ekanos integrations
|
|
2
|
+
|
|
3
|
+
Read this before building the first widget. The mechanics (declaration shape,
|
|
4
|
+
`Widget.*` API, `WidgetContext`, preview) are in `AGENTS.md` and the
|
|
5
|
+
`@ekanos/sdk` README §`components.widgets` — this file is the design judgment.
|
|
6
|
+
|
|
7
|
+
## Philosophy: form over function
|
|
8
|
+
|
|
9
|
+
Build the widget beautiful with realistic MOCK data first; wire real data
|
|
10
|
+
last. A widget that renders nothing interesting with perfect plumbing is a
|
|
11
|
+
failure; the reverse is a demo. Sequence per widget:
|
|
12
|
+
|
|
13
|
+
1. Distinctive UI with hardcoded, realistic mock data
|
|
14
|
+
2. All four states (loading / empty / error / success)
|
|
15
|
+
3. Real data via the wiring reference
|
|
16
|
+
4. A final styling pass against the checklist below
|
|
17
|
+
|
|
18
|
+
"Distinctive" means: if the mock data were swapped for another service's, the
|
|
19
|
+
widget should NOT still look right. Lead with the one number, chart, or list
|
|
20
|
+
the user said matters (the interview's SEE-outcome); trim everything that
|
|
21
|
+
does not serve it.
|
|
22
|
+
|
|
23
|
+
## The key question — decide it before writing JSX
|
|
24
|
+
|
|
25
|
+
Every widget answers 1–2 questions, and the answer must be the most prominent
|
|
26
|
+
element on it — size, weight, position. Everything else is supporting context
|
|
27
|
+
and recedes (`text-muted-foreground`, smaller). Name the question from the
|
|
28
|
+
interview's SEE-outcome before writing the first class; a widget you cannot
|
|
29
|
+
name the question for is a widget you cannot design.
|
|
30
|
+
|
|
31
|
+
| Widget type | Key question | Most prominent element |
|
|
32
|
+
| ------------------- | ---------------------------------- | --------------------------------------------------------------------- |
|
|
33
|
+
| KPI / stat | "How are we doing?" | The hero number: `text-3xl font-semibold tracking-tight tabular-nums` |
|
|
34
|
+
| List / feed | "What needs my attention?" | Item name `text-base font-medium` + its value, right-aligned |
|
|
35
|
+
| Chart | "Which way is it going?" | The chart + current value; axes and legends muted |
|
|
36
|
+
| Action / connection | "What can I do / who's connected?" | One clear CTA or the identity + status |
|
|
37
|
+
|
|
38
|
+
The default failure is uniform weight: everything `text-sm`, values dressed
|
|
39
|
+
like labels. Squint at the render — if nothing reads first, there is no
|
|
40
|
+
hierarchy, and no amount of padding will save it.
|
|
41
|
+
|
|
42
|
+
## Layout recipes (the shapes that work)
|
|
43
|
+
|
|
44
|
+
**KPI / stat card** — one hero, a delta, supporting stats demoted:
|
|
45
|
+
|
|
46
|
+
- Center block: label `text-sm text-muted-foreground`, then the hero value
|
|
47
|
+
`text-3xl font-semibold tracking-tight tabular-nums`, then a delta line
|
|
48
|
+
`text-sm font-medium` in `text-success-strong` / `text-destructive`.
|
|
49
|
+
- Supporting stats as a `border-t` footer strip: per-stat
|
|
50
|
+
`text-lg font-semibold` over a `text-xs text-muted-foreground` label,
|
|
51
|
+
columns separated by `bg-border h-8 w-px`.
|
|
52
|
+
- One `text-3xl` element per widget — two heroes is zero heroes.
|
|
53
|
+
|
|
54
|
+
**List / feed** — rows with a first read:
|
|
55
|
+
|
|
56
|
+
- Container `divide-y`; each row `flex items-center gap-3 px-4 py-3`.
|
|
57
|
+
- Name: `min-w-0 flex-1` wrapping `truncate text-base font-medium`, with an
|
|
58
|
+
optional `text-sm text-muted-foreground` second line.
|
|
59
|
+
- Value: `shrink-0 text-right text-base font-semibold tabular-nums`.
|
|
60
|
+
- 5–7 rows visible; the rest behind inner scroll or a "View all" ghost button.
|
|
61
|
+
- One status treatment per row: a `Badge` variant (`success` / `warning` /
|
|
62
|
+
`info` / `destructive`) or an `h-2.5 w-2.5` semantic dot — never both.
|
|
63
|
+
|
|
64
|
+
**Chart** — the picture is the point:
|
|
65
|
+
|
|
66
|
+
- Chart fills the body; current value as a `text-2xl font-semibold` readout
|
|
67
|
+
above or beside it, axes/gridlines/legend all muted.
|
|
68
|
+
- Series colors from chart tokens (`--chart-1`…), never hand-picked palette.
|
|
69
|
+
|
|
70
|
+
## The skeleton
|
|
71
|
+
|
|
72
|
+
Every widget: `'use client'`, `Widget.DataState` wrapping `Widget.Card` →
|
|
73
|
+
`Widget.Header` + `Widget.Loading` + `Widget.Error` + `Widget.Active`. Gate
|
|
74
|
+
fetching on `ctx?.state.state === 'active'` from `WidgetContext`. Copy the
|
|
75
|
+
canonical example from the SDK README rather than improvising the tree.
|
|
76
|
+
|
|
77
|
+
## Four states, no exceptions
|
|
78
|
+
|
|
79
|
+
- **Loading**: `Widget.Loading` (or `skeleton` blocks shaped like the real
|
|
80
|
+
content — never a bare spinner in a big empty card).
|
|
81
|
+
- **Error**: `Widget.Error`, plus a one-line human cause when known. Never
|
|
82
|
+
render an exception message verbatim.
|
|
83
|
+
- **Empty**: a real designed state — icon (h-8 w-8, muted), one sentence
|
|
84
|
+
saying what will appear here and what action produces it. Empty ≠ error.
|
|
85
|
+
- **Success**: the design.
|
|
86
|
+
|
|
87
|
+
While iterating, expose a temporary way to flip states (a dev-only prop or a
|
|
88
|
+
mock switch) so the harness can show all four; remove it before publish.
|
|
89
|
+
|
|
90
|
+
## Fusion design rules (the harness applies the host theme)
|
|
91
|
+
|
|
92
|
+
- **Semantic tokens only**: `bg-card`, `text-foreground`, `text-muted-foreground`,
|
|
93
|
+
`border-border`, `bg-primary`, `text-destructive`. NEVER raw palette
|
|
94
|
+
classes (`bg-white`, `text-gray-500`, `bg-amber-400`) or hex/oklch
|
|
95
|
+
literals — they break in dark mode and off-brand tenants.
|
|
96
|
+
- **Accent discipline**: the primary (coral-orange) on ≤10% of the widget —
|
|
97
|
+
one CTA, one highlighted value, one active state. Semantic status colors
|
|
98
|
+
(success/warning/destructive) are not accents.
|
|
99
|
+
- **Depth via shadow, not borders** where a surface needs lift.
|
|
100
|
+
- **Sizing floors**: body text `text-sm`+; `text-xs` only for tertiary
|
|
101
|
+
metadata (timestamps, badge counts). Icons in lists `h-5 w-5`+. Card
|
|
102
|
+
padding `p-5`/`p-6`. Row padding: `px-4 py-3` (the recipes above use
|
|
103
|
+
this; hard floor `py-2 px-3` — never below). Section gaps `space-y-5`+.
|
|
104
|
+
- **Numbers**: hero/KPI values large and `tracking-tight` with
|
|
105
|
+
`tabular-nums` when digits align.
|
|
106
|
+
- **Overflow**: the widget must hold its grid size — inner scroll
|
|
107
|
+
(`overflow-y-auto` on the list, never the card) and `truncate` on
|
|
108
|
+
user-supplied strings. Test the sm layout, not just lg.
|
|
109
|
+
- **Components**: build from `@ekanos/ui/*` (the module list is in
|
|
110
|
+
AGENTS.md); `cn()` from `@ekanos/ui/utils` for conditional classes; icons
|
|
111
|
+
through `@ekanos/ui/icon` (Font Awesome names — they render empty outside
|
|
112
|
+
a host that loads FA; that is expected in bare tests).
|
|
113
|
+
|
|
114
|
+
## Layout sizing
|
|
115
|
+
|
|
116
|
+
Pick `layouts` from the content, not habit: KPI/stat cards 3–4 wide × 6–8
|
|
117
|
+
high; lists and feeds 4–6 × 10–14; charts 6–8 × 10–12 (grid units, lg
|
|
118
|
+
breakpoint). Give md/sm the same content priority order — cut columns, not
|
|
119
|
+
meaning.
|
|
120
|
+
|
|
121
|
+
## Checklist before calling a widget done
|
|
122
|
+
|
|
123
|
+
- [ ] Four states render in the harness; user has seen them
|
|
124
|
+
- [ ] No raw color literals; accent ≤10%; dark mode looks intentional
|
|
125
|
+
- [ ] Text floors respected; nothing truncates that matters at sm
|
|
126
|
+
- [ ] Mock data replaced by real data (or the mock is clearly temporary and
|
|
127
|
+
the wiring phase is next)
|
|
128
|
+
- [ ] Widget id is slug-prefixed and globally unique
|
|
129
|
+
|
|
130
|
+
A styling review of existing widgets — worst-first findings, before→after
|
|
131
|
+
fixes, the tile — is the `polish-widgets` skill; use it rather than
|
|
132
|
+
re-deriving a review pass from this file.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: polish-widgets
|
|
3
|
+
description: Review-and-fix pass over existing widget styling. Use when the user asks to review widget styling, wants widgets or the marketplace tile to look better, says the tile design is bad or the widgets look plain/generic/flat, reports overflow or responsiveness problems, or before publishing as a final styling pass.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Polish Widgets
|
|
7
|
+
|
|
8
|
+
A widget that renders the right data can still be a bad widget: every row the
|
|
9
|
+
same weight, no element answering the widget's question, a grid of identical
|
|
10
|
+
stat chips. This skill is the fix pass — render each widget in the harness,
|
|
11
|
+
find the worst offenses, fix them, show the user before and after. The
|
|
12
|
+
creation-time design rules live in
|
|
13
|
+
`../build-integration/references/widget-design.md`; this skill assumes they
|
|
14
|
+
exist and enforces them on code already written.
|
|
15
|
+
|
|
16
|
+
## The key question
|
|
17
|
+
|
|
18
|
+
Every widget exists to answer 1–2 questions. Name the question FIRST. Its
|
|
19
|
+
answer must be the most prominent element — size, weight, position — and
|
|
20
|
+
everything else demotes to supporting context. If you cannot name the
|
|
21
|
+
question, that is the finding: ask the user what the widget is for before
|
|
22
|
+
touching its classes.
|
|
23
|
+
|
|
24
|
+
| Widget type | Key question | Most prominent element |
|
|
25
|
+
| ------------------- | ---------------------------------- | --------------------------------------------------------------------------- |
|
|
26
|
+
| KPI / stat | "How are we doing?" | The hero number: `text-3xl font-semibold tracking-tight tabular-nums` |
|
|
27
|
+
| List / feed | "What needs my attention?" | Item name `text-base font-medium` + its value, right-aligned `tabular-nums` |
|
|
28
|
+
| Chart | "Which way is it going?" | The chart plus the current value; axes and legends muted |
|
|
29
|
+
| Action card | "What can I do here?" | One clear CTA; supporting copy secondary |
|
|
30
|
+
| Status / connection | "Is it working? What's connected?" | Badge or status dot + the identity it describes |
|
|
31
|
+
|
|
32
|
+
The most common failure is uniform weight: everything `text-sm`, values styled
|
|
33
|
+
like their labels. Squint at the rendered widget — if nothing reads first,
|
|
34
|
+
there is no hierarchy, and that fix comes before any padding nit.
|
|
35
|
+
|
|
36
|
+
## Review procedure
|
|
37
|
+
|
|
38
|
+
1. **Enumerate** the widgets from `components.widgets` in `src/integration.ts`
|
|
39
|
+
— id, name, component file. Review all of them unless the user scoped it.
|
|
40
|
+
2. **Render**: `npx ekanos dev`, then the Widgets surface at
|
|
41
|
+
`http://localhost:3100/<slug>/widgets`. Drive the review from the dev
|
|
42
|
+
toolbar: Render state (loading / error / inactive / disabled / active),
|
|
43
|
+
Viewport presets (check sm, not just lg), Collapsed, Unhealthy, and the
|
|
44
|
+
theme toggle — dark mode is part of the review, not an extra.
|
|
45
|
+
3. **Assess** each widget against the checklist below and write findings per
|
|
46
|
+
widget, worst-first. Missing hierarchy outranks spacing; a broken sm layout
|
|
47
|
+
outranks a badge color.
|
|
48
|
+
4. **Fix** worst-first, one widget at a time. After each: `npx tsc --noEmit`,
|
|
49
|
+
re-render in the harness.
|
|
50
|
+
5. **Show the user** before/after for each widget — screenshots or ask them to
|
|
51
|
+
look — and get their read before the next one. Never claim a widget looks
|
|
52
|
+
better without one of the two.
|
|
53
|
+
|
|
54
|
+
## Checklist
|
|
55
|
+
|
|
56
|
+
**Hierarchy**
|
|
57
|
+
|
|
58
|
+
- The key question's answer is the visually dominant element
|
|
59
|
+
- One prominent element per row; supporting info is `text-muted-foreground`
|
|
60
|
+
- Hero/stat values match the recipe: `text-3xl font-semibold tracking-tight
|
|
61
|
+
tabular-nums` (never below `text-2xl`, and never `text-sm` on a number
|
|
62
|
+
that matters)
|
|
63
|
+
- List row titles `text-base font-medium`; subtitles `text-sm text-muted-foreground`
|
|
64
|
+
- `text-xs` only on tertiary metadata (timestamps, header badge counts)
|
|
65
|
+
|
|
66
|
+
**Tokens & color**
|
|
67
|
+
|
|
68
|
+
- Semantic tokens only — no `bg-white`, `text-gray-500`, hex, raw palette shades
|
|
69
|
+
- Primary accent on ≤10% of the widget; status through `Badge` variants
|
|
70
|
+
(`success` / `warning` / `info` / `destructive`) or a semantic status dot
|
|
71
|
+
- No two colored elements competing in one row
|
|
72
|
+
- Both themes checked with the toolbar toggle
|
|
73
|
+
|
|
74
|
+
**Layout & overflow**
|
|
75
|
+
|
|
76
|
+
- Lists cap at 5–7 visible items; more goes behind inner scroll
|
|
77
|
+
(`min-h-0 flex-1 overflow-y-auto` on the list, never the card) or "View all"
|
|
78
|
+
- `truncate` + `min-w-0` on user-supplied strings; values `shrink-0` and right-aligned
|
|
79
|
+
- The sm viewport holds: cut columns, keep meaning
|
|
80
|
+
- Sizing floors hold: rows `px-4 py-3`+, list icons `h-5 w-5`+, gaps `gap-3`+,
|
|
81
|
+
body text `text-sm`+
|
|
82
|
+
|
|
83
|
+
**States & numbers**
|
|
84
|
+
|
|
85
|
+
- All four states render (the toolbar flips loading/error/inactive; empty needs
|
|
86
|
+
a data mock) and empty is a designed state, not a blank card
|
|
87
|
+
- Digits that align use `tabular-nums`; money is right-aligned with fixed width
|
|
88
|
+
|
|
89
|
+
The canonical fix for the flat-row failure, since it is the one you will apply
|
|
90
|
+
most:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Before: three spans, one size, no first read
|
|
94
|
+
<div className="flex justify-between text-sm">
|
|
95
|
+
<span>{item.name}</span>
|
|
96
|
+
<span>{item.amount}</span>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
// After: name leads, value anchors, metadata recedes
|
|
100
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
101
|
+
<div className="min-w-0 flex-1">
|
|
102
|
+
<p className="truncate text-base font-medium">{item.name}</p>
|
|
103
|
+
<p className="text-muted-foreground text-sm">{item.detail}</p>
|
|
104
|
+
</div>
|
|
105
|
+
<span className="shrink-0 text-base font-semibold tabular-nums">
|
|
106
|
+
{formatCurrency(item.amount)}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Full before→after gallery — hero KPI, status treatment, sizing floors, inner
|
|
112
|
+
scroll, row actions, designed empty state — in `references/patterns.md`. Read
|
|
113
|
+
it before rewriting any widget body.
|
|
114
|
+
|
|
115
|
+
## The marketplace tile
|
|
116
|
+
|
|
117
|
+
"The tile looks bad" is usually not a styling bug — it is missing branding.
|
|
118
|
+
The tile is `components.marketplaceTile` in the definition, typically built on
|
|
119
|
+
`BaseMarketplaceTile` from `@ekanos/sdk/components`, and it renders from the
|
|
120
|
+
props you pass: `name`, `summary` (1–2 sentences shown on the card),
|
|
121
|
+
`brandName`, `logoPath`, `categories`, `productColor`, `ctaText`. Omit
|
|
122
|
+
`logoPath` and the card falls back to a single-letter monogram; omit `summary`
|
|
123
|
+
and the body sits blank next to its neighbours. Check the props before
|
|
124
|
+
touching CSS.
|
|
125
|
+
|
|
126
|
+
Review it at `http://localhost:3100/<slug>/tile` — the harness renders it
|
|
127
|
+
inside the real catalogue grid with two inert neighbour cards, which is the
|
|
128
|
+
only honest test: does the title wrap at column width, does the summary clamp
|
|
129
|
+
like the neighbours', is the card the same height? (Register the component as
|
|
130
|
+
`tile:` in `harness.config.ts` if the surface says none is registered.) The
|
|
131
|
+
harness also notes this surface is lightly used by the live marketplace today
|
|
132
|
+
— keep it correct and branded, don't gold-plate it.
|
|
133
|
+
|
|
134
|
+
## Reporting
|
|
135
|
+
|
|
136
|
+
Finish with a per-widget summary: the key question you assigned it, the
|
|
137
|
+
findings fixed (worst-first), anything deliberately left, and confirmation the
|
|
138
|
+
user saw the result. Then run the standard gate: `npx tsc --noEmit` and
|
|
139
|
+
`npx ekanos validate` clean.
|