@arp-protocol/cli 0.0.0-stage → 0.0.1

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
@@ -1,3 +1,69 @@
1
- # Temporary Holding Version
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Wieedze/ARP/main/docs/design/brand/arp-lockup-dark.svg" />
3
+ <img src="https://raw.githubusercontent.com/Wieedze/ARP/main/docs/design/brand/arp-lockup-light.svg" height="40" alt="ARP" />
4
+ </picture>
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @arp-protocol/cli
7
+
8
+ ARP's agent-side programs in one binary. Node 22 or later.
9
+
10
+ ```
11
+ arp mcp the MCP server, over stdio (how an MCP client launches it). --http for 127.0.0.1
12
+ arp hook the runtime hook: one payload on stdin, one line in .arp/session.jsonl
13
+ arp node the hire endpoint. Opt-in: 127.0.0.1, reached through a tunnel you rent
14
+ arp init --node bind the node to your ERC-8004 agent: the token's owner signs once
15
+ ```
16
+
17
+ All three run on your machine, and none of them signs a transaction.
18
+
19
+ - **What each one reads, writes and connects to** is in
20
+ [`docs/22_RUNNING_THE_NODE.md`](https://github.com/Wieedze/ARP/blob/main/docs/22_RUNNING_THE_NODE.md).
21
+ - **What the hook records**, and what it never records, is
22
+ [ADR `0090`](https://github.com/Wieedze/ARP/blob/main/.claude/choices/0090-what-the-hook-records.md).
23
+
24
+ ## The Claude Code plugin
25
+
26
+ The plugin carries this program as one file, `bin/arp.js`, so it needs Node 22 and nothing from npm:
27
+
28
+ ```
29
+ claude plugin marketplace add <a checkout of this repo, or owner/repo>
30
+ claude plugin install arp@arp
31
+ ```
32
+
33
+ With no `arp` on `PATH`, its commands stand in for `arp share` and `arp line`. A person types them, and the model cannot invoke them:
34
+
35
+ A session that allows `Bash` can still run the file itself: that rests on your permissions.
36
+
37
+ ```
38
+ /arp:share what would be public, and what stays here. Sends nothing
39
+ /arp:share-on turn sharing on (off by default). Sends nothing
40
+ /arp:share-off turn it off
41
+ /arp:share-send write the shared failures as public lines on testnet. Needs a sponsoring service
42
+ /arp:line write one public line on testnet: --tool <url> --outcome worked|failed
43
+ ```
44
+
45
+ The search in `arp_pulse` still runs `npx skills`: `npx` comes with Node, and that package comes from the npm registry. Installed from a local path, the plugin is read from that path, so keep the folder where it is.
46
+
47
+ ## Install into your runtime
48
+
49
+ ```
50
+ arp init --dry-run see what would change
51
+ arp init do it: asks first, backs up every file it edits
52
+ arp doctor check it: entries present, hook in place, the server answers
53
+ ```
54
+
55
+ `init` finds Claude Code, Grok Build, Cursor, Gemini CLI, Codex and Copilot (VS Code, CLI) on this machine. It adds the MCP
56
+ server to each one, and the hook to Claude Code (Grok Build reads Claude Code's files). For any
57
+ other MCP client, `arp init --print` prints the entry to paste. `--yes` and `--no-input` are for
58
+ scripts, `--runtime claude,cursor` narrows the run, and `--project` writes the project's files.
59
+
60
+ `init` does not replace an MCP server named `arp` that it did not write. It leaves it, says so, does the rest and exits 6. Rename yours, or pass `--force` to replace it; a file is backed up first. Through the `claude` command there is no backup, so the old entry is printed before it is removed.
61
+
62
+ ## Uninstall
63
+
64
+ ```
65
+ arp uninstall removes what init added, and nothing else
66
+ npm uninstall -g @arp-protocol/cli
67
+ ```
68
+
69
+ The `.arp/` directory in each project is the hook's log. Delete it if you want it gone.
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ SKILL_DOMAINS,
4
+ classifySkill,
5
+ inferDomain,
6
+ readRequires
7
+ } from "./cli-74hv4etk.js";
8
+ import"./cli-c8568g4c.js";
9
+ import"./cli-v446mw5v.js";
10
+ import"./cli-0hqxver6.js";
11
+ import"./cli-d3dy7cxe.js";
12
+ import"./cli-3kbbwfbp.js";
13
+ import"./cli-g5zvr10v.js";
14
+
15
+ // ../hook/src/backfill.ts
16
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
17
+ import { homedir as homedir2 } from "node:os";
18
+ import { basename, join as join2 } from "node:path";
19
+
20
+ // ../hook/src/classify.ts
21
+ var CANCELLED = /\b(interrupted|cancel(l)?ed|aborted) by (the )?user\b|\buser (rejected|denied|declined)\b|\bpermission denied by user\b|\buser doesn't want to (proceed|take this action)\b/i;
22
+ function isCancelled(text) {
23
+ return CANCELLED.test(text);
24
+ }
25
+
26
+ // ../hook/src/append.ts
27
+ import { closeSync, constants, lstatSync, openSync, writeSync } from "node:fs";
28
+ import { dirname } from "node:path";
29
+ function appendOwn(path, text) {
30
+ if (lstatSync(dirname(path)).isSymbolicLink())
31
+ throw new Error(`${dirname(path)} is a symbolic link`);
32
+ const flags = constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0);
33
+ const fd = openSync(path, flags, 384);
34
+ try {
35
+ writeSync(fd, text);
36
+ } finally {
37
+ closeSync(fd);
38
+ }
39
+ }
40
+
41
+ // ../hook/src/skill-failure.ts
42
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
43
+ import { homedir } from "node:os";
44
+ import { dirname as dirname2, join } from "node:path";
45
+ var MISSING_KEY = /\b(api[ _-]?key|access[ _-]?token|credentials?)\b.{0,40}\b(missing|not set|not found|required|invalid|expired)\b|(?<!\bnot )\b(missing|no|invalid|expired)\b.{0,20}\b(api[ _-]?key|access[ _-]?token|credentials?)\b|\b(HTTP|status)(\/[\d.]+| code)?[ :=]*(401|403)\b|\b(401|403) (unauthori[sz]ed|forbidden)\b|\bunauthori[sz]ed\b|not authenticated|not logged in/i;
46
+ var MISSING_KEY_VARIABLE = /\b[A-Z0-9_]*(API_KEY|TOKEN|SECRET)\b.{0,30}\b(not set|missing|required|unset)\b/;
47
+ var MISSING_PROGRAM = /command not found|executable file not found|\bnot installed\b|No module named|spawn \S+ ENOENT|Cannot find module/i;
48
+ var MISSING_FILE = /No such file or directory|\bENOENT\b|file not found|\b(file|path|directory|folder)\b(?!\.).{0,80}\bdoes not exist\b/i;
49
+ function failureReason(text) {
50
+ if (MISSING_PROGRAM.test(text))
51
+ return "missing-program";
52
+ if (MISSING_KEY.test(text) || MISSING_KEY_VARIABLE.test(text))
53
+ return "missing-key";
54
+ if (MISSING_FILE.test(text))
55
+ return "missing-file";
56
+ return null;
57
+ }
58
+ var RUNNERS = new Set(["bash", "bun", "bunx", "node", "npx", "python", "python3", "sh", "uv", "uvx", "zsh"]);
59
+ function programsOf(command) {
60
+ const found = [];
61
+ for (const part of command.split(/&&|\|\||[;|\n]/)) {
62
+ const words = part.trim().split(/\s+/).filter((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word));
63
+ const first = words[0];
64
+ if (first === undefined || first === "")
65
+ continue;
66
+ const program = first.split("/").pop() ?? first;
67
+ if (!RUNNERS.has(program)) {
68
+ found.push(program);
69
+ continue;
70
+ }
71
+ const target = words.slice(1).find((word) => !word.startsWith("-") && word !== "run");
72
+ if (target !== undefined)
73
+ found.push(target.split("/").pop() ?? target);
74
+ }
75
+ return found;
76
+ }
77
+ var escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
78
+ function askedBySkill(command, markdown, directory) {
79
+ if (new RegExp(`${escaped(directory)}(?=[/\\s'"]|$)`).test(command))
80
+ return true;
81
+ const { cli, runs } = readRequires(markdown);
82
+ const named = new Set([...cli, ...runs]);
83
+ return programsOf(command).some((program) => named.has(program) || program.includes(".") && new RegExp(`(?:^|[\\s\`'"/(])${escaped(program)}(?=[\\s\`'")]|$)`, "m").test(markdown));
84
+ }
85
+ var DOMAINS = [...new Set(SKILL_DOMAINS.map((rule) => rule.domain))];
86
+ function contextOf(words, skill, description) {
87
+ return inferDomain(words, DOMAINS) ?? classifySkill(skill.split(":").pop() ?? skill, description ?? "");
88
+ }
89
+ function skillDirectory(name, cwd) {
90
+ const roots = [join(cwd, ".claude", "skills"), join(homedir(), ".claude", "skills")];
91
+ const given = process.env["ARP_SKILLS_DIR"];
92
+ if (given !== undefined && given !== "")
93
+ roots.unshift(given);
94
+ return roots.map((root) => join(root, name)).find((dir) => existsSync(join(dir, "SKILL.md"))) ?? null;
95
+ }
96
+ function arpHome() {
97
+ const given = process.env["ARP_HOME"]?.trim();
98
+ return given !== undefined && given !== "" ? given : join(homedir(), ".arp");
99
+ }
100
+ function appendFailure(line) {
101
+ const path = join(arpHome(), "failures.jsonl");
102
+ const seen = existsSync(path) ? readFileSync(path, "utf8") : "";
103
+ const key = `"session":${JSON.stringify(line.session)},"skill":${JSON.stringify(line.skill)},`;
104
+ if (seen.split(`
105
+ `).some((row) => row.includes(key) && row.includes(`"reason":"${line.reason}"`)))
106
+ return false;
107
+ mkdirSync(dirname2(path), { recursive: true });
108
+ appendOwn(path, `${JSON.stringify(line)}
109
+ `);
110
+ return true;
111
+ }
112
+
113
+ // ../hook/src/backfill.ts
114
+ var BODY = /^Base directory for this skill: ([^\n]+)/;
115
+ function blocksOf(content) {
116
+ return Array.isArray(content) ? content : [];
117
+ }
118
+ function textOf(content) {
119
+ if (typeof content === "string")
120
+ return content;
121
+ return blocksOf(content).map((block) => typeof block?.text === "string" ? block.text : "").join(`
122
+ `);
123
+ }
124
+ var lastPart = (name) => name.split(":").pop() ?? name;
125
+ function fromDisk(name, cwd) {
126
+ try {
127
+ const directory = skillDirectory(name, cwd);
128
+ return directory === null ? null : { name, directory, markdown: readFileSync2(join2(directory, "SKILL.md"), "utf8") };
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ function readTranscript(text, fallback) {
134
+ const active = [];
135
+ const commands = new Map;
136
+ const pending = new Map;
137
+ const failures = [];
138
+ let uses = 0;
139
+ let words = "";
140
+ for (const line of text.split(`
141
+ `)) {
142
+ if (!/tool_use|tool_result|Base directory for this skill|"user"/.test(line))
143
+ continue;
144
+ let entry;
145
+ try {
146
+ entry = JSON.parse(line);
147
+ } catch {
148
+ continue;
149
+ }
150
+ if (entry["isSidechain"] === true)
151
+ continue;
152
+ const message = entry["message"];
153
+ const content = message?.content;
154
+ const session = typeof entry["sessionId"] === "string" ? entry["sessionId"] : fallback;
155
+ const cwd = typeof entry["cwd"] === "string" ? entry["cwd"] : "";
156
+ const at = typeof entry["timestamp"] === "string" ? entry["timestamp"] : "";
157
+ if (entry["type"] === "assistant") {
158
+ for (const block of blocksOf(content)) {
159
+ if (block?.type !== "tool_use" || typeof block.id !== "string")
160
+ continue;
161
+ const input = block.input ?? {};
162
+ if (block.name === "Skill" && typeof input["skill"] === "string") {
163
+ uses += 1;
164
+ pending.set(block.id, input["skill"]);
165
+ const known = fromDisk(input["skill"], cwd);
166
+ if (known !== null)
167
+ active.unshift(known);
168
+ } else if (block.name === "Bash" && typeof input["command"] === "string") {
169
+ commands.set(block.id, input["command"]);
170
+ }
171
+ }
172
+ continue;
173
+ }
174
+ if (entry["type"] !== "user")
175
+ continue;
176
+ const typed = textOf(content);
177
+ if (entry["isMeta"] !== true && typed.trim() !== "" && !typed.trimStart().startsWith("<"))
178
+ words = typed;
179
+ const directory = BODY.exec(textOf(content))?.[1]?.trim();
180
+ if (entry["isMeta"] === true && directory !== undefined) {
181
+ const folder = basename(directory);
182
+ const call = [...pending].find(([, name2]) => lastPart(name2) === folder);
183
+ if (call === undefined)
184
+ uses += 1;
185
+ else
186
+ pending.delete(call[0]);
187
+ const name = call?.[1] ?? folder;
188
+ const known = active.findIndex((skill) => skill.name === name);
189
+ if (known !== -1)
190
+ active.splice(known, 1);
191
+ active.unshift({ name, directory, markdown: textOf(content) });
192
+ continue;
193
+ }
194
+ for (const block of blocksOf(content)) {
195
+ if (block?.type !== "tool_result" || typeof block.tool_use_id !== "string")
196
+ continue;
197
+ if (block.is_error === true)
198
+ pending.delete(block.tool_use_id);
199
+ if (block.is_error !== true)
200
+ continue;
201
+ const command = commands.get(block.tool_use_id);
202
+ if (command === undefined)
203
+ continue;
204
+ const output = textOf(block.content).slice(0, 4000);
205
+ if (isCancelled(output))
206
+ continue;
207
+ const reason = failureReason(output);
208
+ if (reason === null)
209
+ continue;
210
+ const skill = active.find((candidate) => askedBySkill(command, candidate.markdown, candidate.directory));
211
+ if (skill === undefined)
212
+ continue;
213
+ const domain = contextOf(words, skill.name, readRequires(skill.markdown).description);
214
+ failures.push({ at, session, skill: skill.name, source: null, ...domain === null ? {} : { domain }, reason });
215
+ }
216
+ }
217
+ return { uses, failures };
218
+ }
219
+ function transcripts(root) {
220
+ const found = [];
221
+ let names;
222
+ try {
223
+ names = readdirSync(root);
224
+ } catch {
225
+ return found;
226
+ }
227
+ for (const name of names) {
228
+ const path = join2(root, name);
229
+ try {
230
+ if (statSync(path).isDirectory())
231
+ found.push(...transcripts(path));
232
+ else if (name.endsWith(".jsonl"))
233
+ found.push(path);
234
+ } catch {
235
+ continue;
236
+ }
237
+ }
238
+ return found;
239
+ }
240
+ function backfill(root = join2(homedir2(), ".claude", "projects")) {
241
+ const files = transcripts(root);
242
+ let uses = 0;
243
+ let added = 0;
244
+ for (const file of files) {
245
+ let text;
246
+ try {
247
+ text = readFileSync2(file, "utf8");
248
+ } catch {
249
+ continue;
250
+ }
251
+ const read = readTranscript(text, basename(file, ".jsonl"));
252
+ uses += read.uses;
253
+ for (const failure of read.failures)
254
+ if (appendFailure(failure))
255
+ added += 1;
256
+ }
257
+ return { transcripts: files.length, uses, added, file: join2(arpHome(), "failures.jsonl") };
258
+ }
259
+ export {
260
+ readTranscript,
261
+ backfill
262
+ };
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ HttpRequestError,
4
+ call,
5
+ decodeErrorResult,
6
+ getAbortError,
7
+ getUrl,
8
+ isAbortError,
9
+ isAddressEqual,
10
+ localBatchGatewayRequest,
11
+ localBatchGatewayUrl
12
+ } from "./cli-c8568g4c.js";
13
+ import {
14
+ BaseError,
15
+ concat,
16
+ encodeAbiParameters,
17
+ isHex,
18
+ stringify
19
+ } from "./cli-0hqxver6.js";
20
+ import"./cli-3kbbwfbp.js";
21
+ import"./cli-g5zvr10v.js";
22
+
23
+ // ../../node_modules/.bun/viem@2.52.0+1cd18485b0404aca/node_modules/viem/_esm/errors/ccip.js
24
+ class OffchainLookupError extends BaseError {
25
+ constructor({ callbackSelector, cause, data, extraData, sender, urls }) {
26
+ super(cause.shortMessage || "An error occurred while fetching for an offchain result.", {
27
+ cause,
28
+ metaMessages: [
29
+ ...cause.metaMessages || [],
30
+ cause.metaMessages?.length ? "" : [],
31
+ "Offchain Gateway Call:",
32
+ urls && [
33
+ " Gateway URL(s):",
34
+ ...urls.map((url) => ` ${getUrl(url)}`)
35
+ ],
36
+ ` Sender: ${sender}`,
37
+ ` Data: ${data}`,
38
+ ` Callback selector: ${callbackSelector}`,
39
+ ` Extra data: ${extraData}`
40
+ ].flat(),
41
+ name: "OffchainLookupError"
42
+ });
43
+ }
44
+ }
45
+
46
+ class OffchainLookupResponseMalformedError extends BaseError {
47
+ constructor({ result, url }) {
48
+ super("Offchain gateway response is malformed. Response data must be a hex value.", {
49
+ metaMessages: [
50
+ `Gateway URL: ${getUrl(url)}`,
51
+ `Response: ${stringify(result)}`
52
+ ],
53
+ name: "OffchainLookupResponseMalformedError"
54
+ });
55
+ }
56
+ }
57
+
58
+ class OffchainLookupSenderMismatchError extends BaseError {
59
+ constructor({ sender, to }) {
60
+ super("Reverted sender address does not match target contract address (`to`).", {
61
+ metaMessages: [
62
+ `Contract address: ${to}`,
63
+ `OffchainLookup sender address: ${sender}`
64
+ ],
65
+ name: "OffchainLookupSenderMismatchError"
66
+ });
67
+ }
68
+ }
69
+
70
+ // ../../node_modules/.bun/viem@2.52.0+1cd18485b0404aca/node_modules/viem/_esm/utils/ccip.js
71
+ var offchainLookupSignature = "0x556f1830";
72
+ var offchainLookupAbiItem = {
73
+ name: "OffchainLookup",
74
+ type: "error",
75
+ inputs: [
76
+ {
77
+ name: "sender",
78
+ type: "address"
79
+ },
80
+ {
81
+ name: "urls",
82
+ type: "string[]"
83
+ },
84
+ {
85
+ name: "callData",
86
+ type: "bytes"
87
+ },
88
+ {
89
+ name: "callbackFunction",
90
+ type: "bytes4"
91
+ },
92
+ {
93
+ name: "extraData",
94
+ type: "bytes"
95
+ }
96
+ ]
97
+ };
98
+ async function offchainLookup(client, { blockNumber, blockTag, data, requestOptions, to }) {
99
+ const { args } = decodeErrorResult({
100
+ data,
101
+ abi: [offchainLookupAbiItem]
102
+ });
103
+ const [sender, urls, callData, callbackSelector, extraData] = args;
104
+ const { ccipRead } = client;
105
+ const ccipRequest_ = ccipRead && typeof ccipRead?.request === "function" ? ccipRead.request : ccipRequest;
106
+ try {
107
+ if (!isAddressEqual(to, sender))
108
+ throw new OffchainLookupSenderMismatchError({ sender, to });
109
+ const result = urls.includes(localBatchGatewayUrl) ? await localBatchGatewayRequest({
110
+ data: callData,
111
+ ccipRequest: (parameters) => ccipRequest_({ ...parameters, requestOptions })
112
+ }) : await ccipRequest_({ data: callData, requestOptions, sender, urls });
113
+ const { data: data_ } = await call(client, {
114
+ blockNumber,
115
+ blockTag,
116
+ data: concat([
117
+ callbackSelector,
118
+ encodeAbiParameters([{ type: "bytes" }, { type: "bytes" }], [result, extraData])
119
+ ]),
120
+ requestOptions,
121
+ to
122
+ });
123
+ return data_;
124
+ } catch (err) {
125
+ if (requestOptions?.signal?.aborted)
126
+ throw getAbortError(requestOptions.signal);
127
+ if (isAbortError(err))
128
+ throw err;
129
+ throw new OffchainLookupError({
130
+ callbackSelector,
131
+ cause: err,
132
+ data,
133
+ extraData,
134
+ sender,
135
+ urls
136
+ });
137
+ }
138
+ }
139
+ async function ccipRequest({ data, requestOptions, sender, urls }) {
140
+ let error = new Error("An unknown error occurred.");
141
+ for (let i = 0;i < urls.length; i++) {
142
+ if (requestOptions?.signal?.aborted)
143
+ throw getAbortError(requestOptions.signal);
144
+ const url = urls[i];
145
+ const method = url.includes("{data}") ? "GET" : "POST";
146
+ const body = method === "POST" ? { data, sender } : undefined;
147
+ const headers = method === "POST" ? { "Content-Type": "application/json" } : {};
148
+ try {
149
+ const response = await fetch(url.replace("{sender}", sender.toLowerCase()).replace("{data}", data), {
150
+ body: JSON.stringify(body),
151
+ headers,
152
+ method,
153
+ ...requestOptions?.signal ? { signal: requestOptions.signal } : {}
154
+ });
155
+ let result;
156
+ if (response.headers.get("Content-Type")?.startsWith("application/json")) {
157
+ result = (await response.json()).data;
158
+ } else {
159
+ result = await response.text();
160
+ }
161
+ if (!response.ok) {
162
+ error = new HttpRequestError({
163
+ body,
164
+ details: result?.error ? stringify(result.error) : response.statusText,
165
+ headers: response.headers,
166
+ status: response.status,
167
+ url
168
+ });
169
+ continue;
170
+ }
171
+ if (!isHex(result)) {
172
+ error = new OffchainLookupResponseMalformedError({
173
+ result,
174
+ url
175
+ });
176
+ continue;
177
+ }
178
+ return result;
179
+ } catch (err) {
180
+ if (requestOptions?.signal?.aborted)
181
+ throw getAbortError(requestOptions.signal);
182
+ if (isAbortError(err))
183
+ throw err;
184
+ error = new HttpRequestError({
185
+ body,
186
+ details: err.message,
187
+ url
188
+ });
189
+ }
190
+ }
191
+ throw error;
192
+ }
193
+ export {
194
+ offchainLookupSignature,
195
+ offchainLookupAbiItem,
196
+ offchainLookup,
197
+ ccipRequest
198
+ };