@holokai/neuron-sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,7 @@ pnpm add @holokai/neuron-sdk
21
21
 
22
22
  Runs in **Node 20+** (uses the global `fetch` and `ReadableStream`) and **modern browsers** (Chromium, Firefox, Safari latest). The package has no `node:*` imports — Holokai Desktop / Electron and the in-browser neuron-tester both consume the same SDK build.
23
23
 
24
- > **Gateway compatibility (protocol 1.1.0, HOL-2926).** Since protocol
24
+ > **Gateway compatibility (protocol 1.2.0; HOL-2926, HOL-4326).** Since protocol
25
25
  > 1.1.0 the SDK advertises each capability's contract (`description` +
26
26
  > input/output JSON Schemas) on the register frame. The gateway validates
27
27
  > register frames **strictly**, so a gateway older than HOL-2926 rejects
@@ -114,6 +114,29 @@ Examples:
114
114
  - `desktop/mcp/drive.read` — user's Google Drive, scoped to that user
115
115
  - `org/01HBQ8ZK/custom-tool` — org-specific capability
116
116
 
117
+ ### Composing capabilities (`composes`)
118
+
119
+ A capability namespaced `skill/<name>/<id>` may declare `composes`: the other
120
+ capability types it draws on to carry out one recurring job. Set it on
121
+ `defineCapability(...)`, on `neuron.handle(def, opts, fn)`'s `opts`, or on the
122
+ legacy registration object — it is only valid on a `skill/*` type, must name
123
+ 1 to 64 distinct capability types, and none of them may itself be `skill/*`.
124
+
125
+ ```ts
126
+ const weeklyReport = defineCapability({
127
+ type: 'skill/weekly-report/01',
128
+ description: 'Pulls last week\'s issues and writes a summary.',
129
+ input: z.object({}),
130
+ output: z.object({ summary: z.string() }),
131
+ composes: ['mcp/linear/list_issues', 'holo/37bfe871'],
132
+ });
133
+ ```
134
+
135
+ `composes` is carried on the register frame's capability object (gateway
136
+ protocol 1.2.0, HOL-4326) — **the gateway must be running 1.2.0 or later
137
+ before deploying an SDK version that sends it**, or the gateway rejects the
138
+ register frame outright.
139
+
117
140
  ### Namespace authorization
118
141
 
119
142
  The gateway enforces which identities may advertise which namespaces:
@@ -843,69 +866,75 @@ The `auth` callback isn't returning a valid token. If using a refresh flow, ensu
843
866
 
844
867
  ## Releasing (maintainers)
845
868
 
846
- `@holokai/neuron-sdk` is published to public npm. Releases are cut from a
847
- monorepo-scoped git tag, and CI does the publish — there is no npm token on
848
- anyone's laptop and none stored in the repo.
869
+ `@holokai/neuron-sdk` is published to public npm by BigBrain's Release
870
+ workflow (`.github/workflows/release.yml`). There is no npm token on anyone's
871
+ laptop and none stored in the repo, and pushing a tag publishes nothing.
872
+
873
+ ### Pick the version
874
+
875
+ Follow semver:
876
+
877
+ - Pilot phase, we are in **`0.x`**. Because the protocol is still absorbing
878
+ pilot-developer feedback, a **breaking** change bumps the **minor**
879
+ (`0.2.0` to `0.3.0`); a backward-compatible change or fix bumps the
880
+ **patch** (`0.2.0` to `0.2.1`). This is the standard `0.x` convention and
881
+ is what buys us room to break things on feedback without a major bump.
882
+ - Hold **`1.0.0`** until the public API has survived pilot feedback and we're
883
+ ready to commit to "breaking changes bump the major." Jumping to `1.0.0`
884
+ is a stability promise, not a milestone. Don't spend it early.
849
885
 
850
886
  ### Cut a release
851
887
 
852
- 1. **Bump the version** in `packages/neuron-sdk/package.json`
853
- (`version`). Follow semver:
854
- - Pilot phase, we are in **`0.x`**. Because the protocol is still absorbing
855
- pilot-developer feedback, a **breaking** change bumps the **minor**
856
- (`0.2.0` → `0.3.0`); a backward-compatible change or fix bumps the
857
- **patch** (`0.2.0` → `0.2.1`). This is the standard `0.x` convention and
858
- is what buys us room to break things on feedback without a major bump.
859
- - Hold **`1.0.0`** until the public API has survived pilot feedback and we're
860
- ready to commit to "breaking changes bump the major." Jumping to `1.0.0`
861
- is a stability promise, not a milestone — don't spend it early.
862
- 2. **Commit** the bump (reference the Linear ticket) and merge to `main`.
863
- 3. **Tag and push** from `main`:
864
- ```sh
865
- git tag neuron-sdk-v0.2.0 # must equal the package.json version
866
- git push origin neuron-sdk-v0.2.0
867
- ```
868
- The tag is prefixed `neuron-sdk-v` (the Python SDK uses
869
- `neuron-sdk-python-v`) so the two never collide.
870
- 4. **`publish-neuron-sdk.yml`** fires on the tag: it asserts the tag version
871
- matches `package.json`, runs the `prepublish` gate (`build` + `test`), and
872
- publishes to npm. The `npm` GitHub Environment gates the publish behind a
873
- **required-reviewer approval** — approve the run under Actions, and on success
874
- the new version is live at
888
+ 1. Open **Actions, Release, Run workflow** on branch `main`.
889
+ 2. Fill **neuron-sdk** with the new version (`X.Y.Z`). Leave the other lines
890
+ (base, embedded, synapse) empty unless they are being released too; an
891
+ empty line is not touched. Tick **plan only** to read the plan without
892
+ cutting anything.
893
+ 3. The plan job checks the version (shaped `X.Y.Z`, not older than
894
+ `packages/neuron-sdk/package.json`, tag `neuron-sdk-vX.Y.Z` not taken) and
895
+ writes the plan to the run summary.
896
+ 4. A reviewer of the `release` environment approves the run. That is the only
897
+ approval.
898
+ 5. The cut writes the version into `packages/neuron-sdk/package.json`,
899
+ commits it to `main`, tags `neuron-sdk-vX.Y.Z` and pushes both. If the file
900
+ already carries the version (a bump merged by pull request), it only tags.
901
+ 6. From the commit the cut pushed, the run builds and tests the SDK (the
902
+ `prepublish` turbo task), packs it, and publishes the tarball to npm from a
903
+ GitHub-hosted runner. On success the new version is live at
875
904
  [`@holokai/neuron-sdk`](https://www.npmjs.com/package/@holokai/neuron-sdk).
876
905
 
877
- > No `--provenance`: bigbrain is a private repo, so npm's provenance
878
- > attestation (a public transparency-log link back to the source) adds little
879
- > and has historically errored from private repos. Trusted-publishing auth is
880
- > unaffected. Add the flag back if bigbrain ever goes public.
906
+ The tag is prefixed `neuron-sdk-v` (the Python SDK uses `neuron-sdk-python-v`)
907
+ so the two never collide. The workflow file's header comments describe every
908
+ job and how to recover a failed run.
881
909
 
882
- The tag→`package.json` assertion means a mismatched tag fails **before** the
883
- registry is touched — npm refuses to re-upload an existing version, so there's
884
- no half-published state to clean up.
910
+ > No `--provenance`: bigbrain is a private repo, so npm's provenance
911
+ > attestation (a public transparency-log link back to the source) adds little
912
+ > and has historically errored from private repos. Trusted-publishing auth is
913
+ > unaffected. Add the flag back if bigbrain ever goes public.
885
914
 
886
915
  ### Publish auth: npm Trusted Publishing (one-time setup)
887
916
 
888
- CI authenticates to npm via **OIDC trusted publishing** — GitHub Actions
917
+ CI authenticates to npm via **OIDC trusted publishing**: GitHub Actions
889
918
  exchanges a short-lived OIDC token for publish rights, so no long-lived
890
- `NPM_TOKEN` exists to leak. This mirrors the Python SDK's PyPI trusted
891
- publishing. It requires two one-time setups:
919
+ `NPM_TOKEN` exists to leak. It requires two one-time setups:
892
920
 
893
921
  1. **On npmjs.com** (a package admin, once): open the
894
922
  [`@holokai/neuron-sdk`](https://www.npmjs.com/package/@holokai/neuron-sdk)
895
- package → **Settings → Trusted Publisher → GitHub Actions**, and register:
923
+ package, **Settings, Trusted Publisher, GitHub Actions**, and register:
896
924
  - Organization / repository: `holok-ai/bigbrain`
897
- - Workflow filename: `publish-neuron-sdk.yml`
925
+ - Workflow filename: `release.yml`
898
926
  - Environment: `npm`
899
- 2. **In this repo** (once): create the **`npm`** GitHub Environment
900
- (Settings → Environments). Add reviewers/branch protection here if you want a
901
- manual approval gate before each publish.
927
+ 2. **In this repo** (once): the **`npm`** GitHub Environment (Settings,
928
+ Environments) with no required reviewer, because the Release run's one
929
+ approval is on its cut, and a deployment branch rule of `main` only, so a
930
+ copy of `release.yml` on another branch cannot publish to npm.
902
931
 
903
932
  Until the trusted publisher is registered, the `npm publish` step fails with an
904
- auth error — that's the expected "not set up yet" signal, not a code bug.
933
+ auth error. That is the expected "not set up yet" signal, not a code bug.
905
934
 
906
935
  ### Manual publish (fallback / first bootstrap)
907
936
 
908
- The tag flow is the norm. If you ever need to publish by hand (e.g. to bootstrap
937
+ The Release workflow is the norm. If you ever need to publish by hand (e.g. to bootstrap
909
938
  the very first version before trusted publishing is configured):
910
939
 
911
940
  ```sh
@@ -915,6 +944,6 @@ pnpm run publish:npm # runs the prepublishOnly build+test gate, then publi
915
944
  ```
916
945
 
917
946
  This uses your own `npm login` credentials and skips the CI approval gate —
918
- prefer the tag flow for anything a developer will consume.
947
+ prefer the Release workflow for anything a developer will consume.
919
948
 
920
949
  <!-- ci: force full build on the self-hosted runner (HOL-2028 verify); safe to remove -->
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-Ckcwj-Do.mjs';
2
- export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-Ckcwj-Do.mjs';
1
+ import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-Va7tTRVO.mjs';
2
+ export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-Va7tTRVO.mjs';
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import { z } from 'zod';
5
5
  import { ErrorObject } from 'ajv';
@@ -48,6 +48,12 @@ interface CapabilityDefInit<TIn extends z.ZodTypeAny, TOut extends z.ZodTypeAny>
48
48
  * Bounded `[1_000, 3_600_000]` (1s..1h) by `CapabilitySchema`.
49
49
  */
50
50
  leaseTtlMs?: number;
51
+ /**
52
+ * Capability types this `skill/*` capability composes (HOL-4326). Only
53
+ * valid when `type` is namespaced `skill/*` — enforced by
54
+ * `CapabilitySchema` on the gateway.
55
+ */
56
+ composes?: readonly string[];
51
57
  }
52
58
  /**
53
59
  * A typed capability definition. The phantom `_input` / `_output` fields
@@ -68,6 +74,8 @@ interface CapabilityDef<TIn = unknown, TOut = unknown> {
68
74
  readonly concurrency?: number;
69
75
  /** See {@link CapabilityDefInit.leaseTtlMs}. */
70
76
  readonly leaseTtlMs?: number;
77
+ /** See {@link CapabilityDefInit.composes}. */
78
+ readonly composes?: readonly string[];
71
79
  /** Original zod schemas — kept for runtime validation in handlers if needed. */
72
80
  readonly _zod: {
73
81
  input: z.ZodType<TIn>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-Ckcwj-Do.js';
2
- export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-Ckcwj-Do.js';
1
+ import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-Va7tTRVO.js';
2
+ export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-Va7tTRVO.js';
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import { z } from 'zod';
5
5
  import { ErrorObject } from 'ajv';
@@ -48,6 +48,12 @@ interface CapabilityDefInit<TIn extends z.ZodTypeAny, TOut extends z.ZodTypeAny>
48
48
  * Bounded `[1_000, 3_600_000]` (1s..1h) by `CapabilitySchema`.
49
49
  */
50
50
  leaseTtlMs?: number;
51
+ /**
52
+ * Capability types this `skill/*` capability composes (HOL-4326). Only
53
+ * valid when `type` is namespaced `skill/*` — enforced by
54
+ * `CapabilitySchema` on the gateway.
55
+ */
56
+ composes?: readonly string[];
51
57
  }
52
58
  /**
53
59
  * A typed capability definition. The phantom `_input` / `_output` fields
@@ -68,6 +74,8 @@ interface CapabilityDef<TIn = unknown, TOut = unknown> {
68
74
  readonly concurrency?: number;
69
75
  /** See {@link CapabilityDefInit.leaseTtlMs}. */
70
76
  readonly leaseTtlMs?: number;
77
+ /** See {@link CapabilityDefInit.composes}. */
78
+ readonly composes?: readonly string[];
71
79
  /** Original zod schemas — kept for runtime validation in handlers if needed. */
72
80
  readonly _zod: {
73
81
  input: z.ZodType<TIn>;
package/dist/index.js CHANGED
@@ -17,6 +17,11 @@ var CapabilityTypeSchema = zod.z.string().min(1).regex(
17
17
  "capability type must be namespaced: {owner}/{package}[/{name}][.{variant}]"
18
18
  );
19
19
  var UserIdSchema = zod.z.string().min(1);
20
+ var SKILL_CAPABILITY_PREFIX = "skill/";
21
+ function isSkillCapabilityType(type) {
22
+ return type.startsWith(SKILL_CAPABILITY_PREFIX);
23
+ }
24
+ var MAX_COMPOSED_CAPABILITIES = 64;
20
25
  var JsonSchemaSchema = zod.z.unknown();
21
26
  var CapabilityScopeSchema = zod.z.union([
22
27
  zod.z.literal("any"),
@@ -69,8 +74,29 @@ var CapabilitySchema = zod.z.object({
69
74
  * heartbeat budget should be the limiter, not a single
70
75
  * capability).
71
76
  */
72
- leaseTtlMs: zod.z.number().int().min(1e3).max(60 * 60 * 1e3).optional()
73
- }).strict();
77
+ leaseTtlMs: zod.z.number().int().min(1e3).max(60 * 60 * 1e3).optional(),
78
+ /**
79
+ * Capability types this `skill/*` capability composes (HOL-4326).
80
+ * Only valid on a `skill/*` type — enforced below, since a non-skill
81
+ * type carrying `composes` is a modeling error the gateway should
82
+ * reject rather than silently ignore.
83
+ */
84
+ composes: zod.z.array(CapabilityTypeSchema).min(1).max(MAX_COMPOSED_CAPABILITIES).refine(
85
+ (types) => new Set(types).size === types.length,
86
+ "composes must not contain duplicate capability types"
87
+ ).refine(
88
+ (types) => types.every((t) => !isSkillCapabilityType(t)),
89
+ "composes must not contain skill/* capability types"
90
+ ).optional()
91
+ }).strict().superRefine((cap, ctx) => {
92
+ if (cap.composes !== void 0 && !isSkillCapabilityType(cap.type)) {
93
+ ctx.addIssue({
94
+ code: zod.z.ZodIssueCode.custom,
95
+ message: "composes is only valid on a skill/* capability type",
96
+ path: ["composes"]
97
+ });
98
+ }
99
+ });
74
100
  var LeaseIdSchema = zod.z.string().min(1);
75
101
  var TaskPayloadSchema = zod.z.object({
76
102
  taskId: zod.z.string().min(1),
@@ -209,7 +235,7 @@ zod.z.discriminatedUnion("type", [
209
235
  ]);
210
236
 
211
237
  // ../neuron-protocol/src/transport.ts
212
- var PROTOCOL_VERSION = "1.1.0";
238
+ var PROTOCOL_VERSION = "1.2.0";
213
239
  var SSE_PATH = "/neuron/events";
214
240
  var SSE_EVENT = {
215
241
  registered: "registered",
@@ -235,6 +261,7 @@ function defineCapability(init) {
235
261
  scope: init.scope,
236
262
  concurrency: init.concurrency,
237
263
  leaseTtlMs: init.leaseTtlMs,
264
+ composes: init.composes,
238
265
  _zod: { input: init.input, output: init.output }
239
266
  };
240
267
  }
@@ -260,6 +287,7 @@ function toRegistrationCapability(def, opts) {
260
287
  );
261
288
  }
262
289
  const leaseTtlMs = opts?.leaseTtlMs ?? def.leaseTtlMs;
290
+ const composes = opts?.composes ?? def.composes;
263
291
  return {
264
292
  type: def.type,
265
293
  scope,
@@ -270,7 +298,8 @@ function toRegistrationCapability(def, opts) {
270
298
  description: opts?.description ?? def.description,
271
299
  inputSchema: def.inputSchema,
272
300
  outputSchema: def.outputSchema,
273
- ...typeof leaseTtlMs === "number" ? { leaseTtlMs } : {}
301
+ ...typeof leaseTtlMs === "number" ? { leaseTtlMs } : {},
302
+ ...composes !== void 0 ? { composes: [...composes] } : {}
274
303
  };
275
304
  }
276
305
 
@@ -979,7 +1008,8 @@ var Neuron = class extends eventemitter3.EventEmitter {
979
1008
  ...reg.description !== void 0 ? { description: reg.description } : {},
980
1009
  ...reg.inputSchema !== void 0 ? { inputSchema: reg.inputSchema } : {},
981
1010
  ...reg.outputSchema !== void 0 ? { outputSchema: reg.outputSchema } : {},
982
- ...typeof reg.leaseTtlMs === "number" ? { leaseTtlMs: reg.leaseTtlMs } : {}
1011
+ ...typeof reg.leaseTtlMs === "number" ? { leaseTtlMs: reg.leaseTtlMs } : {},
1012
+ ...reg.composes !== void 0 ? { composes: reg.composes } : {}
983
1013
  };
984
1014
  inputSchema = reg.inputSchema;
985
1015
  outputSchema = reg.outputSchema;