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