@elisym/cli 0.27.0 → 0.28.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisym/cli",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "CLI agent runner for elisym - provider mode, skills, crash recovery",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "bun run build && node scripts/preflight-publish.mjs"
45
45
  },
46
46
  "dependencies": {
47
- "@elisym/sdk": "~0.33.0",
47
+ "@elisym/sdk": "~0.34.0",
48
48
  "@solana-program/memo": "~0.11.0",
49
49
  "@solana-program/system": "~0.12.0",
50
50
  "@solana-program/token": "~0.5.0",
@@ -21,6 +21,9 @@ There is deliberately no checked-in example for `mode: x402` (an x402 bridge ski
21
21
  | [static-welcome](./static-welcome/) | 0.05 USDC | static-file | Sells a fixed Markdown welcome doc. No input box on the buyer side, just a Buy button |
22
22
  | [static-now](./static-now/) | 0.01 USDC | static-script | Returns the current UTC timestamp from a 1-line shell script. No LLM, no input |
23
23
  | [uppercase-proxy](./uppercase-proxy/) | 0.01 USDC | dynamic-script | Pipes the buyer's text to a script (`tr a-z A-Z`). Skeleton for crypto-paid model proxies |
24
+ | [sol-transfer-call](./sol-transfer-call/) | 0.01 USDC | onchain | Builds an UNSIGNED Solana transfer the buyer signs with their own wallet |
25
+
26
+ `sol-transfer-call` is deliberately priced in USDC while its call moves SOL: what a capability charges and what its ceilings are denominated in are independent.
24
27
 
25
28
  Every paid example is **priced in USDC on Solana devnet**; `general-assistant` is the only free one. Paid skills publish a payment requirement with their capability card and only run after the customer's on-chain transfer is confirmed. To make one free, drop `price` and `token` from its frontmatter; to switch to SOL, set `token: sol` and price in SOL.
26
29
 
@@ -31,6 +34,7 @@ The last three rows skip the LLM entirely. Set `mode:` in the frontmatter:
31
34
  - `mode: static-file` + `output_file:` - sells the literal contents of a file. No input box on the buyer side; the webapp shows just a Buy button.
32
35
  - `mode: static-script` + `script:` - runs a script with no stdin per purchase, returns its stdout. Same "Buy only" UI.
33
36
  - `mode: dynamic-script` + `script:` - the buyer's text is piped to the script's stdin, stdout becomes the result. The buyer still sees an input box; the agent is just not an LLM. This is the right shape for a crypto-paid proxy in front of any HTTP-callable model.
37
+ - `mode: onchain` + `script:` + an `onchain:` block - the script builds a **Solana call** and the BUYER signs it. The agent never signs and never takes custody; what it sells is knowing how to shape the call. The `onchain:` block is a public promise checked by every client before a wallet is asked, so `programs` must list every program the call touches. See [`SKILLS.md`](../SKILLS.md) for the schema and [the provider guide](../../docs/pages/providers/onchain-calls.mdx) for the whole path.
34
38
 
35
39
  When every loaded skill is non-LLM, `npx @elisym/cli start` does not require an `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`.
36
40
 
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: sol-transfer-call
3
+ description: Builds an unsigned Solana transfer for you to sign yourself - this agent never holds your funds. Put YOUR wallet address first in the message, then the destination, then the amount, for example "from <your address> to <their address>, 0.01 SOL".
4
+ capabilities:
5
+ - onchain-call
6
+ - transfer
7
+ price: 0.01
8
+ token: usdc
9
+ mode: onchain
10
+ script: ./scripts/build_transfer.py
11
+ onchain:
12
+ kind: transfer
13
+ # Quoted: the System program's address is all digits, and bare YAML reads it as a number.
14
+ programs:
15
+ - '11111111111111111111111111111111'
16
+ token: sol
17
+ max_per_call: '0.1'
18
+ params:
19
+ - { name: wallet, type: address, required: true, description: The wallet that signs and pays }
20
+ - { name: destination, type: address, required: true, description: Where the SOL goes }
21
+ - { name: amount, type: amount, required: true, description: How much SOL to send }
22
+ ---
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """Build an UNSIGNED Solana transfer for the customer to sign.
3
+
4
+ mode: onchain - the buyer's text arrives on stdin, one call envelope goes to
5
+ stdout. This agent never signs and never holds the funds; it only knows how to
6
+ shape the call. Standard library only, so the skill runs anywhere python does.
7
+
8
+ The buyer's text is the ONLY input: no wallet address is handed to a provider,
9
+ so the signer has to be named in the message, and so does the destination.
10
+ """
11
+
12
+ import base64
13
+ import json
14
+ import os
15
+ import re
16
+ import struct
17
+ import sys
18
+ import time
19
+ import urllib.request
20
+
21
+ SYSTEM_PROGRAM = "11111111111111111111111111111111"
22
+ TRANSFER_DISCRIMINATOR = 2
23
+ LAMPORTS_PER_SOL = 1_000_000_000
24
+ CALL_TTL_SECONDS = 600
25
+ B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
26
+
27
+ NETWORK = os.environ.get("ELISYM_NETWORK", "devnet")
28
+ RPC_URL = os.environ.get(
29
+ "SOLANA_RPC_URL",
30
+ "https://api.devnet.solana.com" if NETWORK == "devnet" else "https://api.mainnet-beta.solana.com",
31
+ )
32
+
33
+
34
+ def fail(message):
35
+ print(message, file=sys.stderr)
36
+ sys.exit(1)
37
+
38
+
39
+ def b58decode(text):
40
+ number = 0
41
+ for character in text:
42
+ index = B58_ALPHABET.find(character)
43
+ if index < 0:
44
+ fail(f"'{text}' is not a Solana address")
45
+ number = number * 58 + index
46
+ decoded = number.to_bytes(32, "big") if number.bit_length() <= 256 else b""
47
+ leading_zeros = len(text) - len(text.lstrip("1"))
48
+ body = decoded[leading_zeros:] if leading_zeros else decoded
49
+ result = b"\x00" * leading_zeros + body
50
+ if len(result) != 32:
51
+ fail(f"'{text}' is not a 32-byte Solana address")
52
+ return result
53
+
54
+
55
+ def compact_u16(value):
56
+ out = bytearray()
57
+ while True:
58
+ byte = value & 0x7F
59
+ value >>= 7
60
+ if value:
61
+ out.append(byte | 0x80)
62
+ else:
63
+ out.append(byte)
64
+ return bytes(out)
65
+
66
+
67
+ def latest_blockhash():
68
+ payload = json.dumps(
69
+ {"jsonrpc": "2.0", "id": 1, "method": "getLatestBlockhash", "params": [{"commitment": "confirmed"}]}
70
+ ).encode()
71
+ request = urllib.request.Request(RPC_URL, data=payload, headers={"Content-Type": "application/json"})
72
+ with urllib.request.urlopen(request, timeout=20) as response:
73
+ body = json.load(response)
74
+ if "result" not in body:
75
+ fail(f"the cluster would not give a blockhash: {body.get('error')}")
76
+ return body["result"]["value"]["blockhash"]
77
+
78
+
79
+ def build_wire_transaction(payer, destination, lamports, blockhash):
80
+ """A v0 message with one System transfer, and an empty signature slot."""
81
+ if payer == destination:
82
+ fail("the destination is the signer's own wallet, so this call would move nothing")
83
+ keys = [b58decode(payer), b58decode(destination), b58decode(SYSTEM_PROGRAM)]
84
+
85
+ message = bytearray()
86
+ message.append(0x80) # v0
87
+ message += bytes([1, 0, 1]) # 1 signer, 0 readonly signed, 1 readonly unsigned
88
+ message += compact_u16(len(keys))
89
+ for key in keys:
90
+ message += key
91
+ message += b58decode(blockhash)
92
+
93
+ data = struct.pack("<IQ", TRANSFER_DISCRIMINATOR, lamports)
94
+ message += compact_u16(1) # one instruction
95
+ message.append(2) # program: the System program, at index 2
96
+ message += compact_u16(2) + bytes([0, 1]) # accounts: payer, destination
97
+ message += compact_u16(len(data)) + data
98
+ message += compact_u16(0) # no address lookup tables
99
+
100
+ return base64.b64encode(compact_u16(1) + bytes(64) + bytes(message)).decode()
101
+
102
+
103
+ def parse_request(text):
104
+ addresses = re.findall(r"\b[1-9A-HJ-NP-Za-km-z]{32,44}\b", text)
105
+ if len(addresses) < 2:
106
+ fail(
107
+ "name two Solana addresses: your own wallet first (it signs and pays), "
108
+ "then where the SOL should go"
109
+ )
110
+ amounts = re.findall(r"(\d+(?:\.\d+)?)\s*(?:sol\b|$)", text, flags=re.IGNORECASE)
111
+ if not amounts:
112
+ fail("say how much SOL to send, for example '0.01 SOL'")
113
+ lamports = int(round(float(amounts[0]) * LAMPORTS_PER_SOL))
114
+ if lamports <= 0:
115
+ fail("the amount has to be more than zero")
116
+ return addresses[0], addresses[1], lamports
117
+
118
+
119
+ def main():
120
+ signer, destination, lamports = parse_request(sys.stdin.read())
121
+ envelope = {
122
+ "elisym_call": "v1",
123
+ "network": NETWORK,
124
+ "transaction": build_wire_transaction(signer, destination, lamports, latest_blockhash()),
125
+ "signer": signer,
126
+ "expires_at": int(time.time()) + CALL_TTL_SECONDS,
127
+ "explain": [
128
+ {
129
+ "kind": "transfer",
130
+ "asset": "sol",
131
+ "amount": f"{lamports / LAMPORTS_PER_SOL:g}",
132
+ "to": destination,
133
+ }
134
+ ],
135
+ }
136
+ json.dump(envelope, sys.stdout)
137
+
138
+
139
+ main()