@coinfello/agent-cli 0.1.12 → 0.1.14

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 {
@@ -3220,22 +3355,26 @@ program.command("send_prompt").description("Send a prompt to CoinFello, creating
3220
3355
  delegateAddress,
3221
3356
  scope
3222
3357
  });
3223
- console.log("Signing subdelegation...");
3358
+ console.log("Signing subdelegation... ", JSON.stringify(subdelegation, null, 4));
3224
3359
  const signature = await smartAccount.signDelegation({
3225
3360
  delegation: subdelegation
3226
3361
  });
3362
+ console.log("Signed subdelegation");
3227
3363
  let sig = signature;
3228
3364
  const chain = resolveChainInput(config.chain);
3229
3365
  const publicClient = createPublicClient(chain);
3366
+ console.log("Getting code...");
3230
3367
  const code = await publicClient.getCode({ address: smartAccount.address });
3231
3368
  const isDeployed = !!(code && code !== "0x");
3232
3369
  if (!isDeployed) {
3370
+ console.log("Getting factory args...");
3233
3371
  const factoryArgs = await smartAccount.getFactoryArgs();
3234
3372
  sig = serializeErc6492Signature({
3235
3373
  signature,
3236
3374
  address: factoryArgs.factory,
3237
3375
  data: factoryArgs.factoryData
3238
3376
  });
3377
+ console.log("Serialized 6492 sig");
3239
3378
  }
3240
3379
  const signedSubdelegation = { ...subdelegation, signature: sig };
3241
3380
  console.log("Sending signed delegation...");
@@ -3257,4 +3396,44 @@ program.command("send_prompt").description("Send a prompt to CoinFello, creating
3257
3396
  process.exit(1);
3258
3397
  }
3259
3398
  });
3399
+ const signerDaemon = program.command("signer-daemon").description("Manage the Secure Enclave signing daemon");
3400
+ signerDaemon.command("start").description("Start the signing daemon (authenticates via Touch ID / password once)").action(async () => {
3401
+ try {
3402
+ const running = await isDaemonRunning();
3403
+ if (running) {
3404
+ console.log("Signing daemon is already running.");
3405
+ return;
3406
+ }
3407
+ console.log("Starting signing daemon (authenticate when prompted)...");
3408
+ const { pid, socket } = await startDaemon();
3409
+ console.log(`Signing daemon started.`);
3410
+ console.log(`PID: ${pid}`);
3411
+ console.log(`Socket: ${socket}`);
3412
+ } catch (err) {
3413
+ console.error(`Failed to start daemon: ${err.message}`);
3414
+ process.exit(1);
3415
+ }
3416
+ });
3417
+ signerDaemon.command("stop").description("Stop the signing daemon").action(async () => {
3418
+ try {
3419
+ const running = await isDaemonRunning();
3420
+ if (!running) {
3421
+ console.log("Signing daemon is not running.");
3422
+ return;
3423
+ }
3424
+ await stopDaemon();
3425
+ console.log("Signing daemon stopped.");
3426
+ } catch (err) {
3427
+ console.error(`Failed to stop daemon: ${err.message}`);
3428
+ process.exit(1);
3429
+ }
3430
+ });
3431
+ signerDaemon.command("status").description("Check if the signing daemon is running").action(async () => {
3432
+ const running = await isDaemonRunning();
3433
+ if (running) {
3434
+ console.log("Signing daemon is running.");
3435
+ } else {
3436
+ console.log("Signing daemon is not running.");
3437
+ }
3438
+ });
3260
3439
  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.14",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",