@calimero-network/agent-skills 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -6,8 +6,9 @@ This package provides AI agent skills for building on the Calimero Network stack
6
6
 
7
7
  | Skill | Install command | When to use |
8
8
  | --- | --- | --- |
9
- | `calimero-rust-sdk` | `npx @calimero-network/agent-skills calimero-rust-sdk` | Building Rust WASM applications |
10
- | `calimero-client-js` | `npx @calimero-network/agent-skills calimero-client-js` | Frontend / Node.js clients connecting to a node |
9
+ | `calimero-rust-sdk` | `npx @calimero-network/agent-skills calimero-rust-sdk` | Building Rust WASM applications that run on a node |
10
+ | `calimero-sdk-js` | `npx @calimero-network/agent-skills calimero-sdk-js` | Building TypeScript/JS WASM applications that run on a node |
11
+ | `calimero-client-js` | `npx @calimero-network/agent-skills calimero-client-js` | Browser/Node.js frontends connecting to a node (not building apps) |
11
12
  | `calimero-registry` | `npx @calimero-network/agent-skills calimero-registry` | Signing and publishing apps to the registry |
12
13
  | `calimero-desktop` | `npx @calimero-network/agent-skills calimero-desktop` | Integrating apps with Calimero Desktop SSO |
13
14
  | `calimero-node` | `npx @calimero-network/agent-skills calimero-node` | Node operators and meroctl scripting |
@@ -22,11 +23,15 @@ Skills should be loaded when the following are detected in the project:
22
23
  | Signal | Load skill |
23
24
  | --- | --- |
24
25
  | `calimero-sdk` in `Cargo.toml` | `calimero-rust-sdk` |
26
+ | `@calimero-network/calimero-sdk-js` in `package.json` | `calimero-sdk-js` |
27
+ | `@State` / `@Logic` decorators in TypeScript source | `calimero-sdk-js` |
28
+ | `calimero-sdk build` in any script | `calimero-sdk-js` |
25
29
  | `@calimero-network/calimero-client` in `package.json` | `calimero-client-js` |
26
30
  | `@calimero-network/mero-js` in `package.json` | `calimero-client-js` |
27
31
  | `mero-sign` in any script or Makefile | `calimero-registry` |
28
32
  | `calimero-registry` CLI usage | `calimero-registry` |
29
33
  | `access_token` read from `window.location.hash` | `calimero-desktop` |
34
+ | `readDesktopSSO` / `hash.get('access_token')` pattern in frontend code | `calimero-desktop` |
30
35
  | `merobox` in `package.json` or `requirements.txt` | `calimero-merobox` |
31
36
  | `calimero-client-py` in `requirements.txt` or `pyproject.toml` | `calimero-client-py` |
32
37
  | `calimero-abi-codegen` or `abi.json` in project | `calimero-abi-codegen` |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@calimero-network/agent-skills",
3
- "version": "0.2.0",
4
- "description": "AI agent skills for Calimero Network development — Rust SDK, JS client, registry publishing, desktop SSO, and more.",
3
+ "version": "0.3.0",
4
+ "description": "AI agent skills for Calimero Network development — Rust SDK, JS SDK, JS client, registry publishing, desktop SSO, and more.",
5
5
  "keywords": [
6
6
  "calimero",
7
7
  "agent-skills",
@@ -1,6 +1,12 @@
1
1
  # calimero-client-js — Agent Instructions
2
2
 
3
- You are helping a developer connect a **browser or Node.js frontend** to a Calimero node using `@calimero-network/calimero-client` or `@calimero-network/mero-js`.
3
+ You are helping a developer connect a **browser or Node.js frontend** to a Calimero node
4
+ using `@calimero-network/calimero-client` or `@calimero-network/mero-js`.
5
+
6
+ > **NOT this skill** if the developer is building the application logic that *runs on the
7
+ > node* in TypeScript — that is `calimero-sdk-js` (`@calimero-network/calimero-sdk-js`).
8
+ > This skill is for the *client* side: auth, RPC calls, and WebSocket subscriptions from
9
+ > a browser or backend service.
4
10
 
5
11
  ## Package versions
6
12
 
@@ -9,6 +15,10 @@ You are helping a developer connect a **browser or Node.js frontend** to a Calim
9
15
  | `@calimero-network/calimero-client` | latest | Stable client for browser/Node — auth, RPC, WebSocket |
10
16
  | `@calimero-network/mero-js` | `>=2.0.0-beta.1` | v2 API — all request fields are **camelCase** |
11
17
 
18
+ **Which to use:** new projects should prefer `@calimero-network/mero-js` v2. If you are
19
+ maintaining an existing codebase that uses `calimero-client`, check for snake_case field
20
+ names before migrating — do not mix both packages in the same project.
21
+
12
22
  ## Critical: mero-js v2 uses camelCase
13
23
 
14
24
  v2 changed all request field names from `snake_case` to `camelCase`.
@@ -97,6 +97,39 @@ ws.disconnect();
97
97
 
98
98
  ---
99
99
 
100
+ ## Connection errors and reconnection
101
+
102
+ `WsSubscriptionsClient` does **not** reconnect automatically. If the connection drops
103
+ or the initial connect fails, you must retry manually.
104
+
105
+ ```typescript
106
+ async function connectWithRetry(ws: WsSubscriptionsClient, contextId: string, retries = 3): Promise<void> {
107
+ for (let i = 0; i < retries; i++) {
108
+ try {
109
+ await ws.connect();
110
+ ws.subscribe([contextId]);
111
+ return;
112
+ } catch (err: any) {
113
+ if (err?.status === 401) {
114
+ // Token expired — cannot reconnect until re-authenticated
115
+ clientLogout();
116
+ return;
117
+ }
118
+ if (i === retries - 1) throw err;
119
+ await new Promise(r => setTimeout(r, 1000 * (i + 1))); // exponential backoff
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ To detect drops after the connection is established, check if events stop arriving and
126
+ reconnect periodically, or listen to `ws.onClose` if exposed.
127
+
128
+ > WebSocket tokens are **not** auto-refreshed unlike `rpcClient` RPC calls.
129
+ > See `rules/token-refresh.md` for details.
130
+
131
+ ---
132
+
100
133
  ## React hook pattern
101
134
 
102
135
  ```typescript
@@ -1,41 +1,79 @@
1
- # Rule: Always handle 401 with token refresh
1
+ # Rule: rpcClient handles token refresh automatically — but not all 401s
2
2
 
3
- Access tokens expire. Any RPC call or WebSocket connection can return `401 Unauthorized`. Never let this surface as an unhandled error.
3
+ `rpcClient` (from `@calimero-network/calimero-client`) automatically retries with a
4
+ refreshed token when the server returns `401 token_expired`. **You do not need to
5
+ implement a refresh wrapper around `rpcClient.execute()` calls.**
4
6
 
5
- ## Pattern
7
+ What you DO need to handle: 401s that cannot be refreshed.
8
+
9
+ ## WRONG — manual refresh wrapper (not needed for rpcClient):
6
10
 
7
11
  ```typescript
8
- async function callWithRefresh<T>(callFn: () => Promise<T>): Promise<T> {
12
+ // Don't do this rpcClient already retries on token_expired
13
+ async function callWithRefresh<T>(fn: () => Promise<T>) {
9
14
  try {
10
- return await callFn();
15
+ return await fn();
11
16
  } catch (err: any) {
12
- if (err?.code === 401 || err?.status === 401) {
13
- await refreshAccessToken();
14
- return callFn(); // retry once
17
+ if (err?.code === 401) {
18
+ await refreshAccessToken(); // rpcClient already does this
19
+ return fn();
15
20
  }
16
21
  throw err;
17
22
  }
18
23
  }
24
+ ```
25
+
26
+ ## CORRECT — handle only non-refreshable auth failures:
27
+
28
+ ```typescript
29
+ const response = await rpcClient.execute({ ... });
30
+
31
+ if (response.error) {
32
+ const authError = response.error.headers?.['x-auth-error'];
19
33
 
20
- async function refreshAccessToken() {
21
- const jwt = getJWTObject();
22
- if (!jwt?.refresh_token) {
23
- // No refresh token — redirect to login
24
- window.location.href = '/login';
34
+ if (response.error.code === 401) {
35
+ if (authError === 'token_revoked' || authError === 'invalid_token') {
36
+ // Token is invalid — cannot be refreshed. Send user to login.
37
+ clientLogout();
38
+ return;
39
+ }
40
+ // token_expired: rpcClient already retried and the retry failed.
41
+ // This means the refresh token is also expired — send to login.
42
+ clientLogout();
25
43
  return;
26
44
  }
27
45
 
28
- const newTokens = await client.refreshToken(jwt.refresh_token);
29
- localStorage.setItem('calimero_jwt', JSON.stringify(newTokens));
46
+ throw new Error(response.error.error.cause.info?.message ?? 'Unknown error');
30
47
  }
31
48
  ```
32
49
 
33
- ## Why
50
+ ## WebSocket connections are different — no auto-refresh
51
+
52
+ `WsSubscriptionsClient` does **not** auto-refresh tokens. If the token expires while
53
+ a WebSocket connection is open, events will stop arriving silently. Reconnect manually:
34
54
 
35
- Access tokens are short-lived by design. Without refresh handling, users will see
36
- random authentication errors mid-session. The refresh token is longer-lived and
37
- should be used to silently re-authenticate.
55
+ ```typescript
56
+ async function connectWithAuth() {
57
+ const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
58
+ try {
59
+ await ws.connect();
60
+ ws.subscribe([getContextId()!]);
61
+ ws.addCallback(handleEvent);
62
+ } catch (err: any) {
63
+ if (err?.status === 401) {
64
+ // Token expired before WS connect — logout and re-authenticate
65
+ clientLogout();
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+ ```
38
71
 
39
- ## What to do if refresh also fails
72
+ ## Summary
40
73
 
41
- Redirect to login. Do not retry the refresh indefinitely.
74
+ | Scenario | Handled by |
75
+ | --- | --- |
76
+ | `401 token_expired` on RPC call | `rpcClient` automatically (transparent retry) |
77
+ | `401 token_revoked` on RPC call | You — redirect to login |
78
+ | `401 invalid_token` on RPC call | You — redirect to login |
79
+ | Auth failure on WebSocket connect | You — catch and redirect to login |
@@ -3,6 +3,10 @@
3
3
  You are helping a developer use the **Calimero Python client** (`calimero-client-py`) to
4
4
  interact with a Calimero node from Python — automation scripts, backend services, or CLI tools.
5
5
 
6
+ > **NOT this skill** if the developer is building the application logic that runs on the
7
+ > node — that is `calimero-rust-sdk` (Rust/WASM) or `calimero-sdk-js` (TypeScript/WASM).
8
+ > This skill is for Python code that *calls* the node from outside.
9
+
6
10
  ## What it is
7
11
 
8
12
  `calimero-client-py` is a Python package built with PyO3 (Rust bindings). It provides:
@@ -1,6 +1,12 @@
1
1
  # calimero-desktop — Agent Instructions
2
2
 
3
- You are helping a developer integrate their app frontend with **Calimero Desktop SSO**.
3
+ You are helping a developer integrate their app frontend with **Calimero Desktop SSO** —
4
+ the flow where users open an app from the Desktop and are automatically logged in without
5
+ a manual auth screen.
6
+
7
+ > **NOT this skill** if the developer just needs to authenticate manually via the login
8
+ > form — that is handled entirely by `calimero-client-js`. This skill is specifically for
9
+ > reading SSO tokens that Calimero Desktop passes in the URL hash.
4
10
 
5
11
  ## What Desktop does
6
12
 
@@ -2,28 +2,39 @@
2
2
 
3
3
  ## Full startup flow
4
4
 
5
+ Use the storage helpers from `@calimero-network/calimero-client` — do **not** write to
6
+ `localStorage` directly. The helpers ensure correct key names and extract contextId +
7
+ executorPublicKey from the JWT automatically.
8
+
5
9
  ```typescript
10
+ import {
11
+ setAppEndpointKey,
12
+ setAccessToken,
13
+ setRefreshToken,
14
+ setApplicationId,
15
+ setContextAndIdentityFromJWT,
16
+ getAuthConfig,
17
+ } from '@calimero-network/calimero-client';
18
+
6
19
  interface SSOParams {
7
20
  accessToken: string;
8
- refreshToken: string;
21
+ refreshToken: string | null;
9
22
  nodeUrl: string;
10
- applicationId: string;
23
+ applicationId: string | null;
11
24
  }
12
25
 
13
26
  function readDesktopSSO(): SSOParams | null {
14
27
  const hash = new URLSearchParams(window.location.hash.slice(1));
15
28
  const accessToken = hash.get('access_token');
16
- const refreshToken = hash.get('refresh_token');
17
29
  const nodeUrl = hash.get('node_url');
18
- const applicationId = hash.get('application_id');
19
30
 
20
31
  if (!accessToken || !nodeUrl) return null;
21
32
 
22
33
  return {
23
34
  accessToken,
24
- refreshToken: refreshToken ?? '',
35
+ refreshToken: hash.get('refresh_token'),
25
36
  nodeUrl,
26
- applicationId: applicationId ?? '',
37
+ applicationId: hash.get('application_id'),
27
38
  };
28
39
  }
29
40
 
@@ -31,21 +42,29 @@ async function bootstrap() {
31
42
  const sso = readDesktopSSO();
32
43
 
33
44
  if (sso) {
45
+ // Store tokens via SDK helpers (sets localStorage keys correctly)
46
+ setAppEndpointKey(sso.nodeUrl);
47
+ setAccessToken(sso.accessToken);
48
+ if (sso.refreshToken) setRefreshToken(sso.refreshToken);
49
+ if (sso.applicationId) setApplicationId(sso.applicationId);
50
+ // Extracts contextId + executorPublicKey from JWT claims
51
+ setContextAndIdentityFromJWT(sso.accessToken);
52
+
34
53
  // Clear hash from URL bar (tokens shouldn't sit in browser history)
35
54
  history.replaceState(null, '', window.location.pathname + window.location.search);
36
55
 
37
- // Store for use by the client
38
- localStorage.setItem('calimero_node_url', sso.nodeUrl);
39
- localStorage.setItem('calimero_jwt', JSON.stringify({
40
- access_token: sso.accessToken,
41
- refresh_token: sso.refreshToken,
42
- }));
43
- localStorage.setItem('calimero_app_id', sso.applicationId);
44
-
45
- await renderAuthenticatedApp(sso);
46
- } else {
47
- renderLoginScreen();
56
+ renderAuthenticatedApp();
57
+ return;
58
+ }
59
+
60
+ // No SSO hash — check if already authenticated from a prior session
61
+ const config = getAuthConfig();
62
+ if (config.error === null) {
63
+ renderAuthenticatedApp();
64
+ return;
48
65
  }
66
+
67
+ renderLoginScreen();
49
68
  }
50
69
 
51
70
  document.addEventListener('DOMContentLoaded', bootstrap);
@@ -53,7 +72,8 @@ document.addEventListener('DOMContentLoaded', bootstrap);
53
72
 
54
73
  ## How Desktop discovers your app's frontend URL
55
74
 
56
- Desktop reads the `links.frontend` field from your app's `manifest.json` inside the installed bundle. To ensure Desktop can open your app correctly, set this field:
75
+ Desktop reads the `links.frontend` field from your app's `manifest.json` inside the
76
+ installed bundle. Set this field so Desktop can open your app:
57
77
 
58
78
  ```json
59
79
  {
@@ -70,18 +90,25 @@ Desktop opens this URL and appends the SSO hash params.
70
90
 
71
91
  ```typescript
72
92
  // App.tsx
93
+ import { getAuthConfig, setAccessToken, setAppEndpointKey,
94
+ setRefreshToken, setApplicationId, setContextAndIdentityFromJWT } from '@calimero-network/calimero-client';
95
+
73
96
  function App() {
74
97
  const [authState, setAuthState] = useState<'loading' | 'authenticated' | 'unauthenticated'>('loading');
75
98
 
76
99
  useEffect(() => {
77
100
  const sso = readDesktopSSO();
78
101
  if (sso) {
79
- storeTokens(sso);
80
- setAuthState('authenticated');
81
- } else if (hasStoredTokens()) {
102
+ setAppEndpointKey(sso.nodeUrl);
103
+ setAccessToken(sso.accessToken);
104
+ if (sso.refreshToken) setRefreshToken(sso.refreshToken);
105
+ if (sso.applicationId) setApplicationId(sso.applicationId);
106
+ setContextAndIdentityFromJWT(sso.accessToken);
107
+ history.replaceState(null, '', window.location.pathname);
82
108
  setAuthState('authenticated');
83
109
  } else {
84
- setAuthState('unauthenticated');
110
+ const config = getAuthConfig();
111
+ setAuthState(config.error === null ? 'authenticated' : 'unauthenticated');
85
112
  }
86
113
  }, []);
87
114
 
@@ -67,6 +67,12 @@ meroctl --node-url http://localhost:2428 <command> # connect to specific node
67
67
  meroctl --home ~/.calimero <command> # use alternate config path
68
68
  ```
69
69
 
70
+ `--home` defaults to `~/.calimero`. The home directory contains `config.toml`,
71
+ the node's key material, and local storage. Each node must have its own home directory.
72
+
73
+ `--node-url` defaults to `http://localhost:2428` if not specified (the default port
74
+ `merod` listens on after `merod --home ~/.calimero run`).
75
+
70
76
  ## References
71
77
 
72
78
  See `references/` for full meroctl command reference and context lifecycle.
@@ -0,0 +1,46 @@
1
+ # Rule: State struct must derive Default, BorshDeserialize, BorshSerialize
2
+
3
+ The three derives are all required. Missing any one of them causes a **runtime panic**,
4
+ not a compile error — the app will install and start, then crash when the context is
5
+ first accessed.
6
+
7
+ ## WRONG — missing derives:
8
+
9
+ ```rust
10
+ // ✗ Missing all derives — runtime panic on first call
11
+ pub struct AppState {
12
+ items: UnorderedMap<String, String>,
13
+ }
14
+
15
+ // ✗ Missing BorshSerialize — state cannot be persisted
16
+ #[derive(Default, BorshDeserialize)]
17
+ pub struct AppState {
18
+ items: UnorderedMap<String, String>,
19
+ }
20
+ ```
21
+
22
+ ## CORRECT:
23
+
24
+ ```rust
25
+ #[derive(Default, BorshDeserialize, BorshSerialize)]
26
+ pub struct AppState {
27
+ items: UnorderedMap<String, String>,
28
+ }
29
+ ```
30
+
31
+ ## What each derive does
32
+
33
+ | Derive | Required for |
34
+ | --- | --- |
35
+ | `Default` | `#[app::init]` calls `AppState::default()` as the baseline |
36
+ | `BorshDeserialize` | Loading state from node storage on every method call |
37
+ | `BorshSerialize` | Saving state to node storage after every mutation |
38
+
39
+ ## Import
40
+
41
+ ```rust
42
+ use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
43
+ ```
44
+
45
+ Do not use `borsh` crate directly — import from `calimero_sdk::borsh` to ensure
46
+ version compatibility.
@@ -0,0 +1,137 @@
1
+ # calimero-sdk-js — Agent Instructions
2
+
3
+ You are helping a developer **build a Calimero P2P application in TypeScript** using
4
+ `@calimero-network/calimero-sdk-js`. The app compiles to WebAssembly and runs inside the
5
+ `merod` node runtime.
6
+
7
+ > **NOT this skill** if the developer is connecting a browser/Node.js *frontend* to a node
8
+ > (that's `calimero-client-js` / `@calimero-network/calimero-client`). This skill is for
9
+ > writing the application logic that *runs on the node*.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @calimero-network/calimero-sdk-js
15
+ pnpm add -D @calimero-network/calimero-cli-js typescript
16
+ ```
17
+
18
+ The CLI's `postinstall` hook downloads QuickJS, WASI-SDK, and Binaryen automatically.
19
+ If you used `--ignore-scripts`, re-run with `pnpm install --ignore-scripts=false`.
20
+
21
+ ## Core concepts
22
+
23
+ | Concept | What it is |
24
+ | --- | --- |
25
+ | `@State` class | Persisted data — fields must be CRDT types |
26
+ | `@Logic(StateClass)` class | Entry points callable via JSON-RPC; must extend the state class |
27
+ | `@Init` static method | Seeds the initial state when context is first created |
28
+ | `@View()` method | Read-only — skips persistence; required for query methods |
29
+ | CRDT collection | Conflict-free type (`Counter`, `UnorderedMap`, etc.) — all state fields must use these |
30
+
31
+ ## Minimal app
32
+
33
+ ```typescript
34
+ import { State, Logic, Init, View } from '@calimero-network/calimero-sdk-js';
35
+ import { UnorderedMap } from '@calimero-network/calimero-sdk-js/collections';
36
+ import * as env from '@calimero-network/calimero-sdk-js/env';
37
+
38
+ @State
39
+ export class KvState {
40
+ items: UnorderedMap<string, string> = new UnorderedMap();
41
+ }
42
+
43
+ @Logic(KvState)
44
+ export class KvLogic extends KvState {
45
+ @Init
46
+ static init(): KvState {
47
+ return new KvState();
48
+ }
49
+
50
+ set(key: string, value: string): void {
51
+ env.log(`set ${key}`);
52
+ this.items.set(key, value);
53
+ }
54
+
55
+ @View()
56
+ get(key: string): string | null {
57
+ return this.items.get(key) ?? null;
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## CRDT collections quick reference
63
+
64
+ | Type | Use case | Key ops |
65
+ | --- | --- | --- |
66
+ | `Counter` | Distributed counting (returns `bigint`) | `increment()`, `incrementBy(n)`, `value()` |
67
+ | `UnorderedMap<K,V>` | Key-value store (LWW per key) | `set()`, `get()`, `has()`, `remove()`, `entries()` |
68
+ | `UnorderedSet<T>` | Unique membership (LWW per element) | `add()`, `has()`, `delete()`, `toArray()` |
69
+ | `Vector<T>` | Ordered list | `push()`, `get(i)`, `pop()`, `len()` |
70
+ | `LwwRegister<T>` | Single value (timestamp LWW) | `set()`, `get()` |
71
+
72
+ Nested collections (`Map<K, Set<V>>`) propagate changes automatically — no manual
73
+ re-serialization.
74
+
75
+ ## Build & deploy
76
+
77
+ ```bash
78
+ # Build to WASM
79
+ npx calimero-sdk build src/index.ts -o build/service.wasm
80
+
81
+ # Install on node
82
+ meroctl --node-name <NODE> app install \
83
+ --path build/service.wasm \
84
+ --context-id <CONTEXT_ID>
85
+
86
+ # Call a method
87
+ meroctl --node-name <NODE> call \
88
+ --context-id <CONTEXT_ID> \
89
+ --method set \
90
+ --args '{"key":"hello","value":"world"}'
91
+
92
+ # Call a view
93
+ meroctl --node-name <NODE> call \
94
+ --context-id <CONTEXT_ID> \
95
+ --method get \
96
+ --args '{"key":"hello"}'
97
+ ```
98
+
99
+ ## Key rules
100
+
101
+ - All `@State` fields must be CRDT types — never plain `Map`, `Set`, `Array`, or primitives
102
+ - `@View()` is **required** on every read-only method — omitting it causes unnecessary persistence
103
+ - Use `env.log()` not `console.log()` — `console` is not available in the WASM runtime
104
+ - `Counter.value()` returns `bigint`, not `number`
105
+ - `@Init` must be a static method that returns the state class instance
106
+ - `@Logic(StateClass)` must extend the state class
107
+ - No async, no I/O, no threads in app logic — the WASM runtime is synchronous
108
+ - **Windows: building is not supported natively — use WSL** (QuickJS/WASI-SDK toolchain requires Linux/macOS)
109
+
110
+ ## Events
111
+
112
+ ```typescript
113
+ import { emit } from '@calimero-network/calimero-sdk-js';
114
+
115
+ // Inside a mutation method:
116
+ emit({ type: 'ItemAdded', key: 'foo', value: 'bar' });
117
+ ```
118
+
119
+ Events are pushed to all context members via WebSocket. Clients subscribe using
120
+ `WsSubscriptionsClient` from `@calimero-network/calimero-client`.
121
+
122
+ ## Private storage (node-local)
123
+
124
+ ```typescript
125
+ import { createPrivateEntry } from '@calimero-network/calimero-sdk-js';
126
+
127
+ const secret = createPrivateEntry<string>();
128
+ secret.set('my-api-key');
129
+ const val = secret.get();
130
+ ```
131
+
132
+ Private entries are never broadcast to other nodes.
133
+
134
+ ## References
135
+
136
+ See `references/` for CRDT collections, events, and build pipeline details.
137
+ See `rules/` for hard constraints.
@@ -0,0 +1,98 @@
1
+ # Build Pipeline
2
+
3
+ `@calimero-network/calimero-cli-js` compiles your TypeScript app to a `.wasm` binary.
4
+
5
+ ## Pipeline stages
6
+
7
+ ```
8
+ TypeScript → Rollup → QuickJS → WASI-SDK → Binaryen → .wasm
9
+ Source Bundle C bytecode WASM Optimized
10
+ ```
11
+
12
+ ## Setup
13
+
14
+ ```bash
15
+ # Install SDK and CLI
16
+ pnpm add @calimero-network/calimero-sdk-js
17
+ pnpm add -D @calimero-network/calimero-cli-js typescript
18
+
19
+ # If postinstall didn't run (--ignore-scripts was set):
20
+ pnpm install --ignore-scripts=false
21
+ # Or manually:
22
+ pnpm --filter @calimero-network/calimero-cli-js run install-deps
23
+ ```
24
+
25
+ ## package.json (minimal)
26
+
27
+ ```json
28
+ {
29
+ "dependencies": {
30
+ "@calimero-network/calimero-sdk-js": "^0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "@calimero-network/calimero-cli-js": "^0.1.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "scripts": {
37
+ "build": "calimero-sdk build src/index.ts -o build/service.wasm"
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## Build command
43
+
44
+ ```bash
45
+ # Build
46
+ npx calimero-sdk build src/index.ts -o build/service.wasm
47
+
48
+ # Or via npm script
49
+ pnpm build
50
+ ```
51
+
52
+ ## Deploy to node
53
+
54
+ ```bash
55
+ # Install the app
56
+ meroctl --node-name <NODE> app install \
57
+ --path build/service.wasm \
58
+ --context-id <CONTEXT_ID>
59
+
60
+ # Create a new context (runs @Init)
61
+ meroctl --node-name <NODE> context create \
62
+ --app-id <APP_ID>
63
+
64
+ # Call a mutation
65
+ meroctl --node-name <NODE> call \
66
+ --context-id <CONTEXT_ID> \
67
+ --method set \
68
+ --args '{"key":"hello","value":"world"}'
69
+
70
+ # Call a view
71
+ meroctl --node-name <NODE> call \
72
+ --context-id <CONTEXT_ID> \
73
+ --method get \
74
+ --args '{"key":"hello"}'
75
+ ```
76
+
77
+ ## End-to-end test with Merobox
78
+
79
+ Each example in the SDK ships a Merobox workflow for multi-node E2E testing:
80
+
81
+ ```bash
82
+ merobox bootstrap run examples/counter/workflows/counter-js.yml --log-level=trace
83
+ ```
84
+
85
+ ## Platform support
86
+
87
+ | Platform | Supported |
88
+ | --- | --- |
89
+ | macOS (x64, arm64) | Yes |
90
+ | Linux (x64, arm64) | Yes |
91
+ | Windows (native) | No — use WSL |
92
+
93
+ ## Definition of done (before PR)
94
+
95
+ 1. `pnpm lint` passes
96
+ 2. `pnpm format:check` passes
97
+ 3. `pnpm test` passes
98
+ 4. Example app builds: `cd examples/counter && pnpm build`
@@ -0,0 +1,132 @@
1
+ # CRDT Collections
2
+
3
+ All persistent state in a `calimero-sdk-js` app must use CRDT types from
4
+ `@calimero-network/calimero-sdk-js/collections`.
5
+
6
+ ## Counter (G-Counter)
7
+
8
+ Distributed counting. The value is the sum across all contributing nodes.
9
+
10
+ ```typescript
11
+ import { Counter } from '@calimero-network/calimero-sdk-js/collections';
12
+
13
+ const counter = new Counter();
14
+ counter.increment(); // +1
15
+ counter.incrementBy(5n); // +5 (bigint)
16
+ const total = counter.value(); // bigint
17
+ ```
18
+
19
+ > Counter values are always `bigint`. Do not compare with `=== 0` — use `=== 0n`.
20
+
21
+ ## UnorderedMap\<K, V\>
22
+
23
+ Key-value store with Last-Write-Wins conflict resolution per key.
24
+
25
+ ```typescript
26
+ import { UnorderedMap } from '@calimero-network/calimero-sdk-js/collections';
27
+
28
+ const map = new UnorderedMap<string, string>();
29
+ map.set('key', 'value');
30
+ const val = map.get('key'); // 'value' | undefined
31
+ const exists = map.has('key'); // boolean
32
+ map.remove('key');
33
+ const all = map.entries(); // [['key', 'value'], ...]
34
+ ```
35
+
36
+ ## UnorderedSet\<T\>
37
+
38
+ Set of unique values with Last-Write-Wins per element.
39
+
40
+ ```typescript
41
+ import { UnorderedSet } from '@calimero-network/calimero-sdk-js/collections';
42
+
43
+ const set = new UnorderedSet<string>();
44
+ set.add('item'); // true on first insert, false if already present
45
+ set.has('item'); // true
46
+ set.delete('item');
47
+ const items = set.toArray();
48
+ ```
49
+
50
+ ## Vector\<T\>
51
+
52
+ Ordered list. Conflicts resolved by position.
53
+
54
+ ```typescript
55
+ import { Vector } from '@calimero-network/calimero-sdk-js/collections';
56
+
57
+ const vec = new Vector<string>();
58
+ vec.push('first');
59
+ vec.push('second');
60
+ const item = vec.get(0); // 'first'
61
+ const last = vec.pop(); // 'second'
62
+ const len = vec.len(); // 1 (bigint)
63
+ ```
64
+
65
+ ## LwwRegister\<T\>
66
+
67
+ Holds a single value. Last write wins based on timestamp.
68
+
69
+ ```typescript
70
+ import { LwwRegister } from '@calimero-network/calimero-sdk-js/collections';
71
+
72
+ const reg = new LwwRegister<string>();
73
+ reg.set('current value');
74
+ const current = reg.get(); // 'current value' | undefined
75
+ ```
76
+
77
+ ## UserStorage
78
+
79
+ User-owned signed data. Writes are verified by the owner's signature.
80
+
81
+ ```typescript
82
+ import { UserStorage } from '@calimero-network/calimero-sdk-js/collections';
83
+
84
+ const storage = new UserStorage<string>();
85
+ storage.set(executorId, 'my value');
86
+ const val = storage.get(executorId);
87
+ ```
88
+
89
+ ## FrozenStorage
90
+
91
+ Immutable, content-addressed storage. Write once; content never changes.
92
+
93
+ ```typescript
94
+ import { FrozenStorage } from '@calimero-network/calimero-sdk-js/collections';
95
+
96
+ const frozen = new FrozenStorage<string>();
97
+ const id = frozen.store('immutable content');
98
+ const val = frozen.get(id);
99
+ ```
100
+
101
+ ## Nested collections
102
+
103
+ Nested structures propagate changes automatically — no manual re-serialization needed.
104
+
105
+ ```typescript
106
+ // Map<projectId, Set<tags>> — just works
107
+ const projectTags = new UnorderedMap<string, UnorderedSet<string>>();
108
+
109
+ const tags = new UnorderedSet<string>();
110
+ tags.add('urgent');
111
+ projectTags.set('proj:123', tags);
112
+
113
+ // Modifying the inner set propagates automatically
114
+ projectTags.get('proj:123')?.add('high-priority');
115
+ ```
116
+
117
+ ## State field initialization
118
+
119
+ CRDT fields must be initialized inline in the `@State` class:
120
+
121
+ ```typescript
122
+ @State
123
+ export class AppState {
124
+ // ✅ Inline initialization
125
+ items: UnorderedMap<string, string> = new UnorderedMap();
126
+ count: Counter = new Counter();
127
+
128
+ // ❌ Never use plain JS types for state
129
+ // plainMap: Map<string, string> = new Map();
130
+ // array: string[] = [];
131
+ }
132
+ ```
@@ -0,0 +1,59 @@
1
+ # Events
2
+
3
+ Events let your app push real-time notifications to all context members.
4
+ They are emitted during mutation methods and received by clients via WebSocket.
5
+
6
+ ## Emitting events
7
+
8
+ ```typescript
9
+ import { emit, emitWithHandler } from '@calimero-network/calimero-sdk-js';
10
+
11
+ // Simple event — clients receive it and dispatch themselves
12
+ emit({ type: 'ItemAdded', key: 'foo', value: 'bar' });
13
+
14
+ // Event with a named handler — clients call `onItemAdded(event)` if defined
15
+ emitWithHandler({ type: 'ItemAdded', key: 'foo' }, 'onItemAdded');
16
+ ```
17
+
18
+ Events can only be emitted inside mutation methods (not `@View()` methods).
19
+
20
+ ## Receiving events on the client
21
+
22
+ Clients use `WsSubscriptionsClient` from `@calimero-network/calimero-client`:
23
+
24
+ ```typescript
25
+ import { WsSubscriptionsClient, getAppEndpointKey, getContextId } from '@calimero-network/calimero-client';
26
+
27
+ const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
28
+ await ws.connect();
29
+ ws.subscribe([getContextId()!]);
30
+
31
+ ws.addCallback((event) => {
32
+ if (event.type === 'ExecutionEvent') {
33
+ for (const e of event.data.events) {
34
+ // e.kind — matches the `type` field from emit()
35
+ // e.data — the rest of the emitted object
36
+ console.log(e.kind, e.data);
37
+ }
38
+ }
39
+ });
40
+ ```
41
+
42
+ ## Event typing (recommended)
43
+
44
+ Define a discriminated union to type your events:
45
+
46
+ ```typescript
47
+ type AppEvent =
48
+ | { type: 'ItemAdded'; key: string; value: string }
49
+ | { type: 'ItemRemoved'; key: string };
50
+
51
+ // Emit
52
+ emit({ type: 'ItemAdded', key: 'foo', value: 'bar' } satisfies AppEvent);
53
+ ```
54
+
55
+ ## Important
56
+
57
+ - Events are best-effort — clients may miss them if disconnected
58
+ - Do not rely on events for state consistency — use RPC calls to query current state
59
+ - Emitting from a `@View()` method is not supported
@@ -0,0 +1,47 @@
1
+ # Rule: All @State fields must be CRDT types
2
+
3
+ Every field on a `@State` class must be a CRDT collection from
4
+ `@calimero-network/calimero-sdk-js/collections`. Plain JavaScript types are not
5
+ persisted or synchronized.
6
+
7
+ ## WRONG — plain JS types in state:
8
+
9
+ ```typescript
10
+ @State
11
+ export class AppState {
12
+ items: Map<string, string> = new Map(); // ✗ — not a CRDT, not synced
13
+ tags: string[] = []; // ✗ — not a CRDT, not synced
14
+ name: string = ''; // ✗ — use LwwRegister instead
15
+ count: number = 0; // ✗ — use Counter instead
16
+ }
17
+ ```
18
+
19
+ ## CORRECT — CRDT types:
20
+
21
+ ```typescript
22
+ @State
23
+ export class AppState {
24
+ items: UnorderedMap<string, string> = new UnorderedMap();
25
+ tags: UnorderedSet<string> = new UnorderedSet();
26
+ name: LwwRegister<string> = new LwwRegister();
27
+ count: Counter = new Counter();
28
+ }
29
+ ```
30
+
31
+ ## Why this matters
32
+
33
+ Plain JS types are not serialized to the node's CRDT storage. State that uses
34
+ `Map`, `Set`, `Array`, or primitives directly will be lost on the next call and
35
+ will never sync to other nodes in the context.
36
+
37
+ ## Choosing the right CRDT
38
+
39
+ | Data shape | Use |
40
+ | --- | --- |
41
+ | Counting (monotonic) | `Counter` |
42
+ | Key-value lookup | `UnorderedMap<K, V>` |
43
+ | Unique membership | `UnorderedSet<T>` |
44
+ | Ordered list | `Vector<T>` |
45
+ | Single value (overwrites) | `LwwRegister<T>` |
46
+ | User-owned signed data | `UserStorage<T>` |
47
+ | Immutable content | `FrozenStorage<T>` |
@@ -0,0 +1,38 @@
1
+ # Rule: Use env.log() — not console.log()
2
+
3
+ `console` is not available in the QuickJS/WASM runtime. Calling `console.log()` will
4
+ throw a runtime error. Use `env.log()` from the SDK's env module instead.
5
+
6
+ ## WRONG:
7
+
8
+ ```typescript
9
+ console.log('Processing item:', key); // ✗ — throws at runtime
10
+ console.error('Something went wrong'); // ✗ — throws at runtime
11
+ ```
12
+
13
+ ## CORRECT:
14
+
15
+ ```typescript
16
+ import * as env from '@calimero-network/calimero-sdk-js/env';
17
+
18
+ env.log(`Processing item: ${key}`); // ✓ — output appears in node logs
19
+ env.log(`Error: ${error}`); // ✓
20
+ ```
21
+
22
+ ## env.log() format
23
+
24
+ `env.log()` accepts a single string. Use template literals for dynamic values:
25
+
26
+ ```typescript
27
+ env.log(`set called: key=${key}, value=${value}`);
28
+ env.log(`counter is now ${this.count.value()}`);
29
+ ```
30
+
31
+ ## Other env utilities
32
+
33
+ ```typescript
34
+ import * as env from '@calimero-network/calimero-sdk-js/env';
35
+
36
+ // Get the calling executor's public key (Uint8Array, 32 bytes)
37
+ const executorId = env.executorId();
38
+ ```
@@ -0,0 +1,46 @@
1
+ # Rule: @View() is required on read-only methods
2
+
3
+ Every method that does not modify state must be decorated with `@View()`. Without it,
4
+ the runtime will persist state after every call — even when nothing changed — causing
5
+ unnecessary storage writes and cross-node syncs.
6
+
7
+ ## WRONG — read-only method without @View():
8
+
9
+ ```typescript
10
+ @Logic(AppState)
11
+ export class AppLogic extends AppState {
12
+ // ✗ — no @View(), triggers persistence on every call
13
+ getItem(key: string): string | null {
14
+ return this.items.get(key) ?? null;
15
+ }
16
+
17
+ // ✗ — returning a count without @View()
18
+ getCount(): bigint {
19
+ return this.count.value();
20
+ }
21
+ }
22
+ ```
23
+
24
+ ## CORRECT:
25
+
26
+ ```typescript
27
+ @Logic(AppState)
28
+ export class AppLogic extends AppState {
29
+ @View()
30
+ getItem(key: string): string | null {
31
+ return this.items.get(key) ?? null;
32
+ }
33
+
34
+ @View()
35
+ getCount(): bigint {
36
+ return this.count.value();
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## How to tell if a method needs @View()
42
+
43
+ A method is read-only if it:
44
+ - Does not call any mutation methods on CRDT fields (`set`, `insert`, `add`, `push`, `increment`, etc.)
45
+ - Does not call `emit()` or `emitWithHandler()`
46
+ - Only reads values and returns results