@coinfello/agent-cli 0.1.12 → 0.1.13

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
@@ -101,6 +101,23 @@ Transaction submitted successfully.
101
101
  Transaction ID: <txn_hash_>
102
102
  ```
103
103
 
104
+ ### 6. signer-daemon
105
+
106
+ Manages the Secure Enclave signing daemon. Without the daemon, each signing operation (account creation, sign-in, delegation signing) triggers a separate Touch ID / password prompt. Starting the daemon authenticates once and caches the authorization for subsequent operations.
107
+
108
+ ```bash
109
+ # Start the daemon (prompts Touch ID / password once)
110
+ node dist/index.js signer-daemon start
111
+
112
+ # Check if the daemon is running
113
+ node dist/index.js signer-daemon status
114
+
115
+ # Stop the daemon
116
+ node dist/index.js signer-daemon stop
117
+ ```
118
+
119
+ If the daemon is not running, all signing operations fall back to direct Secure Enclave binary execution (which prompts Touch ID each time).
120
+
104
121
  ### Help
105
122
 
106
123
  View all commands and options:
@@ -109,4 +126,5 @@ View all commands and options:
109
126
  node dist/index.js --help
110
127
  node dist/index.js create_account --help
111
128
  node dist/index.js send_prompt --help
129
+ node dist/index.js signer-daemon --help
112
130
  ```
package/dist/index.js CHANGED
@@ -5,24 +5,141 @@ import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
5
5
  import { toWebAuthnAccount } from "viem/account-abstraction";
6
6
  import * as chains from "viem/chains";
7
7
  import { createHash, randomBytes } from "node:crypto";
8
- import { execFile } from "node:child_process";
8
+ import { execFile, spawn } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
10
  import { dirname, join } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
- import { platform, homedir } from "node:os";
13
- import { readFile, mkdir, writeFile } from "node:fs/promises";
12
+ import { userInfo, platform, homedir } from "node:os";
13
+ import { readFile, unlink, mkdir, writeFile } from "node:fs/promises";
14
+ import { createConnection } from "node:net";
14
15
  import { http, createPublicClient as createPublicClient$1, serializeErc6492Signature } from "viem";
15
16
  import { createSiweMessage } from "viem/siwe";
16
17
  const execFileAsync = promisify(execFile);
17
18
  const __filename$1 = fileURLToPath(import.meta.url);
18
19
  const __dirname$1 = dirname(__filename$1);
19
20
  function getBinaryPath() {
20
- return join(__dirname$1, "secure-enclave-signer.app", "Contents", "MacOS", "secure-enclave-signer");
21
+ const bundlePath = join(
22
+ __dirname$1,
23
+ "secure-enclave-signer.app",
24
+ "Contents",
25
+ "MacOS",
26
+ "secure-enclave-signer"
27
+ );
28
+ if (__dirname$1.includes("/src/")) {
29
+ const projectRoot = __dirname$1.split("/src/")[0];
30
+ return join(
31
+ projectRoot,
32
+ "dist",
33
+ "secure-enclave-signer.app",
34
+ "Contents",
35
+ "MacOS",
36
+ "secure-enclave-signer"
37
+ );
38
+ }
39
+ return bundlePath;
21
40
  }
41
+ const SOCKET_PATH = `/tmp/coinfello-se-signer-${userInfo().username}.sock`;
42
+ const PID_PATH = `/tmp/coinfello-se-signer-${userInfo().username}.pid`;
22
43
  function isSecureEnclaveAvailable() {
23
44
  return platform() === "darwin";
24
45
  }
25
- async function runCommand(args) {
46
+ async function sendDaemonRequest(request) {
47
+ return new Promise((resolve, reject) => {
48
+ const client = createConnection({ path: SOCKET_PATH }, () => {
49
+ const data = JSON.stringify(request);
50
+ client.end(data);
51
+ });
52
+ let response = "";
53
+ client.on("data", (chunk) => {
54
+ response += chunk.toString();
55
+ });
56
+ client.on("end", () => {
57
+ try {
58
+ const parsed = JSON.parse(response.trim());
59
+ if (parsed.success) {
60
+ resolve(parsed.result);
61
+ } else {
62
+ reject(new Error(`SecureEnclave [${parsed.error}]: ${parsed.message}`));
63
+ }
64
+ } catch {
65
+ reject(new Error(`Invalid daemon response: ${response}`));
66
+ }
67
+ });
68
+ client.on("error", (err) => {
69
+ reject(err);
70
+ });
71
+ client.setTimeout(3e4, () => {
72
+ client.destroy();
73
+ reject(new Error("Daemon request timed out"));
74
+ });
75
+ });
76
+ }
77
+ async function isDaemonRunning() {
78
+ try {
79
+ await sendDaemonRequest({ command: "ping" });
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ async function startDaemon() {
86
+ const binaryPath = getBinaryPath();
87
+ const child = spawn(binaryPath, ["daemon"], {
88
+ detached: true,
89
+ stdio: ["ignore", "pipe", "pipe"]
90
+ });
91
+ child.unref();
92
+ return new Promise((resolve, reject) => {
93
+ let stdout = "";
94
+ let stderr = "";
95
+ child.stdout.on("data", (chunk) => {
96
+ stdout += chunk.toString();
97
+ try {
98
+ const ready = JSON.parse(stdout.trim());
99
+ if (ready.status === "ready") {
100
+ child.stdout.removeAllListeners();
101
+ child.stderr.removeAllListeners();
102
+ resolve({ pid: ready.pid, socket: ready.socket });
103
+ }
104
+ } catch {
105
+ }
106
+ });
107
+ child.stderr.on("data", (chunk) => {
108
+ stderr += chunk.toString();
109
+ });
110
+ child.on("error", (err) => {
111
+ reject(new Error(`Failed to start daemon: ${err.message}`));
112
+ });
113
+ child.on("exit", (code) => {
114
+ if (code !== 0) {
115
+ try {
116
+ const parsed = JSON.parse(stderr.trim());
117
+ reject(new Error(`Daemon exited: [${parsed.error}] ${parsed.message}`));
118
+ } catch {
119
+ reject(new Error(`Daemon exited with code ${code}: ${stderr}`));
120
+ }
121
+ }
122
+ });
123
+ setTimeout(() => reject(new Error("Daemon startup timed out")), 15e3);
124
+ });
125
+ }
126
+ async function stopDaemon() {
127
+ try {
128
+ const pidStr = await readFile(PID_PATH, "utf-8");
129
+ const pid = parseInt(pidStr.trim(), 10);
130
+ process.kill(pid, "SIGTERM");
131
+ } catch {
132
+ try {
133
+ await unlink(SOCKET_PATH);
134
+ } catch {
135
+ }
136
+ try {
137
+ await unlink(PID_PATH);
138
+ } catch {
139
+ }
140
+ }
141
+ }
142
+ async function runCommandDirect(args) {
26
143
  const binaryPath = getBinaryPath();
27
144
  try {
28
145
  const { stdout } = await execFileAsync(binaryPath, args, {
@@ -45,6 +162,24 @@ async function runCommand(args) {
45
162
  throw new Error(`SecureEnclave command failed: ${error.message ?? "Unknown error"}`);
46
163
  }
47
164
  }
165
+ async function runCommand(args) {
166
+ try {
167
+ const command = args[0];
168
+ const request = { command };
169
+ if (command === "sign") {
170
+ const tagIdx = args.indexOf("--tag");
171
+ const payloadIdx = args.indexOf("--payload");
172
+ if (tagIdx >= 0 && tagIdx + 1 < args.length) request.tag = args[tagIdx + 1];
173
+ if (payloadIdx >= 0 && payloadIdx + 1 < args.length) request.payload = args[payloadIdx + 1];
174
+ } else if (command === "get-public-key") {
175
+ const tagIdx = args.indexOf("--tag");
176
+ if (tagIdx >= 0 && tagIdx + 1 < args.length) request.tag = args[tagIdx + 1];
177
+ }
178
+ return await sendDaemonRequest(request);
179
+ } catch {
180
+ return await runCommandDirect(args);
181
+ }
182
+ }
48
183
  async function generateKey() {
49
184
  const result = await runCommand(["generate"]);
50
185
  return {
@@ -3257,4 +3392,44 @@ program.command("send_prompt").description("Send a prompt to CoinFello, creating
3257
3392
  process.exit(1);
3258
3393
  }
3259
3394
  });
3395
+ const signerDaemon = program.command("signer-daemon").description("Manage the Secure Enclave signing daemon");
3396
+ signerDaemon.command("start").description("Start the signing daemon (authenticates via Touch ID / password once)").action(async () => {
3397
+ try {
3398
+ const running = await isDaemonRunning();
3399
+ if (running) {
3400
+ console.log("Signing daemon is already running.");
3401
+ return;
3402
+ }
3403
+ console.log("Starting signing daemon (authenticate when prompted)...");
3404
+ const { pid, socket } = await startDaemon();
3405
+ console.log(`Signing daemon started.`);
3406
+ console.log(`PID: ${pid}`);
3407
+ console.log(`Socket: ${socket}`);
3408
+ } catch (err) {
3409
+ console.error(`Failed to start daemon: ${err.message}`);
3410
+ process.exit(1);
3411
+ }
3412
+ });
3413
+ signerDaemon.command("stop").description("Stop the signing daemon").action(async () => {
3414
+ try {
3415
+ const running = await isDaemonRunning();
3416
+ if (!running) {
3417
+ console.log("Signing daemon is not running.");
3418
+ return;
3419
+ }
3420
+ await stopDaemon();
3421
+ console.log("Signing daemon stopped.");
3422
+ } catch (err) {
3423
+ console.error(`Failed to stop daemon: ${err.message}`);
3424
+ process.exit(1);
3425
+ }
3426
+ });
3427
+ signerDaemon.command("status").description("Check if the signing daemon is running").action(async () => {
3428
+ const running = await isDaemonRunning();
3429
+ if (running) {
3430
+ console.log("Signing daemon is running.");
3431
+ } else {
3432
+ console.log("Signing daemon is not running.");
3433
+ }
3434
+ });
3260
3435
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinfello/agent-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",