@calimero-network/agent-skills 0.3.0 → 0.4.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 +137 -17
- package/SKILL.md +31 -28
- package/package.json +1 -1
- package/scripts/install.js +3 -3
- package/scripts/test.js +6 -15
- package/skills/calimero-abi-codegen/SKILL.md +121 -22
- package/skills/calimero-abi-codegen/references/abi-format.md +3 -5
- package/skills/calimero-abi-codegen/references/generated-output.md +12 -4
- package/skills/calimero-abi-codegen/rules/schema-version.md +11 -4
- package/skills/calimero-abi-codegen/rules/unique-names.md +2 -6
- package/skills/calimero-client-js/SKILL.md +126 -31
- package/skills/calimero-client-js/references/auth.md +18 -10
- package/skills/calimero-client-js/references/rpc-calls.md +15 -21
- package/skills/calimero-client-js/references/sso.md +9 -9
- package/skills/calimero-client-js/references/websocket-events.md +73 -92
- package/skills/calimero-client-js/rules/camelcase-api.md +10 -7
- package/skills/calimero-client-js/rules/token-refresh.md +11 -11
- package/skills/calimero-client-py/SKILL.md +25 -13
- package/skills/calimero-client-py/references/api.md +41 -43
- package/skills/calimero-client-py/references/auth.md +7 -7
- package/skills/calimero-client-py/rules/async-usage.md +27 -31
- package/skills/calimero-client-py/rules/stable-node-name.md +7 -7
- package/skills/calimero-core/SKILL.md +135 -0
- package/skills/calimero-core/references/architecture.md +101 -0
- package/skills/calimero-core/references/jsonrpc-protocol.md +192 -0
- package/skills/calimero-core/references/namespaces-groups.md +94 -0
- package/skills/calimero-core/references/storage-types.md +118 -0
- package/skills/calimero-core/references/websocket-events.md +142 -0
- package/skills/calimero-core/rules/context-is-not-app.md +35 -0
- package/skills/calimero-core/rules/crdt-types-only.md +55 -0
- package/skills/calimero-desktop/SKILL.md +24 -19
- package/skills/calimero-desktop/references/sso-integration.md +2 -2
- package/skills/calimero-desktop/rules/sso-fallback.md +3 -2
- package/skills/calimero-merobox/SKILL.md +255 -28
- package/skills/calimero-merobox/references/ci-integration.md +3 -2
- package/skills/calimero-merobox/references/workflow-files.md +7 -5
- package/skills/calimero-merobox/rules/docker-required.md +7 -6
- package/skills/calimero-meroctl/SKILL.md +68 -0
- package/skills/calimero-meroctl/references/commands.md +177 -0
- package/skills/calimero-meroctl/references/scripting.md +80 -0
- package/skills/calimero-meroctl/rules/call-view-flag.md +28 -0
- package/skills/calimero-meroctl/rules/register-node-once.md +34 -0
- package/skills/calimero-merod/SKILL.md +49 -0
- package/skills/calimero-merod/references/health-endpoints.md +90 -0
- package/skills/calimero-merod/references/init-flags.md +84 -0
- package/skills/calimero-merod/rules/init-before-run.md +40 -0
- package/skills/calimero-merod/rules/port-assignments.md +33 -0
- package/skills/calimero-node/SKILL.md +50 -39
- package/skills/calimero-node/references/context-lifecycle.md +34 -17
- package/skills/calimero-node/references/meroctl-commands.md +89 -99
- package/skills/calimero-node/rules/app-vs-context.md +4 -4
- package/skills/calimero-registry/SKILL.md +110 -31
- package/skills/calimero-registry/references/bundle-and-push.md +99 -34
- package/skills/calimero-registry/references/manifest-format.md +56 -35
- package/skills/calimero-registry/references/mero-sign.md +10 -9
- package/skills/calimero-registry/rules/key-security.md +3 -2
- package/skills/calimero-registry/rules/sign-before-pack.md +5 -5
- package/skills/calimero-rust-sdk/SKILL.md +154 -44
- package/skills/calimero-rust-sdk/references/blob-api.md +119 -0
- package/skills/calimero-rust-sdk/references/event-handlers.md +122 -0
- package/skills/calimero-rust-sdk/references/events.md +2 -1
- package/skills/calimero-rust-sdk/references/examples.md +81 -29
- package/skills/calimero-rust-sdk/references/migrations.md +123 -0
- package/skills/calimero-rust-sdk/references/nested-crdts.md +113 -0
- package/skills/calimero-rust-sdk/references/private-storage.md +76 -34
- package/skills/calimero-rust-sdk/references/state-collections.md +106 -21
- package/skills/calimero-rust-sdk/references/user-and-frozen-storage.md +169 -0
- package/skills/calimero-rust-sdk/rules/app-macro-placement.md +5 -2
- package/skills/calimero-rust-sdk/rules/no-std-collections.md +5 -2
- package/skills/calimero-rust-sdk/rules/state-derives.md +9 -10
- package/skills/calimero-rust-sdk/rules/wasm-constraints.md +12 -10
- package/skills/calimero-sdk-js/SKILL.md +34 -26
- package/skills/calimero-sdk-js/references/build-pipeline.md +6 -6
- package/skills/calimero-sdk-js/references/collections.md +11 -11
- package/skills/calimero-sdk-js/references/events.md +7 -3
- package/skills/calimero-sdk-js/rules/crdt-only-state.md +18 -18
- package/skills/calimero-sdk-js/rules/no-console-log.md +6 -6
- package/skills/calimero-sdk-js/rules/view-decorator.md +6 -4
|
@@ -6,144 +6,117 @@ Subscribe to real-time events emitted by the application running on the node.
|
|
|
6
6
|
|
|
7
7
|
The node sends two kinds of events to subscribers:
|
|
8
8
|
|
|
9
|
-
| type
|
|
10
|
-
|
|
11
|
-
| `StateMutation`
|
|
12
|
-
| `ExecutionEvent` | App emitted `app::emit!()`
|
|
9
|
+
| type | when | payload |
|
|
10
|
+
| ---------------- | ----------------------------------- | ------------------------------ |
|
|
11
|
+
| `StateMutation` | Another member mutated shared state | `{ newRoot: string }` |
|
|
12
|
+
| `ExecutionEvent` | App emitted `app::emit!()` | `{ events: ExecutionEvent[] }` |
|
|
13
13
|
|
|
14
|
-
An `ExecutionEvent`
|
|
14
|
+
An `ExecutionEvent` entry: `{ kind: string; data: any }` where `kind` matches the Rust event variant
|
|
15
|
+
name and `data` may be a byte array (UTF-8 JSON) or a plain object.
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## mero-react: useSubscription hook (preferred for React)
|
|
19
20
|
|
|
20
21
|
```typescript
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
import { useSubscription } from '@calimero-network/mero-react';
|
|
23
|
+
|
|
24
|
+
function MyComponent({ contextId }: { contextId: string }) {
|
|
25
|
+
useSubscription([contextId], (event: { contextId: string; data: unknown }) => {
|
|
26
|
+
const payload = event.data as any;
|
|
27
|
+
|
|
28
|
+
// ExecutionEvent: data has an `events` array
|
|
29
|
+
if (Array.isArray(payload?.events)) {
|
|
30
|
+
for (const e of payload.events) {
|
|
31
|
+
const data = decodeEventData(e.data);
|
|
32
|
+
switch (e.kind) {
|
|
33
|
+
case 'Inserted':
|
|
34
|
+
console.log('inserted', data);
|
|
35
|
+
break;
|
|
36
|
+
case 'Removed':
|
|
37
|
+
console.log('removed', data);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
// Byte-array payloads are JSON-encoded; decode them:
|
|
46
|
+
function decodeEventData(data: unknown): unknown {
|
|
47
|
+
if (Array.isArray(data) && data.every((n) => typeof n === 'number')) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(new TextDecoder().decode(new Uint8Array(data)));
|
|
50
|
+
} catch {
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
58
|
+
## Subscribe to multiple contexts
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// Subscribe to game context + lobby context simultaneously
|
|
62
|
+
useSubscription([gameContextId, lobbyContextId], (event) => {
|
|
63
|
+
console.log('event from context:', event.contextId);
|
|
35
64
|
});
|
|
36
65
|
```
|
|
37
66
|
|
|
38
67
|
---
|
|
39
68
|
|
|
40
|
-
##
|
|
69
|
+
## Legacy calimero-client: WsSubscriptionsClient
|
|
41
70
|
|
|
42
71
|
```typescript
|
|
43
|
-
import
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
72
|
+
import {
|
|
73
|
+
WsSubscriptionsClient,
|
|
74
|
+
getAppEndpointKey,
|
|
75
|
+
getContextId,
|
|
47
76
|
} from '@calimero-network/calimero-client';
|
|
48
77
|
|
|
49
|
-
ws
|
|
78
|
+
const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
|
|
79
|
+
await ws.connect();
|
|
80
|
+
ws.subscribe([getContextId()!]);
|
|
81
|
+
ws.addCallback((event) => {
|
|
50
82
|
if (event.type === 'ExecutionEvent') {
|
|
51
83
|
for (const e of event.data.events) {
|
|
52
|
-
|
|
53
|
-
// e.data contains the event payload
|
|
54
|
-
switch (e.kind) {
|
|
55
|
-
case 'ItemAdded':
|
|
56
|
-
console.log('Item added:', e.data);
|
|
57
|
-
addItemToUI(e.data.key, e.data.value);
|
|
58
|
-
break;
|
|
59
|
-
case 'ItemRemoved':
|
|
60
|
-
removeItemFromUI(e.data.key);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
84
|
+
console.log(e.kind, e.data);
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
|
|
66
87
|
if (event.type === 'StateMutation') {
|
|
67
|
-
// Another member changed shared state — refresh data from node
|
|
68
|
-
console.log('State root changed:', event.data.newRoot);
|
|
69
88
|
refreshDataFromNode();
|
|
70
89
|
}
|
|
71
90
|
});
|
|
72
91
|
```
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## Subscribe to multiple contexts
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
ws.subscribe([contextId1, contextId2]);
|
|
80
|
-
// Events include event.contextId to identify which context emitted them
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## Cleanup
|
|
93
|
+
Cleanup:
|
|
86
94
|
|
|
87
95
|
```typescript
|
|
88
|
-
// Remove a specific callback
|
|
89
96
|
ws.removeCallback(myCallback);
|
|
90
|
-
|
|
91
|
-
// Unsubscribe from specific contexts
|
|
92
97
|
ws.unsubscribe([contextId]);
|
|
93
|
-
|
|
94
|
-
// Close connection entirely
|
|
95
98
|
ws.disconnect();
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
---
|
|
99
102
|
|
|
100
|
-
##
|
|
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
|
-
|
|
133
|
-
## React hook pattern
|
|
103
|
+
## React hook for legacy client
|
|
134
104
|
|
|
135
105
|
```typescript
|
|
136
106
|
import { useEffect } from 'react';
|
|
137
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
WsSubscriptionsClient,
|
|
109
|
+
getAppEndpointKey,
|
|
110
|
+
getContextId,
|
|
111
|
+
} from '@calimero-network/calimero-client';
|
|
138
112
|
|
|
139
|
-
function useNodeEvents(onEvent: (event:
|
|
113
|
+
function useNodeEvents(onEvent: (event: any) => void) {
|
|
140
114
|
useEffect(() => {
|
|
141
115
|
const nodeUrl = getAppEndpointKey();
|
|
142
116
|
const contextId = getContextId();
|
|
143
117
|
if (!nodeUrl || !contextId) return;
|
|
144
118
|
|
|
145
119
|
const ws = new WsSubscriptionsClient(nodeUrl, '/ws');
|
|
146
|
-
|
|
147
120
|
ws.connect().then(() => {
|
|
148
121
|
ws.subscribe([contextId]);
|
|
149
122
|
ws.addCallback(onEvent);
|
|
@@ -157,3 +130,11 @@ function useNodeEvents(onEvent: (event: NodeEvent) => void) {
|
|
|
157
130
|
}, [onEvent]);
|
|
158
131
|
}
|
|
159
132
|
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Connection notes
|
|
137
|
+
|
|
138
|
+
- `WsSubscriptionsClient` does **not** reconnect automatically — handle reconnect manually
|
|
139
|
+
- `useSubscription` from `mero-react` manages the connection lifecycle for you
|
|
140
|
+
- WebSocket tokens are **not** auto-refreshed unlike `rpcClient` HTTP calls
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# Rule: mero-js v2 uses camelCase — not snake_case
|
|
2
2
|
|
|
3
|
-
`@calimero-network/mero-js` v2 (`>=2.0.0-beta.1`) changed all request object field names from
|
|
3
|
+
`@calimero-network/mero-js` v2 (`>=2.0.0-beta.1`) changed all request object field names from
|
|
4
|
+
`snake_case` to `camelCase`.
|
|
4
5
|
|
|
5
6
|
## WRONG (v1 / old code):
|
|
6
7
|
|
|
7
8
|
```typescript
|
|
8
9
|
await client.generateClientKey({
|
|
9
|
-
context_id: contextId,
|
|
10
|
-
context_identity: identity,
|
|
10
|
+
context_id: contextId, // ✗
|
|
11
|
+
context_identity: identity, // ✗
|
|
11
12
|
});
|
|
12
13
|
```
|
|
13
14
|
|
|
@@ -15,8 +16,8 @@ await client.generateClientKey({
|
|
|
15
16
|
|
|
16
17
|
```typescript
|
|
17
18
|
await client.generateClientKey({
|
|
18
|
-
contextId: contextId,
|
|
19
|
-
contextIdentity: identity,
|
|
19
|
+
contextId: contextId, // ✓
|
|
20
|
+
contextIdentity: identity, // ✓
|
|
20
21
|
});
|
|
21
22
|
```
|
|
22
23
|
|
|
@@ -27,11 +28,13 @@ await client.generateClientKey({
|
|
|
27
28
|
|
|
28
29
|
## Why this matters
|
|
29
30
|
|
|
30
|
-
The v2 mero-js package does not alias the old snake_case keys. Passing `context_id` silently
|
|
31
|
-
|
|
31
|
+
The v2 mero-js package does not alias the old snake_case keys. Passing `context_id` silently sends
|
|
32
|
+
`undefined` to the server, which returns a cryptic error rather than a clear "wrong field name"
|
|
33
|
+
message.
|
|
32
34
|
|
|
33
35
|
## How to detect if a codebase is on v1 or v2
|
|
34
36
|
|
|
35
37
|
Check `package.json`:
|
|
38
|
+
|
|
36
39
|
- `"@calimero-network/mero-js": "^2.0.0-beta.1"` or higher → v2, use camelCase
|
|
37
40
|
- `"@calimero-network/calimero-client": "..."` → original client, check its docs
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Rule: rpcClient handles token refresh automatically — but not all 401s
|
|
2
2
|
|
|
3
|
-
`rpcClient` (from `@calimero-network/calimero-client`) automatically retries with a
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`rpcClient` (from `@calimero-network/calimero-client`) automatically retries with a refreshed token
|
|
4
|
+
when the server returns `401 token_expired`. **You do not need to implement a refresh wrapper around
|
|
5
|
+
`rpcClient.execute()` calls.**
|
|
6
6
|
|
|
7
7
|
What you DO need to handle: 401s that cannot be refreshed.
|
|
8
8
|
|
|
@@ -49,8 +49,8 @@ if (response.error) {
|
|
|
49
49
|
|
|
50
50
|
## WebSocket connections are different — no auto-refresh
|
|
51
51
|
|
|
52
|
-
`WsSubscriptionsClient` does **not** auto-refresh tokens. If the token expires while
|
|
53
|
-
|
|
52
|
+
`WsSubscriptionsClient` does **not** auto-refresh tokens. If the token expires while a WebSocket
|
|
53
|
+
connection is open, events will stop arriving silently. Reconnect manually:
|
|
54
54
|
|
|
55
55
|
```typescript
|
|
56
56
|
async function connectWithAuth() {
|
|
@@ -71,9 +71,9 @@ async function connectWithAuth() {
|
|
|
71
71
|
|
|
72
72
|
## Summary
|
|
73
73
|
|
|
74
|
-
| Scenario
|
|
75
|
-
|
|
|
76
|
-
| `401 token_expired` on RPC call
|
|
77
|
-
| `401 token_revoked` on RPC call
|
|
78
|
-
| `401 invalid_token` on RPC call
|
|
79
|
-
| Auth failure on WebSocket connect | You — catch and redirect to login
|
|
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 |
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
# calimero-client-py — Agent Instructions
|
|
2
2
|
|
|
3
|
-
You are helping a developer use the **Calimero Python client** (`calimero-client-py`) to
|
|
4
|
-
|
|
3
|
+
You are helping a developer use the **Calimero Python client** (`calimero-client-py`) to interact
|
|
4
|
+
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
|
-
>
|
|
8
|
-
>
|
|
6
|
+
> **NOT this skill** if the developer is building the application logic that runs on the node — that
|
|
7
|
+
> is `calimero-rust-sdk` (Rust/WASM) or `calimero-sdk-js` (TypeScript/WASM). This skill is for
|
|
8
|
+
> Python code that _calls_ the node from outside.
|
|
9
9
|
|
|
10
10
|
## What it is
|
|
11
11
|
|
|
12
12
|
`calimero-client-py` is a Python package built with PyO3 (Rust bindings). It provides:
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
- Full API for managing contexts, applications, identities, blobs, namespaces, and groups
|
|
14
15
|
- Automatic JWT token caching and refresh (tokens stored in `~/.merobox/auth_cache/`)
|
|
15
16
|
- A CLI for common node operations
|
|
16
17
|
|
|
18
|
+
> **All methods are synchronous** — the Tokio runtime is managed internally by the Rust bindings. Do
|
|
19
|
+
> NOT use `await` with any client methods.
|
|
20
|
+
|
|
17
21
|
## Install
|
|
18
22
|
|
|
19
23
|
```bash
|
|
@@ -21,6 +25,7 @@ pip install calimero-client-py
|
|
|
21
25
|
```
|
|
22
26
|
|
|
23
27
|
**Build from source (requires Rust 1.70+ and maturin):**
|
|
28
|
+
|
|
24
29
|
```bash
|
|
25
30
|
pip install maturin
|
|
26
31
|
git clone https://github.com/calimero-network/calimero-client-py
|
|
@@ -34,24 +39,31 @@ maturin develop --release
|
|
|
34
39
|
import asyncio
|
|
35
40
|
from calimero_client_py import create_connection, create_client
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
def main():
|
|
38
43
|
connection = create_connection(
|
|
39
44
|
api_url="http://localhost:2428",
|
|
40
45
|
node_name="my-local-node" # must be stable — see rules/
|
|
41
46
|
)
|
|
42
47
|
client = create_client(connection)
|
|
43
|
-
contexts =
|
|
48
|
+
contexts = client.list_contexts() # no await — methods are synchronous
|
|
44
49
|
print(contexts)
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
main()
|
|
47
52
|
```
|
|
48
53
|
|
|
49
54
|
## Critical: `node_name` must be stable
|
|
50
55
|
|
|
51
|
-
`node_name` is used to derive the token cache filename. If it changes between runs,
|
|
52
|
-
|
|
56
|
+
`node_name` is used to derive the token cache filename. If it changes between runs, the client loses
|
|
57
|
+
its stored tokens and will need to re-authenticate.
|
|
58
|
+
|
|
59
|
+
## Related skills
|
|
60
|
+
|
|
61
|
+
- **`calimero-core`** — context/app model and JSON-RPC protocol that the Python client wraps
|
|
62
|
+
- **`calimero-merod`** — `merod` daemon setup if the developer is also running the node
|
|
63
|
+
- **`calimero-meroctl`** — `meroctl` CLI reference for quick manual operations alongside Python
|
|
64
|
+
scripts
|
|
53
65
|
|
|
54
66
|
## References
|
|
55
67
|
|
|
56
|
-
See `references/` for the full API, authentication flow, and CLI commands.
|
|
57
|
-
|
|
68
|
+
See `references/` for the full API, authentication flow, and CLI commands. See `rules/` for the
|
|
69
|
+
`node_name` stability requirement and sync API usage.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Python Client API Reference
|
|
2
2
|
|
|
3
|
+
> All methods are **synchronous** — no `await` needed. The async Tokio runtime is managed internally
|
|
4
|
+
> by the Rust bindings.
|
|
5
|
+
|
|
3
6
|
## Setup
|
|
4
7
|
|
|
5
8
|
```python
|
|
@@ -16,52 +19,55 @@ client = create_client(connection)
|
|
|
16
19
|
|
|
17
20
|
```python
|
|
18
21
|
# List all contexts
|
|
19
|
-
contexts =
|
|
22
|
+
contexts = client.list_contexts()
|
|
20
23
|
|
|
21
24
|
# Get a specific context
|
|
22
|
-
context =
|
|
25
|
+
context = client.get_context(context_id="abc123")
|
|
23
26
|
|
|
24
27
|
# Create a context (instance of an installed app)
|
|
25
|
-
context =
|
|
28
|
+
context = client.create_context(
|
|
26
29
|
application_id="app-id",
|
|
27
30
|
protocol="near", # "near" | "ethereum" | "icp" | "starknet"
|
|
28
31
|
params=None # optional JSON string
|
|
29
32
|
)
|
|
30
33
|
|
|
31
34
|
# Delete a context
|
|
32
|
-
|
|
35
|
+
client.delete_context(context_id="abc123")
|
|
36
|
+
|
|
37
|
+
# Join a context (by context ID)
|
|
38
|
+
client.join_context(context_id="abc123")
|
|
33
39
|
|
|
34
40
|
# Sync a context with peers
|
|
35
|
-
|
|
41
|
+
client.sync_context(context_id="abc123")
|
|
36
42
|
|
|
37
43
|
# Sync all contexts
|
|
38
|
-
|
|
44
|
+
client.sync_all_contexts()
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
## Application management
|
|
42
48
|
|
|
43
49
|
```python
|
|
44
50
|
# List installed apps
|
|
45
|
-
apps =
|
|
51
|
+
apps = client.list_applications()
|
|
46
52
|
|
|
47
53
|
# Get a specific app
|
|
48
|
-
app =
|
|
54
|
+
app = client.get_application(app_id="app-id")
|
|
49
55
|
|
|
50
56
|
# Install from URL (registry or direct)
|
|
51
|
-
result =
|
|
57
|
+
result = client.install_application(
|
|
52
58
|
url="https://registry.calimero.network/myapp/1.0.0",
|
|
53
59
|
hash=None, # optional content hash for verification
|
|
54
60
|
metadata=None # optional bytes
|
|
55
61
|
)
|
|
56
62
|
|
|
57
63
|
# Install from local path (dev mode)
|
|
58
|
-
result =
|
|
64
|
+
result = client.install_dev_application(
|
|
59
65
|
path="./target/wasm32-unknown-unknown/release/myapp.wasm",
|
|
60
66
|
metadata=None
|
|
61
67
|
)
|
|
62
68
|
|
|
63
69
|
# Uninstall
|
|
64
|
-
|
|
70
|
+
client.uninstall_application(app_id="app-id")
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
## Calling app methods
|
|
@@ -70,7 +76,7 @@ await client.uninstall_application(app_id="app-id")
|
|
|
70
76
|
import json
|
|
71
77
|
|
|
72
78
|
# Execute a method (mutation or view)
|
|
73
|
-
result =
|
|
79
|
+
result = client.execute_function(
|
|
74
80
|
context_id="ctx-id",
|
|
75
81
|
method="set",
|
|
76
82
|
args=json.dumps({"key": "hello", "value": "world"}),
|
|
@@ -78,59 +84,51 @@ result = await client.execute_function(
|
|
|
78
84
|
)
|
|
79
85
|
```
|
|
80
86
|
|
|
81
|
-
## Context membership
|
|
82
|
-
|
|
83
|
-
```python
|
|
84
|
-
# Invite a member
|
|
85
|
-
invitation = await client.invite_to_context(
|
|
86
|
-
context_id="ctx-id",
|
|
87
|
-
inviter_id="inviter-public-key",
|
|
88
|
-
invitee_id="invitee-public-key"
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# Join a context (on the invitee's node)
|
|
92
|
-
await client.join_context(
|
|
93
|
-
context_id="ctx-id",
|
|
94
|
-
invitee_id="invitee-public-key",
|
|
95
|
-
invitation_payload=invitation["payload"]
|
|
96
|
-
)
|
|
97
|
-
```
|
|
98
|
-
|
|
99
87
|
## Identity management
|
|
100
88
|
|
|
101
89
|
```python
|
|
102
90
|
# Generate a new context identity
|
|
103
|
-
identity =
|
|
91
|
+
identity = client.generate_context_identity()
|
|
104
92
|
# Returns: { "public_key": "...", "private_key": "..." }
|
|
105
93
|
|
|
106
94
|
# List identities for a context
|
|
107
|
-
identities =
|
|
95
|
+
identities = client.get_context_identities(context_id="ctx-id")
|
|
108
96
|
|
|
109
97
|
# Get client keys for a context
|
|
110
|
-
keys =
|
|
98
|
+
keys = client.get_context_client_keys(context_id="ctx-id")
|
|
111
99
|
```
|
|
112
100
|
|
|
113
101
|
## Blob management
|
|
114
102
|
|
|
115
103
|
```python
|
|
116
|
-
blobs =
|
|
117
|
-
info =
|
|
118
|
-
|
|
104
|
+
blobs = client.list_blobs()
|
|
105
|
+
info = client.get_blob_info(blob_id="blob-id")
|
|
106
|
+
client.delete_blob(blob_id="blob-id")
|
|
107
|
+
|
|
108
|
+
# Upload bytes
|
|
109
|
+
result = client.upload_blob(data=b"hello", context_id=None)
|
|
110
|
+
|
|
111
|
+
# Download bytes
|
|
112
|
+
data = client.download_blob(blob_id="blob-id", context_id=None)
|
|
119
113
|
```
|
|
120
114
|
|
|
121
115
|
## Alias management
|
|
122
116
|
|
|
123
117
|
```python
|
|
124
118
|
# Create aliases for easier reference
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
client.create_context_alias(alias="my-context", context_id="ctx-id")
|
|
120
|
+
client.create_application_alias(alias="my-app", application_id="app-id")
|
|
121
|
+
client.create_context_identity_alias(
|
|
128
122
|
context_id="ctx-id", alias="alice", public_key="pubkey"
|
|
129
123
|
)
|
|
130
124
|
|
|
131
125
|
# Delete aliases
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
client.delete_context_alias(alias="my-context")
|
|
127
|
+
client.delete_context_identity_alias(alias="alice", context_id="ctx-id")
|
|
128
|
+
|
|
129
|
+
# Lookup / resolve aliases
|
|
130
|
+
client.lookup_context_alias(alias="my-context")
|
|
131
|
+
client.resolve_context_alias(alias="my-context")
|
|
134
132
|
```
|
|
135
133
|
|
|
136
134
|
## Utility
|
|
@@ -140,8 +138,8 @@ await client.delete_context_identity_alias(alias="alice", context_id="ctx-id")
|
|
|
140
138
|
url = client.get_api_url()
|
|
141
139
|
|
|
142
140
|
# Get connected peers count
|
|
143
|
-
peers =
|
|
141
|
+
peers = client.get_peers_count()
|
|
144
142
|
|
|
145
143
|
# Get context storage info
|
|
146
|
-
storage =
|
|
144
|
+
storage = client.get_context_storage(context_id="ctx-id")
|
|
147
145
|
```
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Authentication & Token Caching
|
|
2
2
|
|
|
3
|
-
The Python client automatically manages JWT tokens — loading from cache, using them in
|
|
4
|
-
|
|
3
|
+
The Python client automatically manages JWT tokens — loading from cache, using them in requests, and
|
|
4
|
+
refreshing on 401.
|
|
5
5
|
|
|
6
6
|
## Token cache location
|
|
7
7
|
|
|
8
|
-
```
|
|
8
|
+
```text
|
|
9
9
|
~/.merobox/auth_cache/{sanitized_node_name}-{hash}.json
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -18,8 +18,8 @@ path = get_token_cache_path("my-node")
|
|
|
18
18
|
|
|
19
19
|
## First-time authentication
|
|
20
20
|
|
|
21
|
-
On the first run there are no cached tokens. You need to complete an auth flow (login via
|
|
22
|
-
|
|
21
|
+
On the first run there are no cached tokens. You need to complete an auth flow (login via the admin
|
|
22
|
+
dashboard or meroctl) and then seed the cache manually:
|
|
23
23
|
|
|
24
24
|
```python
|
|
25
25
|
import json
|
|
@@ -54,8 +54,8 @@ client = create_client(connection)
|
|
|
54
54
|
|
|
55
55
|
## Token refresh
|
|
56
56
|
|
|
57
|
-
The client automatically retries with a refreshed token on `401 Unauthorized`.
|
|
58
|
-
|
|
57
|
+
The client automatically retries with a refreshed token on `401 Unauthorized`. Refreshed tokens are
|
|
58
|
+
written back to the cache file automatically.
|
|
59
59
|
|
|
60
60
|
## CI / automated environments
|
|
61
61
|
|
|
@@ -1,52 +1,48 @@
|
|
|
1
|
-
# Rule: All client methods are
|
|
1
|
+
# Rule: All client methods are synchronous — do NOT use await
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Despite being built on an async Rust/Tokio runtime internally, all `client.*` methods are
|
|
4
|
+
**synchronous** from Python's perspective. Do NOT use `async`/`await`.
|
|
5
5
|
|
|
6
6
|
## WRONG:
|
|
7
7
|
|
|
8
8
|
```python
|
|
9
|
-
# ✗
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# ✗ Calling outside async context
|
|
14
|
-
def main():
|
|
15
|
-
client.list_contexts() # won't run
|
|
9
|
+
# ✗ Adding await — methods are not coroutines
|
|
10
|
+
async def main():
|
|
11
|
+
contexts = await client.list_contexts() # TypeError: object is not awaitable
|
|
16
12
|
```
|
|
17
13
|
|
|
18
14
|
## CORRECT:
|
|
19
15
|
|
|
20
16
|
```python
|
|
21
|
-
# ✓
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
# ✓ Call methods directly — no await needed
|
|
18
|
+
def main():
|
|
19
|
+
conn = create_connection(api_url="http://localhost:2428", node_name="dev")
|
|
20
|
+
client = create_client(conn)
|
|
21
|
+
contexts = client.list_contexts() # synchronous, returns result directly
|
|
24
22
|
print(contexts)
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
main()
|
|
27
25
|
```
|
|
28
26
|
|
|
29
27
|
## In scripts
|
|
30
28
|
|
|
31
29
|
```python
|
|
32
|
-
import asyncio
|
|
33
30
|
from calimero_client_py import create_connection, create_client
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
asyncio.run(main())
|
|
31
|
+
import json
|
|
32
|
+
|
|
33
|
+
conn = create_connection(api_url="http://localhost:2428", node_name="dev")
|
|
34
|
+
client = create_client(conn)
|
|
35
|
+
|
|
36
|
+
result = client.execute_function(
|
|
37
|
+
context_id="ctx-id",
|
|
38
|
+
method="get",
|
|
39
|
+
args=json.dumps({"key": "hello"}),
|
|
40
|
+
executor_public_key="pubkey"
|
|
41
|
+
)
|
|
42
|
+
print(result)
|
|
47
43
|
```
|
|
48
44
|
|
|
49
|
-
## Note
|
|
45
|
+
## Note
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
The Tokio async runtime is embedded in the Rust bindings and blocks internally. From Python's view,
|
|
48
|
+
every call is a normal blocking function call.
|