@ekanos/cli 0.1.1 → 0.1.3
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 +35 -4
- package/dist/auth/session.d.ts +25 -7
- package/dist/auth/session.js +88 -15
- package/dist/auth/session.js.map +1 -1
- package/dist/banner.d.ts +21 -0
- package/dist/banner.js +89 -0
- package/dist/banner.js.map +1 -0
- package/dist/commands/init.d.ts +8 -1
- package/dist/commands/init.js +19 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +16 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +3 -1
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.js +85 -27
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +28 -0
- package/dist/commands/status.js +146 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +3 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.js +5 -0
- package/dist/context.js.map +1 -1
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/project.d.ts +11 -0
- package/dist/project.js +28 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +45 -0
- package/dist/publish-api.js +113 -0
- package/dist/publish-api.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +247 -0
- package/templates/CLAUDE.md.tmpl +6 -0
- package/templates/claude-skill.md.tmpl +53 -0
package/dist/publish-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/submissions/route.ts`.\n * As with the auth routes (see `auth/fusion-api.ts`), the two halves are\n * coupled only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Fusion source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no dev seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Fusion route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `dev seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/submissions/route.ts`.\n * As with the auth routes (see `auth/fusion-api.ts`), the two halves are\n * coupled only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Fusion source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no dev seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Fusion page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id> [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced in\n * `apps/web/app/api/partner/submissions/_lib/submission-validation.ts` against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Fusion route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `dev seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running a Fusion build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n): Promise<ListResult> {\n const response = await request(\n `${host}/api/partner/submissions?source=${encodeURIComponent(source)}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login --host ${host}\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Fusion operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekanos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
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.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ekanos/integration-schema": "0.1.
|
|
35
|
+
"@ekanos/integration-schema": "0.1.3",
|
|
36
36
|
"esbuild": "0.28.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# __DISPLAY_NAME__ — an Ekanos integration
|
|
2
|
+
|
|
3
|
+
This project is a **Fusion integration** built with `@ekanos/sdk`. This file is
|
|
4
|
+
the authoring contract: everything an agent needs to build, test, and publish
|
|
5
|
+
it without fetching external documentation. The `@ekanos/sdk` README (in
|
|
6
|
+
`node_modules/@ekanos/sdk/README.md` after install) is the long-form version;
|
|
7
|
+
where the two disagree, the README wins.
|
|
8
|
+
|
|
9
|
+
## What an integration is
|
|
10
|
+
|
|
11
|
+
One call to `defineIntegration()` in `src/integration.ts`, exported under the
|
|
12
|
+
exact name `integration`. It validates at import time and returns a deep-frozen
|
|
13
|
+
definition; the host re-parses the same object against the same schema when it
|
|
14
|
+
registers the package, so a definition that loads locally is one the platform
|
|
15
|
+
accepts.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineIntegration } from '@ekanos/sdk/integration';
|
|
19
|
+
|
|
20
|
+
export const integration = defineIntegration<MyStorage>({
|
|
21
|
+
slug: '__SLUG__', // must match ekanos.json's "slug"
|
|
22
|
+
name: '__DISPLAY_NAME__',
|
|
23
|
+
description: '…',
|
|
24
|
+
version: '0.1.0', // semver — bump it before every publish
|
|
25
|
+
|
|
26
|
+
capabilities: [ /* copy for the detail page */ ],
|
|
27
|
+
permissions: [ /* disclosure of what you read/write */ ],
|
|
28
|
+
components: { widgets: [ /* … */ ] },
|
|
29
|
+
tools: [ /* assistant-callable functions */ ],
|
|
30
|
+
webhooks: [ /* inbound deliveries */ ],
|
|
31
|
+
schedules: [ /* cron runs */ ],
|
|
32
|
+
storage: { account: { /* key → zod schema */ } },
|
|
33
|
+
egress: ['https://api.example.com'],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Only `slug`, `name`, `description`, `version` are required. The schema is
|
|
38
|
+
**strict everywhere**: an unrecognized key is a hard error, and host-assigned
|
|
39
|
+
fields (`productId`, `kind`, trust tier, per-tool `effect`/`sensitivity`) are
|
|
40
|
+
structurally un-settable — the closest you get is the `proposes` block.
|
|
41
|
+
|
|
42
|
+
`ekanos.json` at the project root maps the slug to the entry module. The slug
|
|
43
|
+
lives in both files; `ekanos validate` reports a finding if they disagree.
|
|
44
|
+
|
|
45
|
+
## The capability context (`ctx`) — the ONLY platform surface
|
|
46
|
+
|
|
47
|
+
Every server-side handler (tool `run`, webhook `handler`, schedule `handler`,
|
|
48
|
+
`oauth.onTokens`) receives one `ctx` object, already scoped to
|
|
49
|
+
`{account, integration}` before your code runs.
|
|
50
|
+
|
|
51
|
+
**Hard rule: NEVER use global `fetch`, `process.env`, or `node:fs` in
|
|
52
|
+
integration code.** There is no ambient platform access — network goes through
|
|
53
|
+
`ctx.fetch` (refused synchronously unless the origin is in your `egress`
|
|
54
|
+
list), credentials through `ctx.secrets`, state through `ctx.storage`. Code
|
|
55
|
+
that reaches around `ctx` fails review and breaks under the mock.
|
|
56
|
+
|
|
57
|
+
| Surface | What it is |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `ctx.storage.account` | per-account state, schema-validated (`get`/`set`/`delete`, all async) |
|
|
60
|
+
| `ctx.storage.user` | per-user state — **throws for machine actors** (workflows) |
|
|
61
|
+
| `ctx.secrets` | `get(name)`, `names()`, `set(name, value)` — `set` writes the account tier only |
|
|
62
|
+
| `ctx.fetch` | `fetch` constrained to your `egress` allowlist, redirects included |
|
|
63
|
+
| `ctx.logger` | `debug`/`info`/`warn`/`error`, each `(contextObject, message)` — context FIRST |
|
|
64
|
+
| `ctx.actor` | `{ kind: 'user', userId }` or `{ kind: 'machine', tokenId, createdBy }` |
|
|
65
|
+
|
|
66
|
+
Read-only identity facts: `ctx.accountId`, `ctx.accountSlug`, `ctx.userId`
|
|
67
|
+
(null for machine actors), `ctx.sourceId`, `ctx.integration`
|
|
68
|
+
(`{ slug, productId }`), `ctx.timezone`.
|
|
69
|
+
|
|
70
|
+
### Storage
|
|
71
|
+
|
|
72
|
+
Declare every key up front with a zod schema; an undeclared key cannot be
|
|
73
|
+
read, written, or deleted (`StorageValidationError` at runtime, a type error
|
|
74
|
+
when the schema map is statically known).
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import type { StorageSchemas } from '@ekanos/sdk';
|
|
78
|
+
|
|
79
|
+
export const myStorage = {
|
|
80
|
+
account: {
|
|
81
|
+
'cache/latest': z.object({ fetchedAt: z.string(), body: z.unknown() }),
|
|
82
|
+
// Descriptor form opts a key in to the browser-readable storage route.
|
|
83
|
+
'settings/main': { schema: SettingsSchema, clientReadable: true },
|
|
84
|
+
},
|
|
85
|
+
user: { 'preferences/units': z.enum(['metric', 'imperial']) },
|
|
86
|
+
} satisfies StorageSchemas;
|
|
87
|
+
|
|
88
|
+
export type MyStorage = typeof myStorage;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pass the type as the generic (`defineIntegration<MyStorage>`) and every
|
|
92
|
+
handler's `ctx.storage` is typed to it.
|
|
93
|
+
|
|
94
|
+
Key names are `[a-z0-9_-]+` with at most one `/` segment, and the first
|
|
95
|
+
segment is the **data type**, drawn from a closed vocabulary. The canonical
|
|
96
|
+
lists are exported from `@ekanos/integration-schema` as
|
|
97
|
+
`ACCOUNT_STORAGE_DATA_TYPES` and `USER_STORAGE_DATA_TYPES` — import and check
|
|
98
|
+
them rather than guessing; a key outside them fails `defineIntegration()`.
|
|
99
|
+
`secret` is reserved (`RESERVED_STORAGE_DATA_TYPES`): credentials go through
|
|
100
|
+
`ctx.secrets`, never `ctx.storage`.
|
|
101
|
+
|
|
102
|
+
`get` returns `{ data, externalId, expiresAt, updatedAt } | null` (null for a
|
|
103
|
+
missing key and for an entry past its `expiresAt`). Evolve a schema with a
|
|
104
|
+
versioned key or a `z.union` — a stored value that no longer parses throws on
|
|
105
|
+
read.
|
|
106
|
+
|
|
107
|
+
### Secrets and the activation contract
|
|
108
|
+
|
|
109
|
+
There is **no `secrets` block** in the definition. Secrets are *named*, not
|
|
110
|
+
declared, and the activation form is where most names come from:
|
|
111
|
+
|
|
112
|
+
**Every string-valued field of the `activationData` a user submits becomes
|
|
113
|
+
readable as `ctx.secrets.get('<that exact field name>')`.** No prefixing, no
|
|
114
|
+
transformation. Renaming a form field renames the secret and breaks every
|
|
115
|
+
handler reading it, so keep each name in one exported constant
|
|
116
|
+
(`src/config.ts`) imported by both the form and the handlers.
|
|
117
|
+
|
|
118
|
+
Secret values go to the platform vault — **never** write a credential into
|
|
119
|
+
`ctx.storage`, `configData`, logs, or a tool result. `ctx.secrets.set()` is
|
|
120
|
+
for token rotation (OAuth refresh) and writes the account tier only; a name
|
|
121
|
+
that resolves from an admin-issued tier throws `SecretAccessError`.
|
|
122
|
+
|
|
123
|
+
The activation form itself is a client component declared at
|
|
124
|
+
`components.activationForm`, built with `react-hook-form` + a zod resolver and
|
|
125
|
+
rendered inside `BaseActivationForm` from `@ekanos/sdk/components` (pass the
|
|
126
|
+
`inline` prop through). `useActivateIntegration()` from `@ekanos/sdk/hooks`
|
|
127
|
+
posts the activation. Omit the form and the host renders a generic one.
|
|
128
|
+
|
|
129
|
+
### Tools
|
|
130
|
+
|
|
131
|
+
Assistant-callable functions. `parameters` is a narrow JSON-Schema slice: the
|
|
132
|
+
top level accepts exactly `type: 'object'`, `properties`, `required`,
|
|
133
|
+
`additionalProperties` — nothing else. Return a plain JSON value; return
|
|
134
|
+
`{ error: '…' }` for an expected failure the model should explain; **throw**
|
|
135
|
+
for a genuine fault. Tool names are `^[a-z][a-z0-9_]*$` and are namespaced by
|
|
136
|
+
your slug at discovery.
|
|
137
|
+
|
|
138
|
+
### Webhooks and schedules
|
|
139
|
+
|
|
140
|
+
Declared with kebab-case ids. The transport validates `payloadSchema` before
|
|
141
|
+
your handler runs and owns signature verification (`signature: { header,
|
|
142
|
+
secretName }` or `'none'`) — never verify a signature in the handler.
|
|
143
|
+
|
|
144
|
+
Webhook results: `{ status: 'processed' }` (done), `{ status: 'ignored' }`
|
|
145
|
+
(valid but irrelevant — the sender must not retry), or **throw** (retryable
|
|
146
|
+
failure). Schedule results: `{ status: 'completed' }` / `{ status: 'skipped' }`
|
|
147
|
+
/ throw. Handlers must be safe under redelivery and under a manual "Run now"
|
|
148
|
+
(`invocation.trigger` distinguishes them). Cron is 5-field numeric only — no
|
|
149
|
+
names, no `@daily`, no seconds.
|
|
150
|
+
|
|
151
|
+
## Widgets
|
|
152
|
+
|
|
153
|
+
Dashboard widgets are client components (`'use client'`) declared at
|
|
154
|
+
`components.widgets`. Widget ids are **globally** unique — always prefix with
|
|
155
|
+
your slug (`__SLUG__-<name>`). `widgetState` (usually `'active'`), plus
|
|
156
|
+
`layouts` per breakpoint, seed the dashboard placement.
|
|
157
|
+
|
|
158
|
+
Render through the `Widget.*` compound API and `WidgetContext` from
|
|
159
|
+
`@ekanos/sdk/components`; wrap in `WidgetPreviewProvider` to render outside
|
|
160
|
+
the host. There is **no client-side `ctx.fetch`** — a widget reads declared
|
|
161
|
+
`clientReadable` account-scope storage back with `fetchIntegrationStorage()`
|
|
162
|
+
from `@ekanos/sdk/hooks`, and anything else needs your own seam.
|
|
163
|
+
|
|
164
|
+
Build widget UI from `@ekanos/ui` — the published slice of the host design
|
|
165
|
+
system. Available modules (import individually, e.g. `@ekanos/ui/button`):
|
|
166
|
+
`alert`, `badge`, `button`, `card`, `dialog`, `dropdown-menu`, `form`, `icon`,
|
|
167
|
+
`if`, `input`, `label`, `select`, `separator`, `skeleton`, `spinner`,
|
|
168
|
+
`switch`, `textarea`, `tooltip`, `trans`, `utils` (the `cn()` helper),
|
|
169
|
+
`ai-prompt-input`, plus the stylesheets `styles.css` / `tokens.css` /
|
|
170
|
+
`theme.css` / `base.css`. Icons are Font Awesome glyph names via
|
|
171
|
+
`@ekanos/ui/icon`; outside a host that loads Font Awesome they render as
|
|
172
|
+
nothing — expected, not a bug.
|
|
173
|
+
|
|
174
|
+
## Testing
|
|
175
|
+
|
|
176
|
+
`@ekanos/sdk/testing` builds the same context production uses, in memory —
|
|
177
|
+
same storage schema validation, same egress allowlist, same secret tiers:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { createMockContext, invokeSchedule, invokeWebhook } from '@ekanos/sdk/testing';
|
|
181
|
+
|
|
182
|
+
const ctx = createMockContext({
|
|
183
|
+
integration: { slug: '__SLUG__' }, // the options object is REQUIRED
|
|
184
|
+
storageSchemas: myStorage,
|
|
185
|
+
egress: integration.egress,
|
|
186
|
+
secrets: { account: { api_key: 'test-key' } },
|
|
187
|
+
fetchHandlers: [
|
|
188
|
+
{ match: 'https://api.example.com', respond: () => Response.json({ ok: true }) },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Inspect what happened:
|
|
193
|
+
ctx.fetchCalls; // { url, init, denied }[]
|
|
194
|
+
ctx.logs; // { level, context, message }[]
|
|
195
|
+
ctx.dumpStorage(); // { account, user }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`invokeWebhook(integration, '<webhook-id>', payload, options)` and
|
|
199
|
+
`invokeSchedule(integration, '<schedule-id>', options)` run declared handlers
|
|
200
|
+
with transport semantics (payload validated first, signature skip logged) and
|
|
201
|
+
derive the context from the definition. Reuse one context across invocations
|
|
202
|
+
via `options.context` to accumulate state.
|
|
203
|
+
|
|
204
|
+
## The iteration loop (`npx ekanos <verb>`)
|
|
205
|
+
|
|
206
|
+
Every verb speaks a JSON envelope on stdout with `--json` (auto-on when piped):
|
|
207
|
+
`{ ok: true, data }` or `{ ok: false, error: { code, message, hint }, data? }`
|
|
208
|
+
— exactly one JSON object per process, everything else on stderr. Every error
|
|
209
|
+
carries an imperative `hint`; treat hints as remediation instructions.
|
|
210
|
+
|
|
211
|
+
| Verb | Purpose |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `ekanos validate` | Parse ekanos.json + load the definition + run every finding pass. Findings land in `data.findings`, each with severity and a hint |
|
|
214
|
+
| `ekanos dev` | Scaffold `.ekanos/harness` and run the integration in a local Next dev shell (loopback by default) |
|
|
215
|
+
| `ekanos test` | Run this project's test script through its package manager |
|
|
216
|
+
| `ekanos login --host <url>` | Device-flow login to a Fusion deployment (stores the host for later verbs) |
|
|
217
|
+
| `ekanos status` | Login state for the resolved host + this project's submissions |
|
|
218
|
+
| `ekanos publish` | Validate, pack (whitelist: ekanos.json, package.json, README.md, src/), and submit. Refuses on any error-severity finding |
|
|
219
|
+
|
|
220
|
+
Host resolution for `logout`/`whoami`/`status`/`publish`: `--host` flag →
|
|
221
|
+
`EKANOS_HOST` → the `host` field in ekanos.json → the sole stored login. The
|
|
222
|
+
first successful `publish` saves `host` and `source` into ekanos.json.
|
|
223
|
+
`login` is the exception — it never reads ekanos.json for a host (`--host` →
|
|
224
|
+
`EKANOS_HOST` → the sole stored login only), since it is the verb that
|
|
225
|
+
creates credentials and a committed file must not be able to redirect it.
|
|
226
|
+
|
|
227
|
+
Exit codes (frozen contract — branch on these): `0` ok, `1` internal, `2`
|
|
228
|
+
usage, `3` validation, `4` auth required (run `ekanos login`), `5` forbidden,
|
|
229
|
+
`6` not found, `7` invalid state (e.g. version already submitted — bump
|
|
230
|
+
`package.json#version`), `8` network (retry, do NOT re-login), `9`
|
|
231
|
+
precondition failed, `10` publish gate failed (fix `data.findings`).
|
|
232
|
+
|
|
233
|
+
The loop: `ekanos dev` → edit → `npx tsc --noEmit` → `ekanos validate` →
|
|
234
|
+
`ekanos test` → `ekanos publish`.
|
|
235
|
+
|
|
236
|
+
## Verify your work
|
|
237
|
+
|
|
238
|
+
Before claiming anything is done:
|
|
239
|
+
|
|
240
|
+
1. `npm run typecheck` (or `npx tsc --noEmit`) — zero errors.
|
|
241
|
+
2. `npx ekanos validate --json` — zero error-severity findings; fix by
|
|
242
|
+
following each finding's `hint` verbatim.
|
|
243
|
+
3. `npm test` — green, and confirm vitest actually ran your test files (the
|
|
244
|
+
scaffolded vitest config's `server.deps.inline` block is REQUIRED for SDK
|
|
245
|
+
imports to load; without it vitest reports green over zero tests).
|
|
246
|
+
4. `ekanos publish` re-runs the same findings pass and refuses on errors, so a
|
|
247
|
+
clean validate is what makes a publish land.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# __DISPLAY_NAME__
|
|
2
|
+
|
|
3
|
+
This project is an Ekanos integration for the Fusion platform — read
|
|
4
|
+
[AGENTS.md](./AGENTS.md) for the full authoring contract before changing code.
|
|
5
|
+
The toolchain is `npx ekanos <verb>` (init, validate, dev, test, login,
|
|
6
|
+
status, publish); every verb supports `--json` and a stable exit-code taxonomy.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ekanos
|
|
3
|
+
description: Build, iterate on, validate, and publish this Fusion integration with the ekanos CLI. Use for any change to the integration definition, widgets, tools, webhooks, schedules, or tests, and for publishing to a Fusion host.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Working on this Ekanos integration
|
|
7
|
+
|
|
8
|
+
The authoring contract — what an integration is, the `ctx` API, storage/secret
|
|
9
|
+
rules, widget and testing patterns — lives in **AGENTS.md** at the project
|
|
10
|
+
root. Read it before writing integration code. The definition is one
|
|
11
|
+
`defineIntegration()` in `src/integration.ts`, exported as `integration`.
|
|
12
|
+
|
|
13
|
+
## The loop
|
|
14
|
+
|
|
15
|
+
1. `npx ekanos dev` — local harness (only when you need to see UI).
|
|
16
|
+
2. Edit code. Never use global `fetch`/`process.env`/`fs` in integration
|
|
17
|
+
code — everything goes through `ctx`.
|
|
18
|
+
3. `npx tsc --noEmit` — types clean.
|
|
19
|
+
4. `npx ekanos validate --json` — findings clean.
|
|
20
|
+
5. `npm test` — green.
|
|
21
|
+
6. `npx ekanos publish` — submits to the Fusion host (refuses while any
|
|
22
|
+
error-severity finding remains).
|
|
23
|
+
|
|
24
|
+
## Reading CLI output
|
|
25
|
+
|
|
26
|
+
Every verb emits ONE JSON envelope on stdout when piped (or with `--json`):
|
|
27
|
+
`{ ok: true, data }` or `{ ok: false, error: { code, message, hint }, data? }`.
|
|
28
|
+
Progress and prose go to stderr.
|
|
29
|
+
|
|
30
|
+
**Every error and every validation finding carries a `hint` — treat hints as
|
|
31
|
+
remediation instructions and follow them verbatim.** `ekanos validate` puts
|
|
32
|
+
findings in `data.findings`; `ekanos publish` refuses with exit 10 and the
|
|
33
|
+
same findings in `data.findings`.
|
|
34
|
+
|
|
35
|
+
Exit codes to branch on: `0` ok · `2` usage · `3` validation · `4` run
|
|
36
|
+
`ekanos login` · `7` version already submitted (bump `package.json#version`) ·
|
|
37
|
+
`8` network (retry — do NOT re-login) · `9` precondition · `10` fix findings,
|
|
38
|
+
publish again.
|
|
39
|
+
|
|
40
|
+
## Hosts and sessions
|
|
41
|
+
|
|
42
|
+
`ekanos login --host <url>` stores a session; after that, `logout`/`whoami`/
|
|
43
|
+
`status`/`publish` resolve the host from `--host` → `EKANOS_HOST` →
|
|
44
|
+
ekanos.json's `host` field → the sole stored login. `login` itself never reads
|
|
45
|
+
ekanos.json for a host — only `--host`, `EKANOS_HOST`, or the sole stored
|
|
46
|
+
login — since it is the verb that creates credentials. `ekanos status` shows
|
|
47
|
+
who you are logged in as and what this project has submitted. The first
|
|
48
|
+
successful publish saves `host` and `source` into ekanos.json.
|
|
49
|
+
|
|
50
|
+
## Before claiming done
|
|
51
|
+
|
|
52
|
+
Run steps 3-5 above and confirm all three are clean — a publish is only as
|
|
53
|
+
good as the validate that precedes it.
|