arc402-cli 0.9.19 → 0.10.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.
Files changed (359) hide show
  1. package/README.md +41 -2
  2. package/dist/abis.d.ts +1 -0
  3. package/dist/abis.d.ts.map +1 -1
  4. package/dist/abis.js +45 -14
  5. package/dist/abis.js.map +1 -1
  6. package/dist/bundler.d.ts +1 -1
  7. package/dist/bundler.d.ts.map +1 -1
  8. package/dist/bundler.js +61 -27
  9. package/dist/bundler.js.map +1 -1
  10. package/dist/client.d.ts +1 -1
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +9 -5
  13. package/dist/client.js.map +1 -1
  14. package/dist/coinbase-smart-wallet.js +4 -1
  15. package/dist/coinbase-smart-wallet.js.map +1 -1
  16. package/dist/commands/accept.js +28 -25
  17. package/dist/commands/accept.js.map +1 -1
  18. package/dist/commands/agent-handshake.js +18 -15
  19. package/dist/commands/agent-handshake.js.map +1 -1
  20. package/dist/commands/agent.js +104 -98
  21. package/dist/commands/agent.js.map +1 -1
  22. package/dist/commands/agreements.js +98 -62
  23. package/dist/commands/agreements.js.map +1 -1
  24. package/dist/commands/arbitrator.js +81 -45
  25. package/dist/commands/arbitrator.js.map +1 -1
  26. package/dist/commands/arena-handshake.d.ts.map +1 -1
  27. package/dist/commands/arena-handshake.js +35 -53
  28. package/dist/commands/arena-handshake.js.map +1 -1
  29. package/dist/commands/arena.js +18 -12
  30. package/dist/commands/arena.js.map +1 -1
  31. package/dist/commands/backup.js +36 -30
  32. package/dist/commands/backup.js.map +1 -1
  33. package/dist/commands/cancel.js +18 -15
  34. package/dist/commands/cancel.js.map +1 -1
  35. package/dist/commands/channel.js +81 -45
  36. package/dist/commands/channel.js.map +1 -1
  37. package/dist/commands/coldstart.js +34 -31
  38. package/dist/commands/coldstart.js.map +1 -1
  39. package/dist/commands/compute.d.ts +14 -0
  40. package/dist/commands/compute.d.ts.map +1 -0
  41. package/dist/commands/compute.js +466 -0
  42. package/dist/commands/compute.js.map +1 -0
  43. package/dist/commands/config.js +30 -24
  44. package/dist/commands/config.js.map +1 -1
  45. package/dist/commands/contract-interaction.js +15 -12
  46. package/dist/commands/contract-interaction.js.map +1 -1
  47. package/dist/commands/daemon.d.ts.map +1 -1
  48. package/dist/commands/daemon.js +135 -98
  49. package/dist/commands/daemon.js.map +1 -1
  50. package/dist/commands/deliver.js +76 -37
  51. package/dist/commands/deliver.js.map +1 -1
  52. package/dist/commands/discover.js +27 -24
  53. package/dist/commands/discover.js.map +1 -1
  54. package/dist/commands/dispute.js +110 -104
  55. package/dist/commands/dispute.js.map +1 -1
  56. package/dist/commands/doctor.js +55 -16
  57. package/dist/commands/doctor.js.map +1 -1
  58. package/dist/commands/endpoint.js +95 -56
  59. package/dist/commands/endpoint.js.map +1 -1
  60. package/dist/commands/feed.js +18 -11
  61. package/dist/commands/feed.js.map +1 -1
  62. package/dist/commands/hire.js +40 -37
  63. package/dist/commands/hire.js.map +1 -1
  64. package/dist/commands/migrate.js +33 -30
  65. package/dist/commands/migrate.js.map +1 -1
  66. package/dist/commands/negotiate.d.ts.map +1 -1
  67. package/dist/commands/negotiate.js +36 -34
  68. package/dist/commands/negotiate.js.map +1 -1
  69. package/dist/commands/openshell.js +104 -68
  70. package/dist/commands/openshell.js.map +1 -1
  71. package/dist/commands/owner.js +20 -17
  72. package/dist/commands/owner.js.map +1 -1
  73. package/dist/commands/policy.js +43 -41
  74. package/dist/commands/policy.js.map +1 -1
  75. package/dist/commands/relay.d.ts.map +1 -1
  76. package/dist/commands/relay.js +51 -18
  77. package/dist/commands/relay.js.map +1 -1
  78. package/dist/commands/remediate.js +23 -20
  79. package/dist/commands/remediate.js.map +1 -1
  80. package/dist/commands/reputation.js +27 -25
  81. package/dist/commands/reputation.js.map +1 -1
  82. package/dist/commands/setup.js +104 -65
  83. package/dist/commands/setup.js.map +1 -1
  84. package/dist/commands/trust.js +20 -17
  85. package/dist/commands/trust.js.map +1 -1
  86. package/dist/commands/verify.js +21 -18
  87. package/dist/commands/verify.js.map +1 -1
  88. package/dist/commands/wallet.d.ts.map +1 -1
  89. package/dist/commands/wallet.js +645 -679
  90. package/dist/commands/wallet.js.map +1 -1
  91. package/dist/commands/watch.js +36 -33
  92. package/dist/commands/watch.js.map +1 -1
  93. package/dist/commands/watchtower.js +73 -37
  94. package/dist/commands/watchtower.js.map +1 -1
  95. package/dist/commands/workroom.d.ts.map +1 -1
  96. package/dist/commands/workroom.js +282 -143
  97. package/dist/commands/workroom.js.map +1 -1
  98. package/dist/config.d.ts +3 -0
  99. package/dist/config.d.ts.map +1 -1
  100. package/dist/config.js +71 -22
  101. package/dist/config.js.map +1 -1
  102. package/dist/daemon/compute-metering.d.ts +61 -0
  103. package/dist/daemon/compute-metering.d.ts.map +1 -0
  104. package/dist/daemon/compute-metering.js +299 -0
  105. package/dist/daemon/compute-metering.js.map +1 -0
  106. package/dist/daemon/compute-session.d.ts +100 -0
  107. package/dist/daemon/compute-session.d.ts.map +1 -0
  108. package/dist/daemon/compute-session.js +231 -0
  109. package/dist/daemon/compute-session.js.map +1 -0
  110. package/dist/daemon/config.d.ts +19 -1
  111. package/dist/daemon/config.d.ts.map +1 -1
  112. package/dist/daemon/config.js +90 -16
  113. package/dist/daemon/config.js.map +1 -1
  114. package/dist/daemon/credentials.d.ts +24 -0
  115. package/dist/daemon/credentials.d.ts.map +1 -0
  116. package/dist/daemon/credentials.js +80 -0
  117. package/dist/daemon/credentials.js.map +1 -0
  118. package/dist/daemon/delivery-client.d.ts +35 -0
  119. package/dist/daemon/delivery-client.d.ts.map +1 -0
  120. package/dist/daemon/delivery-client.js +231 -0
  121. package/dist/daemon/delivery-client.js.map +1 -0
  122. package/dist/daemon/file-delivery.d.ts +98 -0
  123. package/dist/daemon/file-delivery.d.ts.map +1 -0
  124. package/dist/daemon/file-delivery.js +461 -0
  125. package/dist/daemon/file-delivery.js.map +1 -0
  126. package/dist/daemon/hire-listener.d.ts +3 -3
  127. package/dist/daemon/hire-listener.d.ts.map +1 -1
  128. package/dist/daemon/hire-listener.js +47 -13
  129. package/dist/daemon/hire-listener.js.map +1 -1
  130. package/dist/daemon/index.d.ts +2 -1
  131. package/dist/daemon/index.d.ts.map +1 -1
  132. package/dist/daemon/index.js +526 -53
  133. package/dist/daemon/index.js.map +1 -1
  134. package/dist/daemon/job-lifecycle.d.ts +1 -1
  135. package/dist/daemon/job-lifecycle.d.ts.map +1 -1
  136. package/dist/daemon/job-lifecycle.js +51 -11
  137. package/dist/daemon/job-lifecycle.js.map +1 -1
  138. package/dist/daemon/notify.d.ts +1 -1
  139. package/dist/daemon/notify.d.ts.map +1 -1
  140. package/dist/daemon/notify.js +53 -19
  141. package/dist/daemon/notify.js.map +1 -1
  142. package/dist/daemon/token-metering.js +47 -8
  143. package/dist/daemon/token-metering.js.map +1 -1
  144. package/dist/daemon/userops.d.ts +2 -2
  145. package/dist/daemon/userops.d.ts.map +1 -1
  146. package/dist/daemon/userops.js +27 -23
  147. package/dist/daemon/userops.js.map +1 -1
  148. package/dist/daemon/wallet-monitor.d.ts +1 -1
  149. package/dist/daemon/wallet-monitor.d.ts.map +1 -1
  150. package/dist/daemon/wallet-monitor.js +12 -8
  151. package/dist/daemon/wallet-monitor.js.map +1 -1
  152. package/dist/daemon/worker-executor.d.ts +71 -0
  153. package/dist/daemon/worker-executor.d.ts.map +1 -0
  154. package/dist/daemon/worker-executor.js +382 -0
  155. package/dist/daemon/worker-executor.js.map +1 -0
  156. package/dist/drain-v4.js +64 -26
  157. package/dist/drain-v4.js.map +1 -1
  158. package/dist/endpoint-config.js +63 -20
  159. package/dist/endpoint-config.js.map +1 -1
  160. package/dist/endpoint-notify.d.ts.map +1 -1
  161. package/dist/endpoint-notify.js +49 -15
  162. package/dist/endpoint-notify.js.map +1 -1
  163. package/dist/index.js +50 -18
  164. package/dist/index.js.map +1 -1
  165. package/dist/openshell-runtime.d.ts.map +1 -1
  166. package/dist/openshell-runtime.js +82 -38
  167. package/dist/openshell-runtime.js.map +1 -1
  168. package/dist/program.d.ts.map +1 -1
  169. package/dist/program.js +85 -78
  170. package/dist/program.js.map +1 -1
  171. package/dist/repl.js +31 -25
  172. package/dist/repl.js.map +1 -1
  173. package/dist/signing.js +6 -3
  174. package/dist/signing.js.map +1 -1
  175. package/dist/telegram-notify.js +40 -3
  176. package/dist/telegram-notify.js.map +1 -1
  177. package/dist/tui/App.d.ts.map +1 -1
  178. package/dist/tui/App.js +56 -89
  179. package/dist/tui/App.js.map +1 -1
  180. package/dist/tui/Footer.js +7 -4
  181. package/dist/tui/Footer.js.map +1 -1
  182. package/dist/tui/Header.d.ts +1 -1
  183. package/dist/tui/Header.d.ts.map +1 -1
  184. package/dist/tui/Header.js +14 -9
  185. package/dist/tui/Header.js.map +1 -1
  186. package/dist/tui/InputLine.d.ts +2 -1
  187. package/dist/tui/InputLine.d.ts.map +1 -1
  188. package/dist/tui/InputLine.js +47 -97
  189. package/dist/tui/InputLine.js.map +1 -1
  190. package/dist/tui/Viewport.d.ts +1 -2
  191. package/dist/tui/Viewport.d.ts.map +1 -1
  192. package/dist/tui/Viewport.js +26 -6
  193. package/dist/tui/Viewport.js.map +1 -1
  194. package/dist/tui/WalletConnectPairing.js +19 -16
  195. package/dist/tui/WalletConnectPairing.js.map +1 -1
  196. package/dist/tui/components/Button.js +9 -6
  197. package/dist/tui/components/Button.js.map +1 -1
  198. package/dist/tui/components/CeremonyView.js +8 -5
  199. package/dist/tui/components/CeremonyView.js.map +1 -1
  200. package/dist/tui/components/CompletionDropdown.js +9 -6
  201. package/dist/tui/components/CompletionDropdown.js.map +1 -1
  202. package/dist/tui/components/ConfirmPrompt.js +8 -5
  203. package/dist/tui/components/ConfirmPrompt.js.map +1 -1
  204. package/dist/tui/components/CustomTextInput.js +14 -11
  205. package/dist/tui/components/CustomTextInput.js.map +1 -1
  206. package/dist/tui/components/InteractiveTable.js +12 -9
  207. package/dist/tui/components/InteractiveTable.js.map +1 -1
  208. package/dist/tui/components/StepSpinner.js +13 -10
  209. package/dist/tui/components/StepSpinner.js.map +1 -1
  210. package/dist/tui/components/Toast.js +12 -8
  211. package/dist/tui/components/Toast.js.map +1 -1
  212. package/dist/tui/index.d.ts.map +1 -1
  213. package/dist/tui/index.js +21 -28
  214. package/dist/tui/index.js.map +1 -1
  215. package/dist/tui/useChat.js +19 -13
  216. package/dist/tui/useChat.js.map +1 -1
  217. package/dist/tui/useCommand.d.ts +2 -3
  218. package/dist/tui/useCommand.d.ts.map +1 -1
  219. package/dist/tui/useCommand.js +24 -100
  220. package/dist/tui/useCommand.js.map +1 -1
  221. package/dist/tui/useNotifications.js +8 -5
  222. package/dist/tui/useNotifications.js.map +1 -1
  223. package/dist/tui/useScroll.d.ts.map +1 -1
  224. package/dist/tui/useScroll.js +12 -15
  225. package/dist/tui/useScroll.js.map +1 -1
  226. package/dist/ui/banner.d.ts +0 -12
  227. package/dist/ui/banner.d.ts.map +1 -1
  228. package/dist/ui/banner.js +19 -35
  229. package/dist/ui/banner.js.map +1 -1
  230. package/dist/ui/colors.js +19 -13
  231. package/dist/ui/colors.js.map +1 -1
  232. package/dist/ui/format.js +14 -6
  233. package/dist/ui/format.js.map +1 -1
  234. package/dist/ui/qr-render.js +11 -4
  235. package/dist/ui/qr-render.js.map +1 -1
  236. package/dist/ui/rpc-fallback.js +11 -6
  237. package/dist/ui/rpc-fallback.js.map +1 -1
  238. package/dist/ui/spinner.js +12 -6
  239. package/dist/ui/spinner.js.map +1 -1
  240. package/dist/ui/tree.js +6 -3
  241. package/dist/ui/tree.js.map +1 -1
  242. package/dist/utils/format.js +41 -27
  243. package/dist/utils/format.js.map +1 -1
  244. package/dist/utils/hash.js +42 -4
  245. package/dist/utils/hash.js.map +1 -1
  246. package/dist/utils/time.js +6 -2
  247. package/dist/utils/time.js.map +1 -1
  248. package/dist/wallet-router.d.ts +1 -1
  249. package/dist/wallet-router.d.ts.map +1 -1
  250. package/dist/wallet-router.js +19 -12
  251. package/dist/wallet-router.js.map +1 -1
  252. package/dist/walletconnect-session.d.ts +1 -1
  253. package/dist/walletconnect-session.d.ts.map +1 -1
  254. package/dist/walletconnect-session.js +11 -6
  255. package/dist/walletconnect-session.js.map +1 -1
  256. package/dist/walletconnect.d.ts +5 -6
  257. package/dist/walletconnect.d.ts.map +1 -1
  258. package/dist/walletconnect.js +35 -32
  259. package/dist/walletconnect.js.map +1 -1
  260. package/package.json +11 -10
  261. package/INK6-UX-SPEC.md +0 -446
  262. package/MIGRATION-SPEC.md +0 -108
  263. package/TUI-SPEC.md +0 -214
  264. package/scripts/authorize-machine-key.ts +0 -43
  265. package/scripts/drain-wallet.ts +0 -149
  266. package/scripts/execute-spend-only.ts +0 -81
  267. package/scripts/register-agent-userop.ts +0 -186
  268. package/src/abis.ts +0 -187
  269. package/src/bundler.ts +0 -235
  270. package/src/client.ts +0 -36
  271. package/src/coinbase-smart-wallet.ts +0 -51
  272. package/src/commands/accept.ts +0 -64
  273. package/src/commands/agent-handshake.ts +0 -72
  274. package/src/commands/agent.ts +0 -691
  275. package/src/commands/agreements.ts +0 -350
  276. package/src/commands/arbitrator.ts +0 -180
  277. package/src/commands/arena-handshake.ts +0 -274
  278. package/src/commands/arena.ts +0 -122
  279. package/src/commands/backup.ts +0 -117
  280. package/src/commands/cancel.ts +0 -35
  281. package/src/commands/channel.ts +0 -218
  282. package/src/commands/coldstart.ts +0 -165
  283. package/src/commands/config.ts +0 -68
  284. package/src/commands/contract-interaction.ts +0 -166
  285. package/src/commands/daemon.ts +0 -1054
  286. package/src/commands/deliver.ts +0 -148
  287. package/src/commands/discover.ts +0 -350
  288. package/src/commands/dispute.ts +0 -375
  289. package/src/commands/doctor.ts +0 -172
  290. package/src/commands/endpoint.ts +0 -620
  291. package/src/commands/feed.ts +0 -229
  292. package/src/commands/hire.ts +0 -245
  293. package/src/commands/migrate.ts +0 -177
  294. package/src/commands/negotiate.ts +0 -272
  295. package/src/commands/openshell.ts +0 -1055
  296. package/src/commands/owner.ts +0 -35
  297. package/src/commands/policy.ts +0 -263
  298. package/src/commands/relay.ts +0 -277
  299. package/src/commands/remediate.ts +0 -24
  300. package/src/commands/reputation.ts +0 -79
  301. package/src/commands/setup.ts +0 -343
  302. package/src/commands/trust.ts +0 -27
  303. package/src/commands/verify.ts +0 -91
  304. package/src/commands/wallet.ts +0 -3548
  305. package/src/commands/watch.ts +0 -220
  306. package/src/commands/watchtower.ts +0 -248
  307. package/src/commands/workroom.ts +0 -963
  308. package/src/config.ts +0 -220
  309. package/src/daemon/config.ts +0 -344
  310. package/src/daemon/hire-listener.ts +0 -226
  311. package/src/daemon/index.ts +0 -1089
  312. package/src/daemon/job-lifecycle.ts +0 -215
  313. package/src/daemon/notify.ts +0 -297
  314. package/src/daemon/token-metering.ts +0 -183
  315. package/src/daemon/userops.ts +0 -119
  316. package/src/daemon/wallet-monitor.ts +0 -90
  317. package/src/drain-v4.ts +0 -159
  318. package/src/endpoint-config.ts +0 -83
  319. package/src/endpoint-notify.ts +0 -134
  320. package/src/index.ts +0 -74
  321. package/src/openshell-runtime.ts +0 -281
  322. package/src/program.ts +0 -88
  323. package/src/repl.ts +0 -178
  324. package/src/signing.ts +0 -28
  325. package/src/telegram-notify.ts +0 -88
  326. package/src/tui/App.tsx +0 -263
  327. package/src/tui/Footer.tsx +0 -18
  328. package/src/tui/Header.tsx +0 -45
  329. package/src/tui/InputLine.tsx +0 -243
  330. package/src/tui/Viewport.tsx +0 -51
  331. package/src/tui/WalletConnectPairing.tsx +0 -114
  332. package/src/tui/components/Button.tsx +0 -38
  333. package/src/tui/components/CeremonyView.tsx +0 -39
  334. package/src/tui/components/CompletionDropdown.tsx +0 -56
  335. package/src/tui/components/ConfirmPrompt.tsx +0 -36
  336. package/src/tui/components/CustomTextInput.tsx +0 -132
  337. package/src/tui/components/InteractiveTable.tsx +0 -106
  338. package/src/tui/components/StepSpinner.tsx +0 -84
  339. package/src/tui/components/Toast.tsx +0 -59
  340. package/src/tui/index.tsx +0 -90
  341. package/src/tui/useChat.ts +0 -103
  342. package/src/tui/useCommand.ts +0 -238
  343. package/src/tui/useNotifications.ts +0 -28
  344. package/src/tui/useScroll.ts +0 -69
  345. package/src/ui/banner.ts +0 -78
  346. package/src/ui/colors.ts +0 -30
  347. package/src/ui/format.ts +0 -78
  348. package/src/ui/qr-render.ts +0 -92
  349. package/src/ui/rpc-fallback.ts +0 -59
  350. package/src/ui/spinner.ts +0 -56
  351. package/src/ui/tree.ts +0 -16
  352. package/src/utils/format.ts +0 -48
  353. package/src/utils/hash.ts +0 -5
  354. package/src/utils/time.ts +0 -15
  355. package/src/wallet-router.ts +0 -178
  356. package/src/walletconnect-session.ts +0 -27
  357. package/src/walletconnect.ts +0 -309
  358. package/test/time.test.js +0 -11
  359. package/tsconfig.json +0 -33
@@ -1,1054 +0,0 @@
1
- import { Command } from "commander";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import * as net from "net";
5
- import * as os from "os";
6
- import { spawn, spawnSync } from "child_process";
7
- import { ethers } from "ethers";
8
- import prompts from "prompts";
9
- import { loadConfig } from "../config.js";
10
- import { requireSigner } from "../client.js";
11
- import { startSpinner } from "../ui/spinner.js";
12
- import { renderTree } from "../ui/tree.js";
13
- import { c } from "../ui/colors.js";
14
- import { SERVICE_AGREEMENT_ABI } from "../abis.js";
15
- import {
16
- DAEMON_DIR,
17
- DAEMON_PID,
18
- DAEMON_LOG,
19
- DAEMON_SOCK,
20
- DAEMON_TOML,
21
- TEMPLATE_DAEMON_TOML,
22
- loadDaemonConfig,
23
- } from "../daemon/config.js";
24
- import { buildNotifier } from "../daemon/notify.js";
25
- import {
26
- buildOpenShellSecretExports,
27
- buildOpenShellSshConfig,
28
- DEFAULT_RUNTIME_REMOTE_ROOT,
29
- provisionFileToSandbox,
30
- provisionRuntimeToSandbox,
31
- readOpenShellConfig,
32
- runCmd,
33
- writeOpenShellConfig,
34
- } from "../openshell-runtime.js";
35
-
36
- import { fileURLToPath } from "node:url";
37
- const __filename = fileURLToPath(import.meta.url);
38
- const __dirname = path.dirname(__filename);
39
-
40
- // ─── Constants ────────────────────────────────────────────────────────────────
41
-
42
- const CHANNEL_STATES_DIR = path.join(os.homedir(), ".arc402", "channel-states");
43
-
44
- // ─── Harness registry ─────────────────────────────────────────────────────────
45
-
46
- const HARNESS_REGISTRY: Record<string, string> = {
47
- openclaw: "openclaw run {task}",
48
- claude: "claude --dangerously-skip-permissions {task}",
49
- codex: "codex {task}",
50
- opencode: "opencode {task}",
51
- };
52
-
53
- // ChannelStatus enum (mirrors ServiceAgreement.ChannelStatus)
54
- const ChannelStatus = { OPEN: 0, CLOSING: 1, CHALLENGED: 2, SETTLED: 3 } as const;
55
-
56
- // ─── Local state store ────────────────────────────────────────────────────────
57
-
58
- interface LocalChannelState {
59
- channelId: string;
60
- sequenceNumber: string | number;
61
- callCount: string | number;
62
- cumulativePayment: string;
63
- token: string;
64
- timestamp: string | number;
65
- clientSig: string;
66
- providerSig: string;
67
- }
68
-
69
- function getStatePath(channelId: string): string {
70
- return path.join(CHANNEL_STATES_DIR, `${channelId}.json`);
71
- }
72
-
73
- function loadLocalState(channelId: string): LocalChannelState | null {
74
- const p = getStatePath(channelId);
75
- if (!fs.existsSync(p)) return null;
76
- try {
77
- return JSON.parse(fs.readFileSync(p, "utf-8")) as LocalChannelState;
78
- } catch {
79
- return null;
80
- }
81
- }
82
-
83
- /**
84
- * ABI-encode a ChannelState for submission to challengeChannel().
85
- */
86
- function encodeChannelState(state: LocalChannelState): string {
87
- return ethers.AbiCoder.defaultAbiCoder().encode(
88
- ["tuple(bytes32,uint256,uint256,uint256,address,uint256,bytes,bytes)"],
89
- [[
90
- state.channelId,
91
- BigInt(state.sequenceNumber),
92
- BigInt(state.callCount),
93
- BigInt(state.cumulativePayment),
94
- state.token,
95
- BigInt(state.timestamp),
96
- state.clientSig,
97
- state.providerSig,
98
- ]]
99
- );
100
- }
101
-
102
- // ─── Channel-watch loop ───────────────────────────────────────────────────────
103
-
104
- async function runChannelWatchLoop(opts: {
105
- pollInterval: number;
106
- wallet: string;
107
- contract: ethers.Contract;
108
- json: boolean;
109
- }): Promise<void> {
110
- const { pollInterval, wallet, contract, json } = opts;
111
-
112
- const log = (data: Record<string, unknown> | string) => {
113
- const out: Record<string, unknown> =
114
- typeof data === "string"
115
- ? { msg: data, ts: Date.now() }
116
- : { ...data, ts: Date.now() };
117
- if (json) {
118
- console.log(JSON.stringify(out));
119
- } else {
120
- const ts = new Date(out.ts as number).toISOString();
121
- if ("msg" in out) {
122
- console.log(`[${ts}] ${out.msg}`);
123
- } else {
124
- const { ts: _ts, ...rest } = out;
125
- console.log(`[${ts}] ${JSON.stringify(rest)}`);
126
- }
127
- }
128
- };
129
-
130
- log(`channel-watch started for ${wallet}`);
131
- log(`poll interval: ${pollInterval}ms`);
132
- log(`state store: ${CHANNEL_STATES_DIR}`);
133
-
134
- const poll = async () => {
135
- try {
136
- const clientChannels: string[] = await contract.getChannelsByClient(wallet);
137
- const providerChannels: string[] = await contract.getChannelsByProvider(wallet);
138
- const allChannels = [...new Set([...clientChannels, ...providerChannels])];
139
-
140
- for (const channelId of allChannels) {
141
- try {
142
- const ch = await contract.getChannel(channelId);
143
- const status = Number(ch.status);
144
-
145
- if (status !== ChannelStatus.CLOSING) continue;
146
-
147
- const now = Math.floor(Date.now() / 1000);
148
- const challengeExpiry = Number(ch.challengeExpiry);
149
- if (now > challengeExpiry) continue;
150
-
151
- const localState = loadLocalState(channelId);
152
- if (!localState) {
153
- log({ event: "no_local_state", channelId });
154
- continue;
155
- }
156
-
157
- const localSeq = BigInt(localState.sequenceNumber);
158
- const chainSeq = BigInt(ch.lastSequenceNumber);
159
-
160
- if (localSeq > chainSeq) {
161
- log({
162
- event: "stale_close_detected",
163
- channelId,
164
- chainSeq: chainSeq.toString(),
165
- localSeq: localSeq.toString(),
166
- windowExpiresAt: new Date(challengeExpiry * 1000).toISOString(),
167
- });
168
-
169
- const encoded = encodeChannelState(localState);
170
- const tx = await contract.challengeChannel(channelId, encoded);
171
- const receipt = await tx.wait();
172
- log({ event: "challenge_submitted", channelId, txHash: receipt.hash });
173
- }
174
- } catch (err) {
175
- log({ event: "channel_error", channelId, error: String(err) });
176
- }
177
- }
178
- } catch (err) {
179
- log({ event: "poll_error", error: String(err) });
180
- }
181
- };
182
-
183
- await poll();
184
- const intervalId = setInterval(() => { void poll(); }, pollInterval);
185
-
186
- process.on("SIGINT", () => {
187
- clearInterval(intervalId);
188
- log("channel-watch stopped");
189
- process.exit(0);
190
- });
191
-
192
- process.stdin.resume();
193
- }
194
-
195
- // ─── IPC helper ───────────────────────────────────────────────────────────────
196
-
197
- type IpcResponse = { ok: boolean; data?: unknown; error?: string };
198
-
199
- function sendIpcCommand(cmd: Record<string, unknown>): Promise<IpcResponse> {
200
- if (!fs.existsSync(DAEMON_SOCK)) {
201
- console.error("Daemon is not running. Launch path: run `arc402 openshell init` first, then `arc402 daemon start`.");
202
- process.exit(1);
203
- }
204
-
205
- return new Promise((resolve, reject) => {
206
- const socket = net.createConnection(DAEMON_SOCK, () => {
207
- socket.write(JSON.stringify(cmd) + "\n");
208
- });
209
-
210
- let buf = "";
211
- socket.on("data", (data) => {
212
- buf += data.toString();
213
- const lines = buf.split("\n");
214
- buf = lines.pop() ?? "";
215
- for (const line of lines) {
216
- if (!line.trim()) continue;
217
- try {
218
- const parsed = JSON.parse(line) as IpcResponse;
219
- socket.destroy();
220
- resolve(parsed);
221
- } catch {
222
- socket.destroy();
223
- reject(new Error("Invalid JSON response from daemon"));
224
- }
225
- return;
226
- }
227
- });
228
-
229
- socket.on("error", (err) => {
230
- if ((err as NodeJS.ErrnoException).code === "ENOENT" ||
231
- (err as NodeJS.ErrnoException).code === "ECONNREFUSED") {
232
- console.error("Daemon is not running. Launch path: run `arc402 openshell init` first, then `arc402 daemon start`.");
233
- process.exit(1);
234
- }
235
- reject(err);
236
- });
237
-
238
- socket.setTimeout(5000, () => {
239
- socket.destroy();
240
- reject(new Error("IPC timeout — daemon did not respond within 5 seconds"));
241
- });
242
- });
243
- }
244
-
245
- // ─── PID helpers ─────────────────────────────────────────────────────────────
246
-
247
- function readPid(): number | null {
248
- if (!fs.existsSync(DAEMON_PID)) return null;
249
- const raw = fs.readFileSync(DAEMON_PID, "utf-8").trim();
250
- const pid = parseInt(raw, 10);
251
- return isNaN(pid) ? null : pid;
252
- }
253
-
254
- function isProcessAlive(pid: number): boolean {
255
- try {
256
- process.kill(pid, 0);
257
- return true;
258
- } catch {
259
- return false;
260
- }
261
- }
262
-
263
- // ─── Start helpers ────────────────────────────────────────────────────────────
264
-
265
- const REMOTE_ARC402_DIR = "/sandbox/.arc402";
266
- const REMOTE_DAEMON_PID = path.posix.join(REMOTE_ARC402_DIR, "daemon.pid");
267
- const REMOTE_DAEMON_LOG = path.posix.join(REMOTE_ARC402_DIR, "daemon.log");
268
- const REMOTE_DAEMON_TOML = path.posix.join(REMOTE_ARC402_DIR, "daemon.toml");
269
-
270
- function syncDaemonConfigToSandbox(sandboxName: string): void {
271
- if (!fs.existsSync(DAEMON_TOML)) {
272
- throw new Error("daemon.toml not found. Run `arc402 daemon init` first.");
273
- }
274
- provisionFileToSandbox(sandboxName, DAEMON_TOML, REMOTE_DAEMON_TOML);
275
- }
276
-
277
- async function readRemotePid(sandboxName: string): Promise<number | null> {
278
- const { configPath, host } = buildOpenShellSshConfig(sandboxName);
279
- const pidRead = runCmd("ssh", [
280
- "-F", configPath,
281
- host,
282
- `test -f ${REMOTE_DAEMON_PID} && cat ${REMOTE_DAEMON_PID}`,
283
- ], { timeout: 20000 });
284
- if (!pidRead.ok || !pidRead.stdout.trim()) return null;
285
- const pid = parseInt(pidRead.stdout.trim(), 10);
286
- return Number.isFinite(pid) ? pid : null;
287
- }
288
-
289
- async function startDaemonBackground(sandboxName?: string, runtimeRemoteRoot?: string): Promise<void> {
290
- // Resolve the compiled daemon entry point
291
- const daemonEntry = path.join(__dirname, "..", "daemon", "index.js");
292
- if (!fs.existsSync(daemonEntry)) {
293
- console.error(`Daemon entry not found at ${daemonEntry}. Run: npm run build`);
294
- process.exit(1);
295
- }
296
-
297
- const childEnv: NodeJS.ProcessEnv = {
298
- ...process.env,
299
- ARC402_DAEMON_PROCESS: "1",
300
- };
301
-
302
- let child: ReturnType<typeof spawn>;
303
- if (sandboxName) {
304
- syncDaemonConfigToSandbox(sandboxName);
305
- const { configPath, host } = buildOpenShellSshConfig(sandboxName);
306
- const remoteRoot = runtimeRemoteRoot ?? DEFAULT_RUNTIME_REMOTE_ROOT;
307
- const remoteDaemonEntry = path.posix.join(remoteRoot, "dist/daemon/index.js");
308
- const secretExports = buildOpenShellSecretExports(true);
309
- const remoteCommand = [
310
- `mkdir -p ${REMOTE_ARC402_DIR}`,
311
- `rm -f ${REMOTE_DAEMON_PID}`,
312
- secretExports,
313
- `nohup env HOME=/sandbox ARC402_DAEMON_PROCESS=1 node ${remoteDaemonEntry} > /tmp/arc402-daemon-stdout.log 2> /tmp/arc402-daemon-stderr.log < /dev/null &`,
314
- ].filter(Boolean).join(" && ");
315
- child = spawn("ssh", [
316
- "-F", configPath,
317
- host,
318
- remoteCommand,
319
- ], {
320
- detached: true,
321
- stdio: "ignore",
322
- env: childEnv,
323
- });
324
- } else {
325
- // Direct mode — pass credentials from CLI config
326
- let machineKey: string | undefined;
327
- let telegramBotToken: string | undefined;
328
- let telegramChatId: string | undefined;
329
- try {
330
- const config = loadConfig();
331
- machineKey = config.privateKey;
332
- telegramBotToken = config.telegramBotToken;
333
- telegramChatId = config.telegramChatId;
334
- } catch {
335
- // Config load is optional here — daemon will use its own daemon.toml
336
- }
337
- if (machineKey) childEnv["ARC402_MACHINE_KEY"] = machineKey;
338
- if (telegramBotToken) childEnv["TELEGRAM_BOT_TOKEN"] = telegramBotToken;
339
- if (telegramChatId) childEnv["TELEGRAM_CHAT_ID"] = telegramChatId;
340
-
341
- child = spawn(process.execPath, [daemonEntry], {
342
- detached: true,
343
- stdio: "ignore",
344
- env: childEnv,
345
- });
346
- }
347
- child.unref();
348
-
349
- // Wait up to 8 seconds for PID file to appear
350
- const deadline = Date.now() + 8000;
351
- while (Date.now() < deadline) {
352
- await new Promise((r) => setTimeout(r, 400));
353
- if (sandboxName) {
354
- const remotePid = await readRemotePid(sandboxName);
355
- if (remotePid) {
356
- console.log(` ${c.success} ARC-402 daemon started (OpenShell)`);
357
- renderTree([
358
- { label: "PID", value: String(remotePid) },
359
- { label: "Log", value: REMOTE_DAEMON_LOG, last: true },
360
- ]);
361
- return;
362
- }
363
- } else {
364
- const pid = readPid();
365
- if (pid && isProcessAlive(pid)) {
366
- console.log(` ${c.success} ARC-402 daemon started`);
367
- renderTree([
368
- { label: "PID", value: String(pid) },
369
- { label: "Log", value: DAEMON_LOG, last: true },
370
- ]);
371
- return;
372
- }
373
- }
374
- }
375
-
376
- console.error("Daemon did not start within 8 seconds.");
377
- if (sandboxName) {
378
- console.error("Likely cause: the provisioned runtime booted, but the sandbox daemon config/state path is incomplete or the daemon exited early.");
379
- console.error(`Expected remote log: ${REMOTE_DAEMON_LOG}`);
380
- } else {
381
- console.error(`Check logs: ${DAEMON_LOG}`);
382
- }
383
- process.exit(1);
384
- }
385
-
386
- async function stopDaemon(opts: { wait?: boolean } = {}): Promise<boolean> {
387
- const pid = readPid();
388
- if (!pid) {
389
- return false; // not running
390
- }
391
-
392
- if (!isProcessAlive(pid)) {
393
- // Stale PID file
394
- fs.unlinkSync(DAEMON_PID);
395
- return false;
396
- }
397
-
398
- try {
399
- process.kill(pid, "SIGTERM");
400
- } catch {
401
- console.error(`Failed to send SIGTERM to PID ${pid}`);
402
- return false;
403
- }
404
-
405
- if (opts.wait !== false) {
406
- // Wait up to 10 seconds for process to exit
407
- const deadline = Date.now() + 10000;
408
- while (Date.now() < deadline) {
409
- await new Promise((r) => setTimeout(r, 200));
410
- if (!isProcessAlive(pid)) {
411
- // Clean up stale PID file if daemon didn't remove it
412
- if (fs.existsSync(DAEMON_PID)) fs.unlinkSync(DAEMON_PID);
413
- return true;
414
- }
415
- }
416
- console.error(`Daemon (PID ${pid}) did not exit within 10 seconds`);
417
- return false;
418
- }
419
-
420
- return true;
421
- }
422
-
423
- // ─── Output formatters ────────────────────────────────────────────────────────
424
-
425
- function formatStatus(data: Record<string, unknown>): void {
426
- const relayStatus = data.relay_enabled
427
- ? `active — polling ${data.relay_url || "relay"} every ${data.relay_poll_seconds}s`
428
- : "disabled";
429
- const watchtowerStatus = data.watchtower_enabled ? "active" : "disabled";
430
- const bundlerStatus = `${data.bundler_mode} — ${data.bundler_endpoint || "default"}`;
431
- const pending = Number(data.pending_approval ?? 0);
432
-
433
- console.log(`${c.mark} ARC-402 Daemon Status`);
434
- console.log();
435
- renderTree([
436
- { label: "State", value: String(data.state ?? "unknown") },
437
- { label: "PID", value: String(data.pid ?? "unknown") },
438
- { label: "Uptime", value: String(data.uptime ?? "unknown") },
439
- { label: "Wallet", value: String(data.wallet ?? "unknown") },
440
- { label: "Key", value: String(data.machine_key_address ?? "unknown") },
441
- { label: "Relay", value: relayStatus },
442
- { label: "Watchtower", value: watchtowerStatus },
443
- { label: "Bundler", value: bundlerStatus },
444
- { label: "Agreements", value: String(data.active_agreements ?? 0) },
445
- {
446
- label: "Pending",
447
- value: pending > 0 ? `${pending} ← arc402 daemon pending` : "0",
448
- last: true,
449
- },
450
- ]);
451
- }
452
-
453
- interface HireRow {
454
- id: string;
455
- hirer_address: string;
456
- capability: string;
457
- price_eth: string;
458
- deadline_unix: number;
459
- status: string;
460
- created_at: number;
461
- }
462
-
463
- function formatHireTable(rows: HireRow[]): void {
464
- if (rows.length === 0) {
465
- console.log("(none)");
466
- return;
467
- }
468
- const cols = ["ID", "Hirer", "Capability", "Price (ETH)", "Deadline", "Status"];
469
- const widths = [20, 14, 20, 12, 22, 18];
470
- const header = cols.map((c, i) => c.padEnd(widths[i])).join(" ");
471
- const sep = cols.map((_, i) => "─".repeat(widths[i])).join(" ");
472
- console.log(header);
473
- console.log(sep);
474
- for (const row of rows) {
475
- const deadline = row.deadline_unix
476
- ? new Date(row.deadline_unix * 1000).toISOString()
477
- : "none";
478
- const cols2 = [
479
- row.id.slice(0, 18),
480
- row.hirer_address.slice(0, 12) + "...",
481
- (row.capability || "").slice(0, 18),
482
- row.price_eth || "0",
483
- deadline.replace("T", " ").replace("Z", ""),
484
- row.status,
485
- ];
486
- console.log(cols2.map((c, i) => String(c).padEnd(widths[i])).join(" "));
487
- }
488
- }
489
-
490
- // ─── Command registration ─────────────────────────────────────────────────────
491
-
492
- export function registerDaemonCommands(program: Command): void {
493
- const daemon = program
494
- .command("daemon")
495
- .description("ARC-402 daemon management and hire request workflow (Spec 32)");
496
-
497
- // ── daemon start ────────────────────────────────────────────────────────────
498
- daemon
499
- .command("start")
500
- .description("Start the ARC-402 runtime. For launch, this is the OpenShell-owned sandboxed runtime path; public ingress remains a separate host-managed concern.")
501
- .option("--foreground", "Run in foreground (blocking). Used by systemd/launchd service managers.")
502
- .option("--host", "Run on host directly, bypassing the OpenShell sandbox. Use when sandbox RPC proxy is unavailable.")
503
- .action(async (opts) => {
504
- const foreground = opts.foreground as boolean | undefined;
505
- const forceHost = opts.host as boolean | undefined;
506
-
507
- if (!fs.existsSync(DAEMON_TOML)) {
508
- console.error("daemon.toml not found.");
509
- console.error("Run `arc402 daemon init` first so the OpenShell-owned runtime has a wallet, relay, and harness configuration to boot.");
510
- process.exit(1);
511
- }
512
-
513
- const openShellCfg = forceHost ? undefined : readOpenShellConfig();
514
- const sandboxName = openShellCfg?.sandbox.name;
515
- if (forceHost) {
516
- console.log("Running in host mode (--host). Sandbox bypassed.");
517
- }
518
- let runtimeRemoteRoot = openShellCfg?.runtime?.remote_root ?? DEFAULT_RUNTIME_REMOTE_ROOT;
519
-
520
- if (sandboxName) {
521
- try {
522
- const provisioned = provisionRuntimeToSandbox(sandboxName, runtimeRemoteRoot);
523
- runtimeRemoteRoot = provisioned.remoteRoot;
524
- writeOpenShellConfig({
525
- sandbox: openShellCfg?.sandbox ?? { name: sandboxName },
526
- runtime: {
527
- local_tarball: provisioned.tarballPath,
528
- remote_root: provisioned.remoteRoot,
529
- synced_at: new Date().toISOString(),
530
- },
531
- });
532
- } catch (err) {
533
- console.error(`Failed to sync ARC-402 runtime into OpenShell: ${err instanceof Error ? err.message : String(err)}`);
534
- process.exit(1);
535
- }
536
- }
537
-
538
- if (foreground) {
539
- if (sandboxName) {
540
- syncDaemonConfigToSandbox(sandboxName);
541
- const remoteDaemonEntry = path.posix.join(runtimeRemoteRoot, "dist/daemon/index.js");
542
- const { configPath, host } = buildOpenShellSshConfig(sandboxName);
543
- const secretExports = buildOpenShellSecretExports(true);
544
- const result = spawnSync("ssh", [
545
- "-F", configPath,
546
- host,
547
- [
548
- `mkdir -p ${REMOTE_ARC402_DIR}`,
549
- secretExports,
550
- `HOME=/sandbox ARC402_DAEMON_PROCESS=1 ARC402_DAEMON_FOREGROUND=1 node ${remoteDaemonEntry} --foreground`,
551
- ].filter(Boolean).join(" && "),
552
- ], {
553
- stdio: "inherit",
554
- env: {
555
- ...process.env,
556
- ARC402_DAEMON_PROCESS: "1",
557
- ARC402_DAEMON_FOREGROUND: "1",
558
- },
559
- });
560
- process.exit(result.status ?? 0);
561
- } else {
562
- // Foreground mode without sandbox: import and run directly (blocking)
563
- const { runDaemon } = await import("../daemon/index.js");
564
- await runDaemon(true);
565
- }
566
- return;
567
- }
568
-
569
- // Check if already running
570
- const existingPid = readPid();
571
- if (existingPid && isProcessAlive(existingPid)) {
572
- console.log(`Daemon is already running. PID: ${existingPid}`);
573
- process.exit(0);
574
- }
575
-
576
- // Remove stale PID file if present
577
- if (fs.existsSync(DAEMON_PID)) fs.unlinkSync(DAEMON_PID);
578
-
579
- if (sandboxName) {
580
- console.log(`Starting ARC-402 daemon inside OpenShell sandbox: ${sandboxName}`);
581
- console.log(`Using provisioned runtime: ${runtimeRemoteRoot}`);
582
- }
583
- await startDaemonBackground(sandboxName, runtimeRemoteRoot);
584
- });
585
-
586
- // ── daemon stop ─────────────────────────────────────────────────────────────
587
- daemon
588
- .command("stop")
589
- .description("Gracefully stop the running daemon (SIGTERM + wait for exit).")
590
- .action(async () => {
591
- const openShellCfg = readOpenShellConfig();
592
- if (openShellCfg?.sandbox.name) {
593
- const remotePid = await readRemotePid(openShellCfg.sandbox.name);
594
- if (!remotePid) {
595
- console.log("Daemon is not running.");
596
- process.exit(0);
597
- }
598
- const { configPath, host } = buildOpenShellSshConfig(openShellCfg.sandbox.name);
599
- const stopSpinnerRemote = startSpinner(`Stopping daemon (OpenShell PID ${remotePid})...`);
600
- runCmd("ssh", ["-F", configPath, host, `kill ${remotePid}`], { timeout: 20000 });
601
- stopSpinnerRemote.succeed("Stop signal sent");
602
- return;
603
- }
604
-
605
- const pid = readPid();
606
- if (!pid || !isProcessAlive(pid)) {
607
- console.log("Daemon is not running.");
608
- process.exit(0);
609
- }
610
-
611
- const stopSpinner = startSpinner(`Stopping daemon (PID ${pid})...`);
612
- const stopped = await stopDaemon({ wait: true });
613
- if (stopped) {
614
- stopSpinner.succeed("Daemon stopped");
615
- } else {
616
- stopSpinner.fail("Failed to stop daemon cleanly");
617
- console.error("Failed to stop daemon cleanly.");
618
- process.exit(1);
619
- }
620
- });
621
-
622
- // ── daemon restart ──────────────────────────────────────────────────────────
623
- daemon
624
- .command("restart")
625
- .description("Stop the running daemon then start a new one.")
626
- .action(async () => {
627
- const pid = readPid();
628
- if (pid && isProcessAlive(pid)) {
629
- console.log(`Stopping daemon (PID ${pid})...`);
630
- const stopped = await stopDaemon({ wait: true });
631
- if (!stopped) {
632
- console.error("Failed to stop daemon cleanly.");
633
- process.exit(1);
634
- }
635
- console.log("Daemon stopped.");
636
- } else {
637
- console.log("Daemon was not running.");
638
- if (fs.existsSync(DAEMON_PID)) fs.unlinkSync(DAEMON_PID);
639
- }
640
-
641
- const openShellCfg = readOpenShellConfig();
642
- let runtimeRemoteRoot = openShellCfg?.runtime?.remote_root ?? DEFAULT_RUNTIME_REMOTE_ROOT;
643
- if (openShellCfg?.sandbox.name) {
644
- const provisioned = provisionRuntimeToSandbox(openShellCfg.sandbox.name, runtimeRemoteRoot);
645
- runtimeRemoteRoot = provisioned.remoteRoot;
646
- writeOpenShellConfig({
647
- sandbox: openShellCfg.sandbox,
648
- runtime: {
649
- local_tarball: provisioned.tarballPath,
650
- remote_root: provisioned.remoteRoot,
651
- synced_at: new Date().toISOString(),
652
- },
653
- });
654
- }
655
- await startDaemonBackground(openShellCfg?.sandbox.name, runtimeRemoteRoot);
656
- });
657
-
658
- // ── daemon status ───────────────────────────────────────────────────────────
659
- daemon
660
- .command("status")
661
- .description("Show current daemon status via IPC.")
662
- .action(async () => {
663
- const openShellCfg = readOpenShellConfig();
664
- if (openShellCfg?.sandbox.name) {
665
- const remotePid = await readRemotePid(openShellCfg.sandbox.name);
666
- if (!remotePid) {
667
- console.log("Daemon is not running.");
668
- console.log("Launch path: arc402 openshell init, then arc402 daemon start");
669
- process.exit(1);
670
- }
671
- console.log(`${c.mark} ARC-402 Daemon Status`);
672
- console.log();
673
- renderTree([
674
- { label: "State", value: "running (OpenShell sandbox)" },
675
- { label: "PID", value: String(remotePid) },
676
- { label: "Sandbox", value: openShellCfg.sandbox.name },
677
- { label: "Runtime", value: openShellCfg.runtime?.remote_root ?? DEFAULT_RUNTIME_REMOTE_ROOT },
678
- { label: "Log", value: REMOTE_DAEMON_LOG, last: true },
679
- ]);
680
- return;
681
- }
682
-
683
- // First check if daemon is even running at the PID level
684
- const pid = readPid();
685
- if (!pid || !isProcessAlive(pid)) {
686
- console.log("Daemon is not running.");
687
- console.log("Launch path: arc402 openshell init, then arc402 daemon start");
688
- process.exit(1);
689
- }
690
-
691
- const res = await sendIpcCommand({ command: "status" });
692
- if (!res.ok) {
693
- console.error(`Error: ${res.error}`);
694
- process.exit(1);
695
- }
696
- formatStatus(res.data as Record<string, unknown>);
697
- });
698
-
699
- // ── daemon logs ─────────────────────────────────────────────────────────────
700
- daemon
701
- .command("logs")
702
- .description("Show daemon log output.")
703
- .option("--follow", "Stream live log output (tail -f)")
704
- .option("--lines <n>", "Number of lines to show", "50")
705
- .action((opts) => {
706
- const follow = opts.follow as boolean | undefined;
707
- const lines = parseInt(opts.lines as string, 10) || 50;
708
-
709
- const openShellCfg = readOpenShellConfig();
710
- if (openShellCfg?.sandbox.name) {
711
- const { configPath, host } = buildOpenShellSshConfig(openShellCfg.sandbox.name);
712
- const baseCmd = follow
713
- ? `test -f ${REMOTE_DAEMON_LOG} && tail -f -n ${lines} ${REMOTE_DAEMON_LOG}`
714
- : `test -f ${REMOTE_DAEMON_LOG} && tail -n ${lines} ${REMOTE_DAEMON_LOG}`;
715
- const result = spawn("ssh", ["-F", configPath, host, baseCmd], { stdio: "inherit" });
716
- result.on("error", (err) => {
717
- console.error(`Failed to read remote log: ${err.message}`);
718
- process.exit(1);
719
- });
720
- if (follow) {
721
- process.on("SIGINT", () => {
722
- result.kill();
723
- process.exit(0);
724
- });
725
- }
726
- return;
727
- }
728
-
729
- if (!fs.existsSync(DAEMON_LOG)) {
730
- console.log(`Log file not found: ${DAEMON_LOG}`);
731
- console.log("Has the OpenShell-owned runtime been started? Run `arc402 openshell init` first if needed, then `arc402 daemon start`.");
732
- process.exit(0);
733
- }
734
-
735
- if (follow) {
736
- // Stream with tail -f equivalent using spawn
737
- const tail = spawn("tail", ["-f", "-n", String(lines), DAEMON_LOG], {
738
- stdio: "inherit",
739
- });
740
- tail.on("error", (err) => {
741
- console.error(`Failed to tail log: ${err.message}`);
742
- process.exit(1);
743
- });
744
- process.on("SIGINT", () => {
745
- tail.kill();
746
- process.exit(0);
747
- });
748
- } else {
749
- // Read last N lines
750
- const content = fs.readFileSync(DAEMON_LOG, "utf-8");
751
- const allLines = content.split("\n").filter((l) => l.trim());
752
- const slice = allLines.slice(-lines);
753
- for (const line of slice) {
754
- // Try pretty-print JSON log entries
755
- try {
756
- const entry = JSON.parse(line) as Record<string, unknown>;
757
- const ts = entry.ts ? `[${entry.ts}] ` : "";
758
- const { ts: _ts, ...rest } = entry;
759
- console.log(`${ts}${JSON.stringify(rest)}`);
760
- } catch {
761
- console.log(line);
762
- }
763
- }
764
- }
765
- });
766
-
767
- // ── daemon approve <id> ─────────────────────────────────────────────────────
768
- daemon
769
- .command("approve <id>")
770
- .description("Approve a pending hire request.")
771
- .action(async (id: string) => {
772
- const res = await sendIpcCommand({ command: "approve", id });
773
- if (!res.ok) {
774
- console.error(`Error: ${res.error}`);
775
- process.exit(1);
776
- }
777
- const data = res.data as { approved: boolean; id: string };
778
- console.log(`Approved hire request: ${data.id}`);
779
- });
780
-
781
- // ── daemon reject <id> ──────────────────────────────────────────────────────
782
- daemon
783
- .command("reject <id>")
784
- .description("Reject a pending hire request.")
785
- .option("--reason <reason>", "Rejection reason", "operator_rejected")
786
- .action(async (id: string, opts) => {
787
- const reason = opts.reason as string;
788
- const res = await sendIpcCommand({ command: "reject", id, reason });
789
- if (!res.ok) {
790
- console.error(`Error: ${res.error}`);
791
- process.exit(1);
792
- }
793
- const data = res.data as { rejected: boolean; id: string; reason: string };
794
- console.log(`Rejected hire request: ${data.id} (reason: ${data.reason})`);
795
- });
796
-
797
- // ── daemon pending ──────────────────────────────────────────────────────────
798
- daemon
799
- .command("pending")
800
- .description("List all hire requests awaiting operator approval.")
801
- .action(async () => {
802
- const res = await sendIpcCommand({ command: "pending" });
803
- if (!res.ok) {
804
- console.error(`Error: ${res.error}`);
805
- process.exit(1);
806
- }
807
- const data = res.data as { requests: HireRow[] };
808
- const rows = data.requests ?? [];
809
- if (rows.length === 0) {
810
- console.log("No pending hire requests.");
811
- return;
812
- }
813
- console.log(`Pending Hire Requests (${rows.length}):`);
814
- console.log();
815
- formatHireTable(rows);
816
- console.log();
817
- console.log("Approve: arc402 daemon approve <id>");
818
- console.log("Reject: arc402 daemon reject <id> [--reason <reason>]");
819
- });
820
-
821
- // ── daemon agreements ────────────────────────────────────────────────────────
822
- daemon
823
- .command("agreements")
824
- .description("List all active agreements and their status.")
825
- .action(async () => {
826
- const res = await sendIpcCommand({ command: "agreements" });
827
- if (!res.ok) {
828
- console.error(`Error: ${res.error}`);
829
- process.exit(1);
830
- }
831
- const data = res.data as { agreements: HireRow[] };
832
- const rows = data.agreements ?? [];
833
- if (rows.length === 0) {
834
- console.log("No active agreements.");
835
- return;
836
- }
837
- console.log(`Active Agreements (${rows.length}):`);
838
- console.log();
839
- formatHireTable(rows);
840
- });
841
-
842
- daemon
843
- .command("agreement <id>")
844
- .description("Show full detail on a specific agreement.")
845
- .action(async (id) => {
846
- const res = await sendIpcCommand({ command: "agreement", id: String(id) });
847
- if (!res.ok) {
848
- console.error(`Error: ${res.error}`);
849
- process.exit(1);
850
- }
851
- const data = res.data as { agreement: HireRow };
852
- console.log(JSON.stringify(data.agreement, null, 2));
853
- });
854
-
855
- // ── daemon init ──────────────────────────────────────────────────────────────
856
- daemon
857
- .command("init")
858
- .description("Generate a template ~/.arc402/daemon.toml configuration file.")
859
- .option("--force", "Overwrite existing daemon.toml")
860
- .option("--reconfigure-harness", "Re-run harness selection on an existing daemon.toml")
861
- .action(async (opts) => {
862
- const force = opts.force as boolean | undefined;
863
- const reconfigureHarness = opts.reconfigureHarness as boolean | undefined;
864
-
865
- if (fs.existsSync(DAEMON_TOML) && !force && !reconfigureHarness) {
866
- console.log(`daemon.toml already exists at ${DAEMON_TOML}`);
867
- console.log("Use --force to overwrite, or --reconfigure-harness to update the harness only.");
868
- process.exit(0);
869
- }
870
-
871
- // ── Harness selection ────────────────────────────────────────────────────
872
- console.log("Which harness should execute work tasks?");
873
- console.log();
874
- console.log(" 1. openclaw (OpenClaw agent runtime — default)");
875
- console.log(" 2. claude (Claude Code — Anthropic)");
876
- console.log(" 3. codex (Codex CLI — OpenAI)");
877
- console.log(" 4. opencode (OpenCode)");
878
- console.log(" 5. custom (enter your own exec_command)");
879
- console.log();
880
-
881
- const harnessResponse = await prompts({
882
- type: "select",
883
- name: "harness",
884
- message: "Select harness",
885
- choices: [
886
- { title: "openclaw", value: "openclaw" },
887
- { title: "claude", value: "claude" },
888
- { title: "codex", value: "codex" },
889
- { title: "opencode", value: "opencode" },
890
- { title: "custom", value: "custom" },
891
- ],
892
- initial: 0,
893
- });
894
-
895
- const selectedHarness: string = harnessResponse.harness ?? "openclaw";
896
- let execCommand = HARNESS_REGISTRY[selectedHarness] ?? "";
897
-
898
- if (selectedHarness === "custom") {
899
- const customResponse = await prompts({
900
- type: "text",
901
- name: "exec_command",
902
- message: "Enter your exec_command (use {task} as placeholder)",
903
- validate: (v: string) => v.trim().length > 0 || "exec_command cannot be empty",
904
- });
905
- execCommand = (customResponse.exec_command as string | undefined) ?? "";
906
- }
907
-
908
- const workSection = [
909
- "[work]",
910
- `handler = \"exec\" # exec | http | noop`,
911
- `exec_command = \"${execCommand}\"${selectedHarness === "custom" ? " # Your custom command" : ""}`,
912
- `http_url = \"\" # POST {agreementId, specHash, deadline} as JSON (http mode)`,
913
- `http_auth_token = \"env:WORKER_AUTH_TOKEN\"`,
914
- `harness = \"${selectedHarness}\" # launch metadata only — selected harness label`,
915
- ...(selectedHarness === "custom"
916
- ? []
917
- : [`# To change harness later: arc402 daemon init --reconfigure-harness`]),
918
- "",
919
- ].join("\n");
920
-
921
- if (reconfigureHarness && fs.existsSync(DAEMON_TOML)) {
922
- let existing = fs.readFileSync(DAEMON_TOML, "utf-8");
923
- if (!/^\[work\]/m.test(existing)) {
924
- console.error("[work] section not found in daemon.toml. Run: arc402 daemon init --force");
925
- process.exit(1);
926
- }
927
- existing = existing.replace(/\[work\][\s\S]*$/, workSection);
928
- fs.writeFileSync(DAEMON_TOML, existing, { mode: 0o600 });
929
-
930
- console.log(`\nHarness updated: ${selectedHarness}`);
931
- console.log(`exec_command: ${execCommand}`);
932
- return;
933
- }
934
-
935
- // ── Write full daemon.toml ────────────────────────────────────────────────
936
- const toml = TEMPLATE_DAEMON_TOML.replace(/\[work\][\s\S]*$/, workSection);
937
-
938
- fs.mkdirSync(DAEMON_DIR, { recursive: true, mode: 0o700 });
939
- fs.writeFileSync(DAEMON_TOML, toml, { mode: 0o600 });
940
- console.log(`\nCreated ${DAEMON_TOML}`);
941
- console.log(`Harness: ${selectedHarness}${selectedHarness !== "custom" ? ` (${execCommand})` : ""}`);
942
- console.log();
943
- console.log("Next steps:");
944
- console.log(" 1. Edit daemon.toml — fill in wallet.contract_address and network.rpc_url");
945
- console.log(" 2. Confirm the CLI config or env exposes your machine key and notifications");
946
- console.log(" 3. Run arc402 openshell init — it will create/update providers and sync the runtime bundle automatically");
947
- console.log(" 4. Verify with arc402 openshell status");
948
- console.log(" 5. Start the OpenShell-owned ARC-402 runtime: arc402 daemon start");
949
- });
950
-
951
- // ── daemon notifications ──────────────────────────────────────────────────────
952
- const notifications = daemon
953
- .command("notifications")
954
- .description("Show or test configured notification channels");
955
-
956
- notifications
957
- .command("show")
958
- .description("Show all configured notification channels")
959
- .action(() => {
960
- if (!fs.existsSync(DAEMON_TOML)) {
961
- console.error("daemon.toml not found. Run `arc402 daemon init` first.");
962
- process.exit(1);
963
- }
964
- const cfg = loadDaemonConfig();
965
- const notif = cfg.notifications;
966
- const channels: string[] = [];
967
-
968
- if (notif.telegram_bot_token && notif.telegram_chat_id) {
969
- channels.push(`telegram chat_id=${notif.telegram_chat_id}`);
970
- }
971
- if (notif.discord?.webhook_url) {
972
- const u = new URL(notif.discord.webhook_url);
973
- channels.push(`discord ${u.hostname}${u.pathname.slice(0, 30)}...`);
974
- }
975
- if (notif.webhook?.url) {
976
- channels.push(`webhook ${notif.webhook.url}`);
977
- }
978
- if (notif.email?.smtp_host && notif.email?.to) {
979
- channels.push(`email ${notif.email.smtp_host}:${notif.email.smtp_port} → ${notif.email.to}`);
980
- }
981
-
982
- if (channels.length === 0) {
983
- console.log("No notification channels configured.");
984
- console.log("Edit ~/.arc402/daemon.toml to add Telegram, Discord, webhook, or email.");
985
- } else {
986
- console.log(`Configured channels (${channels.length}):`);
987
- for (const ch of channels) console.log(` ${ch}`);
988
- }
989
- });
990
-
991
- notifications
992
- .command("test")
993
- .description("Send a test message to all configured channels")
994
- .action(async () => {
995
- if (!fs.existsSync(DAEMON_TOML)) {
996
- console.error("daemon.toml not found. Run `arc402 daemon init` first.");
997
- process.exit(1);
998
- }
999
- const cfg = loadDaemonConfig();
1000
- const notifier = buildNotifier(cfg);
1001
- if (!notifier.isEnabled()) {
1002
- console.log("No notification channels configured. Nothing to test.");
1003
- process.exit(0);
1004
- }
1005
- console.log("Sending test notification to all channels...");
1006
- try {
1007
- await notifier.send("daemon_started", "ARC-402 Test Notification",
1008
- "This is a test message from arc402 daemon notifications test."
1009
- );
1010
- console.log("Test notification sent successfully.");
1011
- } catch (err) {
1012
- console.error(`Test notification failed: ${err instanceof Error ? err.message : String(err)}`);
1013
- process.exit(1);
1014
- }
1015
- });
1016
-
1017
- // Default action: show (arc402 daemon notifications → arc402 daemon notifications show)
1018
- notifications.action(() => {
1019
- notifications.help();
1020
- });
1021
-
1022
- // ── daemon channel-watch ─────────────────────────────────────────────────────
1023
- daemon
1024
- .command("channel-watch")
1025
- .description(
1026
- "Monitor all open channels for the configured wallet. " +
1027
- "Polls the chain on an interval and auto-challenges any stale close " +
1028
- "using the latest signed state from ~/.arc402/channel-states/. " +
1029
- "Runs until interrupted (Ctrl+C)."
1030
- )
1031
- .option("--poll-interval <ms>", "Polling interval in milliseconds", "30000")
1032
- .option("--json", "Machine-parseable output (one JSON object per line)")
1033
- .action(async (opts) => {
1034
- const config = loadConfig();
1035
- if (!config.serviceAgreementAddress) {
1036
- console.error("serviceAgreementAddress missing in config. Run `arc402 config init`.");
1037
- process.exit(1);
1038
- }
1039
-
1040
- const { signer, address } = await requireSigner(config);
1041
- const contract = new ethers.Contract(
1042
- config.serviceAgreementAddress,
1043
- SERVICE_AGREEMENT_ABI,
1044
- signer
1045
- );
1046
-
1047
- await runChannelWatchLoop({
1048
- pollInterval: parseInt(opts.pollInterval, 10),
1049
- wallet: address,
1050
- contract,
1051
- json: opts.json || program.opts().json,
1052
- });
1053
- });
1054
- }