@axisagent/cli 0.1.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 ADDED
@@ -0,0 +1,61 @@
1
+ # Axis Local
2
+
3
+ Local runtime package for Axis.
4
+
5
+ It contains the first usable registry, skill manifest generator, policy schema, scanner, and CLI. This is the part that makes the project more than a frontend catalog.
6
+
7
+ ## Commands
8
+
9
+ ```bash
10
+ npx @axisagent/cli validate
11
+ npx @axisagent/cli list
12
+ npx @axisagent/cli list --chain Base
13
+ npm run axis -- inspect bankr-launch-feed
14
+ npx @axisagent/cli install bankr-launch-feed --target .axis/skills
15
+ npx @axisagent/cli scan .axis/skills/bankr-launch-feed
16
+ npx @axisagent/cli connect --pair AXIS-1234 --api https://api.axisagent.xyz
17
+ npx @axisagent/cli serve --port 8788
18
+ ```
19
+
20
+ ## Axis Relay
21
+
22
+ The public dashboard should connect through Axis API, not direct browser-to-localhost permission prompts.
23
+
24
+ Flow:
25
+
26
+ 1. Dashboard creates a pairing code through `POST /v1/pairing/create`.
27
+ 2. User runs `npx @axisagent/cli connect --pair AXIS-1234 --api https://api.axisagent.xyz`.
28
+ 3. The local connector opens an outbound session to Axis API.
29
+ 4. The connector sends heartbeats, detected agents, capabilities and registry stats.
30
+ 5. The dashboard polls `GET /v1/pairing/:code/status`.
31
+
32
+ For local API testing:
33
+
34
+ ```bash
35
+ npx @axisagent/cli connect --pair AXIS-1234 --api http://127.0.0.1:8790 --once
36
+ ```
37
+
38
+ ## Local Connector
39
+
40
+ The localhost connector remains available for development and fallback flows. When the server is running, the dashboard or relay client can call the local connector before any execution:
41
+
42
+ - `POST /v1/install` installs a skill package and scans its policy.
43
+ - `POST /v1/scan` scans an existing skill package.
44
+ - `POST /v1/tickets` creates a simulation ticket with chain, spend cap, permissions, execution plan and policy checks.
45
+ - `POST /v1/tickets/:id/decision` records user approval or rejection. Blocked tickets cannot be approved.
46
+
47
+ Ticket controls are grouped by purpose:
48
+
49
+ - `core`: chain, spend cap, approval, execution mode and session limits
50
+ - `permission`: explicit control for every declared permission
51
+ - `payment`: x402 quote, asset and receipt controls
52
+ - `execution`: wallet execution gates
53
+ - `audit`: secrets and local audit requirements
54
+
55
+ ## Safety Rules
56
+
57
+ - Money-moving skills default to `approval-required`.
58
+ - Critical skills must require approval and declare a spend cap or read-only mode.
59
+ - Private keys are never stored in generated packages.
60
+ - Install output includes `SKILL.md`, `policy.json`, and `README.md`.
61
+ - Simulation tickets are persisted under `.axis/tickets` for local auditability.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { startLocalServer } from "./localServer.js";
5
+ import { getSkillById, getSkillsByChain, skills, summarizeSkill, validateRegistry } from "./skillRegistry.js";
6
+ import { scanSkillPackage, writeSkillPackage } from "./packageWriter.js";
7
+ import { connectRelay } from "./relayClient.js";
8
+ const args = process.argv.slice(2);
9
+ const command = args[0] ?? "help";
10
+ const userCwd = process.env.INIT_CWD ?? process.cwd();
11
+ function getOption(name, fallback) {
12
+ const index = args.indexOf(name);
13
+ if (index === -1)
14
+ return fallback;
15
+ return args[index + 1] ?? fallback;
16
+ }
17
+ function hasFlag(name) {
18
+ return args.includes(name);
19
+ }
20
+ function printHelp() {
21
+ console.log(`Axis Local
22
+
23
+ Commands:
24
+ list [--chain Base] [--json]
25
+ inspect <skill-id> [--json]
26
+ install <skill-id> [--target .axis/skills]
27
+ scan <skill-dir>
28
+ validate
29
+ connect [--pair AXIS-4821] [--api https://api.axisagent.xyz] [--once]
30
+ serve [--port 8788]
31
+ `);
32
+ }
33
+ async function run() {
34
+ if (command === "help" || command === "--help" || command === "-h") {
35
+ printHelp();
36
+ return;
37
+ }
38
+ if (command === "list") {
39
+ const chain = getOption("--chain");
40
+ const rows = chain ? getSkillsByChain(chain) : skills;
41
+ if (hasFlag("--json")) {
42
+ console.log(JSON.stringify(rows.map(summarizeSkill), null, 2));
43
+ return;
44
+ }
45
+ for (const skill of rows) {
46
+ console.log(`${skill.id.padEnd(32)} ${skill.risk.padEnd(8)} ${skill.executionMode.padEnd(17)} ${skill.chains.join(", ")}`);
47
+ }
48
+ return;
49
+ }
50
+ if (command === "inspect") {
51
+ const id = args[1];
52
+ if (!id)
53
+ throw new Error("Missing skill id.");
54
+ const skill = getSkillById(id);
55
+ if (!skill)
56
+ throw new Error(`Unknown skill: ${id}`);
57
+ console.log(JSON.stringify(hasFlag("--summary") ? summarizeSkill(skill) : skill, null, 2));
58
+ return;
59
+ }
60
+ if (command === "install") {
61
+ const id = args[1];
62
+ if (!id)
63
+ throw new Error("Missing skill id.");
64
+ const target = resolve(userCwd, getOption("--target", ".axis/skills") ?? ".axis/skills");
65
+ await mkdir(target, { recursive: true });
66
+ const result = await writeSkillPackage(id, target);
67
+ console.log(`Installed ${result.skill.id}`);
68
+ console.log(`Path: ${result.skillDir}`);
69
+ console.log(`Files: ${result.files.join(", ")}`);
70
+ return;
71
+ }
72
+ if (command === "scan") {
73
+ const target = args[1];
74
+ if (!target)
75
+ throw new Error("Missing skill directory.");
76
+ const result = await scanSkillPackage(resolve(userCwd, target));
77
+ console.log(JSON.stringify(result, null, 2));
78
+ if (!result.ok)
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+ if (command === "validate") {
83
+ const result = validateRegistry();
84
+ console.log(JSON.stringify(result, null, 2));
85
+ if (!result.ok)
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ if (command === "connect") {
90
+ const pairingCode = getOption("--pair", "AXIS-4821") ?? "AXIS-4821";
91
+ const apiUrl = getOption("--api", process.env.AXIS_API_URL ?? "https://api.axisagent.xyz") ?? "https://api.axisagent.xyz";
92
+ const heartbeatIntervalMs = Number(getOption("--heartbeat-ms", "5000"));
93
+ await connectRelay({
94
+ apiUrl,
95
+ pairingCode,
96
+ baseDir: userCwd,
97
+ heartbeatIntervalMs,
98
+ once: hasFlag("--once")
99
+ });
100
+ return;
101
+ }
102
+ if (command === "serve") {
103
+ const port = Number(getOption("--port", "8788"));
104
+ const host = getOption("--host", "127.0.0.1") ?? "127.0.0.1";
105
+ const server = await startLocalServer({ port, host, baseDir: userCwd });
106
+ console.log(`Axis local connector listening at ${server.url}`);
107
+ console.log("Endpoints: /health, /v1/protocol/stats, /v1/skills, /v1/skills/:id");
108
+ return;
109
+ }
110
+ throw new Error(`Unknown command: ${command}`);
111
+ }
112
+ run().catch((error) => {
113
+ console.error(error instanceof Error ? error.message : error);
114
+ process.exitCode = 1;
115
+ });
@@ -0,0 +1,5 @@
1
+ export * from "./schema.js";
2
+ export * from "./skillRegistry.js";
3
+ export * from "./packageWriter.js";
4
+ export * from "./localServer.js";
5
+ export * from "./relayClient.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./schema.js";
2
+ export * from "./skillRegistry.js";
3
+ export * from "./packageWriter.js";
4
+ export * from "./localServer.js";
5
+ export * from "./relayClient.js";
@@ -0,0 +1,11 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ type ServerOptions = {
3
+ port: number;
4
+ host: string;
5
+ baseDir: string;
6
+ };
7
+ export declare function startLocalServer(options: ServerOptions): Promise<{
8
+ server: import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
9
+ url: string;
10
+ }>;
11
+ export {};
@@ -0,0 +1,333 @@
1
+ import { createServer } from "node:http";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { isAbsolute, relative, resolve } from "node:path";
5
+ import { getRegistryStats, getSkillById, getSkillsByChain, skills, summarizeSkill } from "./skillRegistry.js";
6
+ import { scanSkillPackage, writeSkillPackage } from "./packageWriter.js";
7
+ function writeJson(response, statusCode, body) {
8
+ response.writeHead(statusCode, {
9
+ "content-type": "application/json; charset=utf-8",
10
+ "access-control-allow-origin": "*",
11
+ "access-control-allow-methods": "GET,POST,OPTIONS",
12
+ "access-control-allow-headers": "content-type"
13
+ });
14
+ response.end(JSON.stringify(body, null, 2));
15
+ }
16
+ async function readJson(request) {
17
+ const chunks = [];
18
+ for await (const chunk of request) {
19
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
20
+ }
21
+ if (chunks.length === 0)
22
+ return {};
23
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
24
+ }
25
+ function resolveInsideBase(baseDir, inputPath) {
26
+ const base = resolve(baseDir);
27
+ const target = resolve(base, inputPath);
28
+ const pathFromBase = relative(base, target);
29
+ if (pathFromBase === ".." || pathFromBase.startsWith(`..${"/"}`) || isAbsolute(pathFromBase)) {
30
+ throw new Error("Path escapes Axis workspace");
31
+ }
32
+ return target;
33
+ }
34
+ function defaultRequestedSpendUsd(skill) {
35
+ if (skill.policy.maxSpendUsd === null)
36
+ return null;
37
+ if (skill.permissions.includes("paid-api"))
38
+ return Math.min(skill.policy.maxSpendUsd, 0.01);
39
+ return skill.policy.maxSpendUsd;
40
+ }
41
+ function check(group, label, severity, value) {
42
+ return { group, label, severity, value };
43
+ }
44
+ function permissionCheck(permission, severity, value) {
45
+ return check("permission", `permission:${permission}`, severity, value);
46
+ }
47
+ function summarizeChecks(checks) {
48
+ return {
49
+ safe: checks.filter((item) => item.severity === "safe").length,
50
+ warning: checks.filter((item) => item.severity === "warning").length,
51
+ blocked: checks.filter((item) => item.severity === "blocked").length,
52
+ total: checks.length
53
+ };
54
+ }
55
+ function buildPermissionChecks(skill, chain) {
56
+ return skill.permissions.map((permission) => {
57
+ if (permission === "network") {
58
+ return permissionCheck(permission, "safe", `Network scoped to ${skill.protocols.join(", ")} on ${chain}`);
59
+ }
60
+ if (permission === "market-data") {
61
+ return permissionCheck(permission, "safe", "Read-only market data; no order placement from this permission alone");
62
+ }
63
+ if (permission === "wallet-read") {
64
+ return permissionCheck(permission, "safe", "Wallet reads are allowed; private keys are never exposed");
65
+ }
66
+ if (permission === "wallet-signature") {
67
+ if (!skill.policy.requiresApproval) {
68
+ return permissionCheck(permission, "blocked", "Wallet signatures require human approval");
69
+ }
70
+ if (!skill.permissions.includes("transaction-preview")) {
71
+ return permissionCheck(permission, "warning", "Wallet signature allowed, but transaction-preview permission is not declared");
72
+ }
73
+ return permissionCheck(permission, "safe", "Wallet signature requires approval after transaction preview");
74
+ }
75
+ if (permission === "transaction-preview") {
76
+ return permissionCheck(permission, "safe", "Transaction preview must be rendered before signing");
77
+ }
78
+ if (permission === "trading") {
79
+ if (!skill.policy.requiresApproval) {
80
+ return permissionCheck(permission, "blocked", "Trading cannot execute without human approval");
81
+ }
82
+ if (skill.policy.maxSpendUsd === null) {
83
+ return permissionCheck(permission, "blocked", "Trading requires a spend cap");
84
+ }
85
+ return permissionCheck(permission, "safe", `Trading is approval-gated with $${skill.policy.maxSpendUsd} cap`);
86
+ }
87
+ if (permission === "contract-call") {
88
+ if (!skill.permissions.includes("transaction-preview")) {
89
+ return permissionCheck(permission, "blocked", "Contract calls require transaction preview");
90
+ }
91
+ return permissionCheck(permission, "safe", "Contract call must show target, calldata and value before signing");
92
+ }
93
+ if (permission === "paid-api") {
94
+ if (skill.policy.maxSpendUsd === null) {
95
+ return permissionCheck(permission, "blocked", "Paid API calls require a budget cap");
96
+ }
97
+ return permissionCheck(permission, "safe", `Paid API budget capped at $${skill.policy.maxSpendUsd}`);
98
+ }
99
+ if (permission === "api-key") {
100
+ return permissionCheck(permission, "warning", "API keys must be referenced by environment name, never stored in package files");
101
+ }
102
+ if (permission === "secrets-read") {
103
+ if (skill.policy.secrets.length === 0) {
104
+ return permissionCheck(permission, "blocked", "secrets-read requires declared secret names");
105
+ }
106
+ return permissionCheck(permission, "safe", `Secrets restricted to ${skill.policy.secrets.join(", ")}`);
107
+ }
108
+ if (permission === "filesystem-read") {
109
+ return permissionCheck(permission, "safe", "Filesystem reads are limited to user-selected local paths");
110
+ }
111
+ if (permission === "filesystem-write") {
112
+ return permissionCheck(permission, "warning", "Filesystem writes must stay inside the Axis workspace or exported report path");
113
+ }
114
+ if (permission === "notifications") {
115
+ return permissionCheck(permission, "safe", "Notifications can alert the user but cannot execute actions");
116
+ }
117
+ if (permission === "browser") {
118
+ return permissionCheck(permission, "warning", "Browser actions require visible session context and user confirmation for submissions");
119
+ }
120
+ if (permission === "governance") {
121
+ return permissionCheck(permission, "warning", "Governance actions require proposal preview and explicit approval");
122
+ }
123
+ return permissionCheck(permission, "warning", "Permission declared but no specialized control is implemented");
124
+ });
125
+ }
126
+ function createSimulationTicket(skill, body) {
127
+ const isX402 = skill.protocols.some((protocol) => protocol.toLowerCase() === "x402");
128
+ const requestedSpendUsd = Number.isFinite(body.amountUsd) ? Number(body.amountUsd) : defaultRequestedSpendUsd(skill);
129
+ const chain = body.chain ?? skill.policy.allowedChains[0] ?? skill.chains[0];
130
+ const executionPlan = isX402
131
+ ? ["request paid endpoint", "read HTTP 402 payment requirements", "verify Base USDC quote", "approve payment", "retry request and log receipt"]
132
+ : skill.operations.slice(0, 4);
133
+ const checks = [
134
+ check("core", "policy", "safe", skill.policy.humanReadableRule),
135
+ check("core", "execution mode", skill.executionMode === skill.policy.defaultMode ? "safe" : "warning", `Skill mode ${skill.executionMode}; policy default ${skill.policy.defaultMode}`),
136
+ check("core", "chain", skill.policy.allowedChains.includes(chain) ? "safe" : "blocked", skill.policy.allowedChains.includes(chain) ? `${chain} is allowed` : `${chain} is not allowed by policy`),
137
+ check("core", "session limit", "safe", `Session limited to ${skill.policy.sessionLimitMinutes} minutes`),
138
+ check("core", "approval", skill.policy.requiresApproval ? "safe" : "warning", skill.policy.requiresApproval ? "Human approval required before execution" : "Read-only skill does not require approval")
139
+ ];
140
+ if (skill.policy.maxSpendUsd === null) {
141
+ checks.push(check("core", "spend cap", "safe", "No spend path declared"));
142
+ }
143
+ else if (requestedSpendUsd !== null && requestedSpendUsd > skill.policy.maxSpendUsd) {
144
+ checks.push(check("core", "spend cap", "blocked", `Requested $${requestedSpendUsd} exceeds $${skill.policy.maxSpendUsd} policy cap`));
145
+ }
146
+ else {
147
+ checks.push(check("core", "spend cap", "safe", `Capped at $${skill.policy.maxSpendUsd}`));
148
+ }
149
+ if (skill.policy.walletWrite) {
150
+ checks.push(check("execution", "wallet", "safe", "Wallet signature required after approval"));
151
+ }
152
+ else {
153
+ checks.push(check("execution", "wallet", "safe", "No direct wallet write declared"));
154
+ }
155
+ if (skill.policy.secrets.length > 0) {
156
+ checks.push(check("audit", "secrets", "warning", `Requires env refs: ${skill.policy.secrets.join(", ")}`));
157
+ }
158
+ checks.push(...buildPermissionChecks(skill, chain));
159
+ if (isX402) {
160
+ checks.push(check("payment", "x402 quote", "warning", "Payment-Required quote must be inspected before signing"), check("payment", "asset", "safe", "USDC on Base only for this skill"), check("payment", "receipt", "safe", "Payment response must be logged locally"));
161
+ }
162
+ const blocked = checks.some((check) => check.severity === "blocked");
163
+ const controlSummary = summarizeChecks(checks);
164
+ return {
165
+ id: `ticket-${randomUUID()}`,
166
+ skillId: skill.id,
167
+ skillTitle: skill.title,
168
+ status: blocked ? "blocked" : skill.policy.requiresApproval ? "needs-approval" : "ready",
169
+ intent: body.intent ?? `Prepare ${skill.operations[0]} with ${skill.title}`,
170
+ risk: skill.risk,
171
+ chain,
172
+ protocols: skill.protocols,
173
+ permissions: skill.permissions,
174
+ approvalRequired: skill.policy.requiresApproval,
175
+ maxSpendUsd: skill.policy.maxSpendUsd,
176
+ requestedSpendUsd,
177
+ controlSummary,
178
+ payment: isX402
179
+ ? {
180
+ protocol: "x402",
181
+ asset: "USDC",
182
+ network: chain,
183
+ facilitator: "https://x402.org/facilitator",
184
+ resourceUrl: body.resourceUrl ?? "pending 402 response",
185
+ quoteStatus: "required-before-payment",
186
+ receipt: "required"
187
+ }
188
+ : undefined,
189
+ executionPlan,
190
+ checks,
191
+ createdAt: new Date().toISOString()
192
+ };
193
+ }
194
+ export async function startLocalServer(options) {
195
+ const tickets = new Map();
196
+ async function persistTicket(ticket) {
197
+ const ticketDir = resolveInsideBase(options.baseDir, ".axis/tickets");
198
+ await mkdir(ticketDir, { recursive: true });
199
+ await writeFile(resolve(ticketDir, `${ticket.id}.json`), `${JSON.stringify(ticket, null, 2)}\n`, "utf8");
200
+ }
201
+ const server = createServer(async (request, response) => {
202
+ try {
203
+ if (!request.url) {
204
+ writeJson(response, 400, { error: "Missing URL" });
205
+ return;
206
+ }
207
+ if (request.method === "OPTIONS") {
208
+ writeJson(response, 200, { ok: true });
209
+ return;
210
+ }
211
+ const url = new URL(request.url, `http://${options.host}:${options.port}`);
212
+ if (request.method === "GET" && url.pathname === "/health") {
213
+ writeJson(response, 200, {
214
+ service: "axis-local-connector",
215
+ status: "ok",
216
+ timestamp: new Date().toISOString()
217
+ });
218
+ return;
219
+ }
220
+ if (request.method === "GET" && url.pathname === "/v1/protocol/stats") {
221
+ writeJson(response, 200, {
222
+ registry: getRegistryStats(),
223
+ connector: {
224
+ status: "local-ready",
225
+ baseDir: options.baseDir
226
+ }
227
+ });
228
+ return;
229
+ }
230
+ if (request.method === "GET" && url.pathname === "/v1/skills") {
231
+ const chain = url.searchParams.get("chain");
232
+ const rows = chain ? getSkillsByChain(chain) : skills;
233
+ writeJson(response, 200, { skills: rows.map(summarizeSkill) });
234
+ return;
235
+ }
236
+ const skillMatch = url.pathname.match(/^\/v1\/skills\/([a-z0-9-]+)$/);
237
+ if (request.method === "GET" && skillMatch) {
238
+ const skill = getSkillById(skillMatch[1]);
239
+ if (!skill) {
240
+ writeJson(response, 404, { error: "Skill not found" });
241
+ return;
242
+ }
243
+ writeJson(response, 200, { skill });
244
+ return;
245
+ }
246
+ if (request.method === "POST" && url.pathname === "/v1/install") {
247
+ const body = (await readJson(request));
248
+ if (!body.id) {
249
+ writeJson(response, 400, { error: "Missing skill id" });
250
+ return;
251
+ }
252
+ const target = resolveInsideBase(options.baseDir, body.target ?? ".axis/skills");
253
+ const result = await writeSkillPackage(body.id, target);
254
+ const scan = await scanSkillPackage(result.skillDir);
255
+ writeJson(response, 200, {
256
+ installed: result.skill.id,
257
+ path: result.skillDir,
258
+ files: result.files,
259
+ scan
260
+ });
261
+ return;
262
+ }
263
+ if (request.method === "POST" && url.pathname === "/v1/scan") {
264
+ const body = (await readJson(request));
265
+ const skillPath = body.path ?? (body.id ? `${body.target ?? ".axis/skills"}/${body.id}` : null);
266
+ if (!skillPath) {
267
+ writeJson(response, 400, { error: "Missing skill id or path" });
268
+ return;
269
+ }
270
+ const skillDir = resolveInsideBase(options.baseDir, skillPath);
271
+ const scan = await scanSkillPackage(skillDir);
272
+ writeJson(response, 200, scan);
273
+ return;
274
+ }
275
+ if (request.method === "POST" && url.pathname === "/v1/tickets") {
276
+ const body = (await readJson(request));
277
+ if (!body.id) {
278
+ writeJson(response, 400, { error: "Missing skill id" });
279
+ return;
280
+ }
281
+ const skill = getSkillById(body.id);
282
+ if (!skill) {
283
+ writeJson(response, 404, { error: "Skill not found" });
284
+ return;
285
+ }
286
+ const ticket = createSimulationTicket(skill, body);
287
+ tickets.set(ticket.id, ticket);
288
+ await persistTicket(ticket);
289
+ writeJson(response, 200, { ticket });
290
+ return;
291
+ }
292
+ const decisionMatch = url.pathname.match(/^\/v1\/tickets\/([a-z0-9-]+)\/decision$/);
293
+ if (request.method === "POST" && decisionMatch) {
294
+ const body = (await readJson(request));
295
+ const ticket = tickets.get(decisionMatch[1]);
296
+ if (!ticket) {
297
+ writeJson(response, 404, { error: "Ticket not found" });
298
+ return;
299
+ }
300
+ if (body.decision !== "approved" && body.decision !== "rejected") {
301
+ writeJson(response, 400, { error: "Decision must be approved or rejected" });
302
+ return;
303
+ }
304
+ if (ticket.status === "blocked" && body.decision === "approved") {
305
+ writeJson(response, 409, { error: "Blocked tickets cannot be approved", ticket });
306
+ return;
307
+ }
308
+ const updated = {
309
+ ...ticket,
310
+ status: body.decision,
311
+ decidedAt: new Date().toISOString()
312
+ };
313
+ tickets.set(updated.id, updated);
314
+ await persistTicket(updated);
315
+ writeJson(response, 200, { ticket: updated });
316
+ return;
317
+ }
318
+ writeJson(response, 404, { error: "Route not found" });
319
+ }
320
+ catch (error) {
321
+ writeJson(response, 500, { error: error instanceof Error ? error.message : "Unknown error" });
322
+ }
323
+ });
324
+ await new Promise((resolveListen) => {
325
+ server.listen(options.port, options.host, resolveListen);
326
+ });
327
+ const address = server.address();
328
+ const actualPort = typeof address === "object" && address ? address.port : options.port;
329
+ return {
330
+ server,
331
+ url: `http://${options.host}:${actualPort}`
332
+ };
333
+ }
@@ -0,0 +1,41 @@
1
+ export declare function writeSkillPackage(skillId: string, targetDir: string): Promise<{
2
+ skill: {
3
+ id: string;
4
+ title: string;
5
+ category: string;
6
+ summary: string;
7
+ useWhen: string;
8
+ chains: string[];
9
+ protocols: string[];
10
+ risk: "low" | "medium" | "high" | "critical";
11
+ executionMode: "read-only" | "approval-required" | "live-execution";
12
+ permissions: ("network" | "market-data" | "wallet-read" | "wallet-signature" | "transaction-preview" | "trading" | "api-key" | "notifications" | "contract-call" | "filesystem-read" | "filesystem-write" | "secrets-read" | "paid-api" | "governance" | "browser")[];
13
+ installTargets: ("codex" | "claude" | "openclaw" | "cursor" | "generic-mcp")[];
14
+ source: string;
15
+ tokenUtility: string;
16
+ auditNotes: string[];
17
+ operations: string[];
18
+ tests: string[];
19
+ policy: {
20
+ defaultMode: "read-only" | "approval-required" | "live-execution";
21
+ requiresApproval: boolean;
22
+ networkAccess: boolean;
23
+ walletWrite: boolean;
24
+ secrets: string[];
25
+ allowedChains: string[];
26
+ maxSpendUsd: number | null;
27
+ sessionLimitMinutes: number;
28
+ humanReadableRule: string;
29
+ };
30
+ };
31
+ skillDir: string;
32
+ files: string[];
33
+ }>;
34
+ export declare function scanSkillPackage(skillDir: string): Promise<{
35
+ ok: boolean;
36
+ findings: {
37
+ label: string;
38
+ severity: "safe" | "warning" | "blocked";
39
+ value: string;
40
+ }[];
41
+ }>;
@@ -0,0 +1,48 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { getSkillById, toPolicyJson, toSkillMarkdown } from "./skillRegistry.js";
4
+ export async function writeSkillPackage(skillId, targetDir) {
5
+ const skill = getSkillById(skillId);
6
+ if (!skill) {
7
+ throw new Error(`Unknown skill: ${skillId}`);
8
+ }
9
+ const skillDir = join(targetDir, skill.id);
10
+ await mkdir(skillDir, { recursive: true });
11
+ await writeFile(join(skillDir, "SKILL.md"), toSkillMarkdown(skill), "utf8");
12
+ await writeFile(join(skillDir, "policy.json"), `${toPolicyJson(skill)}\n`, "utf8");
13
+ await writeFile(join(skillDir, "README.md"), `# ${skill.title}\n\nGenerated by Axis Local.\n\nInstall command:\n\n\`\`\`bash\nnpx @axisagent/cli install ${skill.id}\n\`\`\`\n`, "utf8");
14
+ return {
15
+ skill,
16
+ skillDir,
17
+ files: ["SKILL.md", "policy.json", "README.md"]
18
+ };
19
+ }
20
+ export async function scanSkillPackage(skillDir) {
21
+ const skillMarkdown = await readFile(join(skillDir, "SKILL.md"), "utf8");
22
+ const policyRaw = await readFile(join(skillDir, "policy.json"), "utf8");
23
+ const findings = [];
24
+ if (!skillMarkdown.startsWith("---")) {
25
+ findings.push({ label: "SKILL.md", severity: "blocked", value: "Missing frontmatter" });
26
+ }
27
+ else {
28
+ findings.push({ label: "SKILL.md", severity: "safe", value: "Frontmatter present" });
29
+ }
30
+ const policyJson = JSON.parse(policyRaw);
31
+ if (!policyJson.id || !policyJson.policy) {
32
+ findings.push({ label: "policy.json", severity: "blocked", value: "Invalid policy file" });
33
+ }
34
+ else {
35
+ findings.push({ label: "policy.json", severity: "safe", value: "Policy present" });
36
+ }
37
+ const isMoneyMoving = policyJson.permissions?.some((permission) => ["trading", "wallet-signature", "contract-call", "paid-api"].includes(permission));
38
+ if (isMoneyMoving && !policyJson.policy?.requiresApproval) {
39
+ findings.push({ label: "approval", severity: "blocked", value: "Money-moving package lacks approval gate" });
40
+ }
41
+ else if (isMoneyMoving) {
42
+ findings.push({ label: "approval", severity: "safe", value: "Approval required" });
43
+ }
44
+ return {
45
+ ok: findings.every((finding) => finding.severity !== "blocked"),
46
+ findings
47
+ };
48
+ }
@@ -0,0 +1,17 @@
1
+ type RelayConnectOptions = {
2
+ apiUrl: string;
3
+ pairingCode: string;
4
+ baseDir: string;
5
+ heartbeatIntervalMs: number;
6
+ once: boolean;
7
+ };
8
+ export declare function connectRelay(options: RelayConnectOptions): Promise<{
9
+ status: string;
10
+ relayToken: string;
11
+ session: {
12
+ pairingCode: string;
13
+ status: string;
14
+ expiresAt: string;
15
+ };
16
+ }>;
17
+ export {};