arc402-cli 0.2.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 (308) hide show
  1. package/README.md +245 -0
  2. package/dist/abis.d.ts +19 -0
  3. package/dist/abis.d.ts.map +1 -0
  4. package/dist/abis.js +177 -0
  5. package/dist/abis.js.map +1 -0
  6. package/dist/bundler.d.ts +65 -0
  7. package/dist/bundler.d.ts.map +1 -0
  8. package/dist/bundler.js +181 -0
  9. package/dist/bundler.js.map +1 -0
  10. package/dist/client.d.ts +14 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +24 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coinbase-smart-wallet.d.ts +28 -0
  15. package/dist/coinbase-smart-wallet.d.ts.map +1 -0
  16. package/dist/coinbase-smart-wallet.js +38 -0
  17. package/dist/coinbase-smart-wallet.js.map +1 -0
  18. package/dist/commands/accept.d.ts +3 -0
  19. package/dist/commands/accept.d.ts.map +1 -0
  20. package/dist/commands/accept.js +26 -0
  21. package/dist/commands/accept.js.map +1 -0
  22. package/dist/commands/agent-handshake.d.ts +3 -0
  23. package/dist/commands/agent-handshake.d.ts.map +1 -0
  24. package/dist/commands/agent-handshake.js +61 -0
  25. package/dist/commands/agent-handshake.js.map +1 -0
  26. package/dist/commands/agent.d.ts +3 -0
  27. package/dist/commands/agent.d.ts.map +1 -0
  28. package/dist/commands/agent.js +417 -0
  29. package/dist/commands/agent.js.map +1 -0
  30. package/dist/commands/agreements.d.ts +3 -0
  31. package/dist/commands/agreements.d.ts.map +1 -0
  32. package/dist/commands/agreements.js +344 -0
  33. package/dist/commands/agreements.js.map +1 -0
  34. package/dist/commands/arbitrator.d.ts +3 -0
  35. package/dist/commands/arbitrator.d.ts.map +1 -0
  36. package/dist/commands/arbitrator.js +157 -0
  37. package/dist/commands/arbitrator.js.map +1 -0
  38. package/dist/commands/arena-handshake.d.ts +3 -0
  39. package/dist/commands/arena-handshake.d.ts.map +1 -0
  40. package/dist/commands/arena-handshake.js +187 -0
  41. package/dist/commands/arena-handshake.js.map +1 -0
  42. package/dist/commands/cancel.d.ts +3 -0
  43. package/dist/commands/cancel.d.ts.map +1 -0
  44. package/dist/commands/cancel.js +30 -0
  45. package/dist/commands/cancel.js.map +1 -0
  46. package/dist/commands/channel.d.ts +3 -0
  47. package/dist/commands/channel.d.ts.map +1 -0
  48. package/dist/commands/channel.js +238 -0
  49. package/dist/commands/channel.js.map +1 -0
  50. package/dist/commands/coldstart.d.ts +3 -0
  51. package/dist/commands/coldstart.d.ts.map +1 -0
  52. package/dist/commands/coldstart.js +148 -0
  53. package/dist/commands/coldstart.js.map +1 -0
  54. package/dist/commands/config.d.ts +3 -0
  55. package/dist/commands/config.d.ts.map +1 -0
  56. package/dist/commands/config.js +40 -0
  57. package/dist/commands/config.js.map +1 -0
  58. package/dist/commands/contract-interaction.d.ts +3 -0
  59. package/dist/commands/contract-interaction.d.ts.map +1 -0
  60. package/dist/commands/contract-interaction.js +165 -0
  61. package/dist/commands/contract-interaction.js.map +1 -0
  62. package/dist/commands/daemon.d.ts +3 -0
  63. package/dist/commands/daemon.d.ts.map +1 -0
  64. package/dist/commands/daemon.js +891 -0
  65. package/dist/commands/daemon.js.map +1 -0
  66. package/dist/commands/deliver.d.ts +3 -0
  67. package/dist/commands/deliver.d.ts.map +1 -0
  68. package/dist/commands/deliver.js +156 -0
  69. package/dist/commands/deliver.js.map +1 -0
  70. package/dist/commands/discover.d.ts +3 -0
  71. package/dist/commands/discover.d.ts.map +1 -0
  72. package/dist/commands/discover.js +224 -0
  73. package/dist/commands/discover.js.map +1 -0
  74. package/dist/commands/dispute.d.ts +3 -0
  75. package/dist/commands/dispute.d.ts.map +1 -0
  76. package/dist/commands/dispute.js +348 -0
  77. package/dist/commands/dispute.js.map +1 -0
  78. package/dist/commands/endpoint.d.ts +3 -0
  79. package/dist/commands/endpoint.d.ts.map +1 -0
  80. package/dist/commands/endpoint.js +604 -0
  81. package/dist/commands/endpoint.js.map +1 -0
  82. package/dist/commands/hire.d.ts +3 -0
  83. package/dist/commands/hire.d.ts.map +1 -0
  84. package/dist/commands/hire.js +189 -0
  85. package/dist/commands/hire.js.map +1 -0
  86. package/dist/commands/migrate.d.ts +3 -0
  87. package/dist/commands/migrate.d.ts.map +1 -0
  88. package/dist/commands/migrate.js +163 -0
  89. package/dist/commands/migrate.js.map +1 -0
  90. package/dist/commands/negotiate.d.ts +3 -0
  91. package/dist/commands/negotiate.d.ts.map +1 -0
  92. package/dist/commands/negotiate.js +247 -0
  93. package/dist/commands/negotiate.js.map +1 -0
  94. package/dist/commands/openshell.d.ts +3 -0
  95. package/dist/commands/openshell.d.ts.map +1 -0
  96. package/dist/commands/openshell.js +952 -0
  97. package/dist/commands/openshell.js.map +1 -0
  98. package/dist/commands/owner.d.ts +3 -0
  99. package/dist/commands/owner.d.ts.map +1 -0
  100. package/dist/commands/owner.js +32 -0
  101. package/dist/commands/owner.js.map +1 -0
  102. package/dist/commands/policy.d.ts +4 -0
  103. package/dist/commands/policy.d.ts.map +1 -0
  104. package/dist/commands/policy.js +248 -0
  105. package/dist/commands/policy.js.map +1 -0
  106. package/dist/commands/relay.d.ts +3 -0
  107. package/dist/commands/relay.d.ts.map +1 -0
  108. package/dist/commands/relay.js +279 -0
  109. package/dist/commands/relay.js.map +1 -0
  110. package/dist/commands/remediate.d.ts +3 -0
  111. package/dist/commands/remediate.d.ts.map +1 -0
  112. package/dist/commands/remediate.js +42 -0
  113. package/dist/commands/remediate.js.map +1 -0
  114. package/dist/commands/reputation.d.ts +4 -0
  115. package/dist/commands/reputation.d.ts.map +1 -0
  116. package/dist/commands/reputation.js +72 -0
  117. package/dist/commands/reputation.js.map +1 -0
  118. package/dist/commands/setup.d.ts +3 -0
  119. package/dist/commands/setup.d.ts.map +1 -0
  120. package/dist/commands/setup.js +332 -0
  121. package/dist/commands/setup.js.map +1 -0
  122. package/dist/commands/trust.d.ts +3 -0
  123. package/dist/commands/trust.d.ts.map +1 -0
  124. package/dist/commands/trust.js +23 -0
  125. package/dist/commands/trust.js.map +1 -0
  126. package/dist/commands/verify.d.ts +3 -0
  127. package/dist/commands/verify.d.ts.map +1 -0
  128. package/dist/commands/verify.js +88 -0
  129. package/dist/commands/verify.js.map +1 -0
  130. package/dist/commands/wallet.d.ts +3 -0
  131. package/dist/commands/wallet.d.ts.map +1 -0
  132. package/dist/commands/wallet.js +2520 -0
  133. package/dist/commands/wallet.js.map +1 -0
  134. package/dist/commands/watchtower.d.ts +3 -0
  135. package/dist/commands/watchtower.d.ts.map +1 -0
  136. package/dist/commands/watchtower.js +238 -0
  137. package/dist/commands/watchtower.js.map +1 -0
  138. package/dist/commands/workroom.d.ts +3 -0
  139. package/dist/commands/workroom.d.ts.map +1 -0
  140. package/dist/commands/workroom.js +855 -0
  141. package/dist/commands/workroom.js.map +1 -0
  142. package/dist/config.d.ts +62 -0
  143. package/dist/config.d.ts.map +1 -0
  144. package/dist/config.js +141 -0
  145. package/dist/config.js.map +1 -0
  146. package/dist/daemon/config.d.ts +74 -0
  147. package/dist/daemon/config.d.ts.map +1 -0
  148. package/dist/daemon/config.js +271 -0
  149. package/dist/daemon/config.js.map +1 -0
  150. package/dist/daemon/hire-listener.d.ts +31 -0
  151. package/dist/daemon/hire-listener.d.ts.map +1 -0
  152. package/dist/daemon/hire-listener.js +207 -0
  153. package/dist/daemon/hire-listener.js.map +1 -0
  154. package/dist/daemon/index.d.ts +29 -0
  155. package/dist/daemon/index.d.ts.map +1 -0
  156. package/dist/daemon/index.js +535 -0
  157. package/dist/daemon/index.js.map +1 -0
  158. package/dist/daemon/job-lifecycle.d.ts +62 -0
  159. package/dist/daemon/job-lifecycle.d.ts.map +1 -0
  160. package/dist/daemon/job-lifecycle.js +201 -0
  161. package/dist/daemon/job-lifecycle.js.map +1 -0
  162. package/dist/daemon/notify.d.ts +22 -0
  163. package/dist/daemon/notify.d.ts.map +1 -0
  164. package/dist/daemon/notify.js +148 -0
  165. package/dist/daemon/notify.js.map +1 -0
  166. package/dist/daemon/token-metering.d.ts +42 -0
  167. package/dist/daemon/token-metering.d.ts.map +1 -0
  168. package/dist/daemon/token-metering.js +178 -0
  169. package/dist/daemon/token-metering.js.map +1 -0
  170. package/dist/daemon/userops.d.ts +21 -0
  171. package/dist/daemon/userops.d.ts.map +1 -0
  172. package/dist/daemon/userops.js +88 -0
  173. package/dist/daemon/userops.js.map +1 -0
  174. package/dist/daemon/wallet-monitor.d.ts +16 -0
  175. package/dist/daemon/wallet-monitor.d.ts.map +1 -0
  176. package/dist/daemon/wallet-monitor.js +57 -0
  177. package/dist/daemon/wallet-monitor.js.map +1 -0
  178. package/dist/drain-v4.d.ts +2 -0
  179. package/dist/drain-v4.d.ts.map +1 -0
  180. package/dist/drain-v4.js +167 -0
  181. package/dist/drain-v4.js.map +1 -0
  182. package/dist/endpoint-config.d.ts +36 -0
  183. package/dist/endpoint-config.d.ts.map +1 -0
  184. package/dist/endpoint-config.js +96 -0
  185. package/dist/endpoint-config.js.map +1 -0
  186. package/dist/index.d.ts +3 -0
  187. package/dist/index.d.ts.map +1 -0
  188. package/dist/index.js +79 -0
  189. package/dist/index.js.map +1 -0
  190. package/dist/openshell-runtime.d.ts +55 -0
  191. package/dist/openshell-runtime.d.ts.map +1 -0
  192. package/dist/openshell-runtime.js +268 -0
  193. package/dist/openshell-runtime.js.map +1 -0
  194. package/dist/signing.d.ts +2 -0
  195. package/dist/signing.d.ts.map +1 -0
  196. package/dist/signing.js +23 -0
  197. package/dist/signing.js.map +1 -0
  198. package/dist/telegram-notify.d.ts +23 -0
  199. package/dist/telegram-notify.d.ts.map +1 -0
  200. package/dist/telegram-notify.js +106 -0
  201. package/dist/telegram-notify.js.map +1 -0
  202. package/dist/ui/banner.d.ts +7 -0
  203. package/dist/ui/banner.d.ts.map +1 -0
  204. package/dist/ui/banner.js +37 -0
  205. package/dist/ui/banner.js.map +1 -0
  206. package/dist/ui/colors.d.ts +14 -0
  207. package/dist/ui/colors.d.ts.map +1 -0
  208. package/dist/ui/colors.js +29 -0
  209. package/dist/ui/colors.js.map +1 -0
  210. package/dist/ui/format.d.ts +26 -0
  211. package/dist/ui/format.d.ts.map +1 -0
  212. package/dist/ui/format.js +77 -0
  213. package/dist/ui/format.js.map +1 -0
  214. package/dist/ui/spinner.d.ts +8 -0
  215. package/dist/ui/spinner.d.ts.map +1 -0
  216. package/dist/ui/spinner.js +43 -0
  217. package/dist/ui/spinner.js.map +1 -0
  218. package/dist/utils/format.d.ts +10 -0
  219. package/dist/utils/format.d.ts.map +1 -0
  220. package/dist/utils/format.js +61 -0
  221. package/dist/utils/format.js.map +1 -0
  222. package/dist/utils/hash.d.ts +3 -0
  223. package/dist/utils/hash.d.ts.map +1 -0
  224. package/dist/utils/hash.js +43 -0
  225. package/dist/utils/hash.js.map +1 -0
  226. package/dist/utils/time.d.ts +3 -0
  227. package/dist/utils/time.d.ts.map +1 -0
  228. package/dist/utils/time.js +21 -0
  229. package/dist/utils/time.js.map +1 -0
  230. package/dist/wallet-router.d.ts +25 -0
  231. package/dist/wallet-router.d.ts.map +1 -0
  232. package/dist/wallet-router.js +153 -0
  233. package/dist/wallet-router.js.map +1 -0
  234. package/dist/walletconnect-session.d.ts +12 -0
  235. package/dist/walletconnect-session.d.ts.map +1 -0
  236. package/dist/walletconnect-session.js +26 -0
  237. package/dist/walletconnect-session.js.map +1 -0
  238. package/dist/walletconnect.d.ts +46 -0
  239. package/dist/walletconnect.d.ts.map +1 -0
  240. package/dist/walletconnect.js +267 -0
  241. package/dist/walletconnect.js.map +1 -0
  242. package/package.json +38 -0
  243. package/scripts/authorize-machine-key.ts +43 -0
  244. package/scripts/drain-wallet.ts +149 -0
  245. package/scripts/execute-spend-only.ts +81 -0
  246. package/scripts/register-agent-userop.ts +186 -0
  247. package/src/abis.ts +187 -0
  248. package/src/bundler.ts +235 -0
  249. package/src/client.ts +34 -0
  250. package/src/coinbase-smart-wallet.ts +51 -0
  251. package/src/commands/accept.ts +25 -0
  252. package/src/commands/agent-handshake.ts +67 -0
  253. package/src/commands/agent.ts +458 -0
  254. package/src/commands/agreements.ts +324 -0
  255. package/src/commands/arbitrator.ts +129 -0
  256. package/src/commands/arena-handshake.ts +217 -0
  257. package/src/commands/cancel.ts +26 -0
  258. package/src/commands/channel.ts +208 -0
  259. package/src/commands/coldstart.ts +156 -0
  260. package/src/commands/config.ts +35 -0
  261. package/src/commands/contract-interaction.ts +166 -0
  262. package/src/commands/daemon.ts +971 -0
  263. package/src/commands/deliver.ts +116 -0
  264. package/src/commands/discover.ts +295 -0
  265. package/src/commands/dispute.ts +373 -0
  266. package/src/commands/endpoint.ts +619 -0
  267. package/src/commands/hire.ts +200 -0
  268. package/src/commands/migrate.ts +175 -0
  269. package/src/commands/negotiate.ts +270 -0
  270. package/src/commands/openshell.ts +1053 -0
  271. package/src/commands/owner.ts +30 -0
  272. package/src/commands/policy.ts +252 -0
  273. package/src/commands/relay.ts +272 -0
  274. package/src/commands/remediate.ts +22 -0
  275. package/src/commands/reputation.ts +71 -0
  276. package/src/commands/setup.ts +343 -0
  277. package/src/commands/trust.ts +15 -0
  278. package/src/commands/verify.ts +88 -0
  279. package/src/commands/wallet.ts +2892 -0
  280. package/src/commands/watchtower.ts +232 -0
  281. package/src/commands/workroom.ts +889 -0
  282. package/src/config.ts +153 -0
  283. package/src/daemon/config.ts +308 -0
  284. package/src/daemon/hire-listener.ts +226 -0
  285. package/src/daemon/index.ts +609 -0
  286. package/src/daemon/job-lifecycle.ts +215 -0
  287. package/src/daemon/notify.ts +157 -0
  288. package/src/daemon/token-metering.ts +183 -0
  289. package/src/daemon/userops.ts +119 -0
  290. package/src/daemon/wallet-monitor.ts +90 -0
  291. package/src/drain-v4.ts +159 -0
  292. package/src/endpoint-config.ts +83 -0
  293. package/src/index.ts +75 -0
  294. package/src/openshell-runtime.ts +277 -0
  295. package/src/signing.ts +28 -0
  296. package/src/telegram-notify.ts +88 -0
  297. package/src/ui/banner.ts +41 -0
  298. package/src/ui/colors.ts +30 -0
  299. package/src/ui/format.ts +77 -0
  300. package/src/ui/spinner.ts +46 -0
  301. package/src/utils/format.ts +48 -0
  302. package/src/utils/hash.ts +5 -0
  303. package/src/utils/time.ts +15 -0
  304. package/src/wallet-router.ts +178 -0
  305. package/src/walletconnect-session.ts +27 -0
  306. package/src/walletconnect.ts +294 -0
  307. package/test/time.test.js +11 -0
  308. package/tsconfig.json +19 -0
@@ -0,0 +1,889 @@
1
+ import { Command } from "commander";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { spawnSync, execSync } from "child_process";
6
+ import {
7
+ ARC402_DIR,
8
+ runCmd,
9
+ } from "../openshell-runtime";
10
+ import { DAEMON_LOG, DAEMON_TOML } from "../daemon/config";
11
+
12
+ // ─── Constants ────────────────────────────────────────────────────────────────
13
+
14
+ const WORKROOM_IMAGE = "arc402-workroom";
15
+ const WORKROOM_CONTAINER = "arc402-workroom";
16
+ const POLICY_FILE = path.join(ARC402_DIR, "openshell-policy.yaml");
17
+ const ARENA_POLICY_FILE = path.join(ARC402_DIR, "arena-policy.yaml");
18
+ const ARENA_DATA_DIR = path.join(ARC402_DIR, "arena");
19
+ const WORKROOM_DIR = path.join(__dirname, "..", "..", "..", "workroom"); // relative to cli/dist
20
+
21
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
22
+
23
+ function dockerAvailable(): boolean {
24
+ const r = runCmd("docker", ["info", "--format", "{{.ServerVersion}}"]);
25
+ return r.ok;
26
+ }
27
+
28
+ function containerExists(): boolean {
29
+ const r = runCmd("docker", ["inspect", WORKROOM_CONTAINER, "--format", "{{.State.Status}}"]);
30
+ return r.ok;
31
+ }
32
+
33
+ function containerRunning(): boolean {
34
+ const r = runCmd("docker", ["inspect", WORKROOM_CONTAINER, "--format", "{{.State.Running}}"]);
35
+ return r.ok && r.stdout.trim() === "true";
36
+ }
37
+
38
+ function imageExists(): boolean {
39
+ const r = runCmd("docker", ["image", "inspect", WORKROOM_IMAGE, "--format", "{{.Id}}"]);
40
+ return r.ok;
41
+ }
42
+
43
+ function buildImage(): boolean {
44
+ // Find the workroom directory (contains Dockerfile)
45
+ const workroomSrc = path.resolve(__dirname, "..", "..", "..", "workroom");
46
+ if (!fs.existsSync(path.join(workroomSrc, "Dockerfile"))) {
47
+ console.error(`Dockerfile not found at ${workroomSrc}/Dockerfile`);
48
+ return false;
49
+ }
50
+ console.log("Building ARC-402 Workroom image...");
51
+ const result = spawnSync("docker", ["build", "-t", WORKROOM_IMAGE, workroomSrc], {
52
+ stdio: "inherit",
53
+ });
54
+ return result.status === 0;
55
+ }
56
+
57
+ function getPolicyHash(): string {
58
+ if (!fs.existsSync(POLICY_FILE)) return "(no policy file)";
59
+ const content = fs.readFileSync(POLICY_FILE, "utf-8");
60
+ const crypto = require("crypto");
61
+ return "0x" + crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
62
+ }
63
+
64
+ // ─── Commands ─────────────────────────────────────────────────────────────────
65
+
66
+ export function registerWorkroomCommands(program: Command): void {
67
+ const workroom = program
68
+ .command("workroom")
69
+ .description("ARC-402 Workroom — governed execution environment for hired work. Your OpenClaw stays on the host; work runs inside the workroom.");
70
+
71
+ // ── init ──────────────────────────────────────────────────────────────────
72
+ workroom
73
+ .command("init")
74
+ .description("Create the ARC-402 Workroom: build Docker image, validate policy, prepare runtime bundle.")
75
+ .action(async () => {
76
+ console.log("ARC-402 Workroom Init");
77
+ console.log("─────────────────────");
78
+
79
+ // Check Docker
80
+ if (!dockerAvailable()) {
81
+ console.error("Docker is not available. Install Docker Desktop and try again.");
82
+ process.exit(1);
83
+ }
84
+ console.log("✓ Docker available");
85
+
86
+ // Check policy file
87
+ if (!fs.existsSync(POLICY_FILE)) {
88
+ console.log("No policy file found. Generating default...");
89
+ // Import and call the existing policy generator
90
+ const { registerOpenShellCommands } = require("./openshell");
91
+ console.log(`Policy file will be generated at: ${POLICY_FILE}`);
92
+ console.log("Run 'arc402 workroom policy preset core-launch' after init to apply defaults.");
93
+ } else {
94
+ console.log(`✓ Policy file: ${POLICY_FILE}`);
95
+ }
96
+
97
+ // Check daemon.toml
98
+ if (!fs.existsSync(DAEMON_TOML)) {
99
+ console.error("daemon.toml not found. Run 'arc402 daemon init' first.");
100
+ process.exit(1);
101
+ }
102
+ console.log("✓ daemon.toml found");
103
+
104
+ // Set up Arena directories and default policy
105
+ if (!fs.existsSync(ARENA_DATA_DIR)) {
106
+ fs.mkdirSync(ARENA_DATA_DIR, { recursive: true });
107
+ for (const sub of ["feed", "profile", "state", "queue"]) {
108
+ fs.mkdirSync(path.join(ARENA_DATA_DIR, sub), { recursive: true });
109
+ }
110
+ console.log("✓ Arena directories created");
111
+ } else {
112
+ console.log("✓ Arena directories exist");
113
+ }
114
+
115
+ // Copy default arena policy if not present
116
+ if (!fs.existsSync(ARENA_POLICY_FILE)) {
117
+ const defaultArenaPolicy = path.join(WORKROOM_DIR, "arena-policy.yaml");
118
+ if (fs.existsSync(defaultArenaPolicy)) {
119
+ fs.copyFileSync(defaultArenaPolicy, ARENA_POLICY_FILE);
120
+ console.log("✓ Arena policy: default installed");
121
+ } else {
122
+ console.log("⚠ Arena policy template not found — create manually at " + ARENA_POLICY_FILE);
123
+ }
124
+ } else {
125
+ console.log("✓ Arena policy exists");
126
+ }
127
+
128
+ // Build image
129
+ if (!imageExists()) {
130
+ if (!buildImage()) {
131
+ console.error("Failed to build workroom image.");
132
+ process.exit(1);
133
+ }
134
+ }
135
+ console.log(`✓ Image: ${WORKROOM_IMAGE}`);
136
+
137
+ // Package CLI runtime for the workroom
138
+ const cliDist = path.resolve(__dirname, "..", "..");
139
+ const cliPackage = path.resolve(__dirname, "..", "..", "..", "package.json");
140
+ if (fs.existsSync(cliDist) && fs.existsSync(cliPackage)) {
141
+ console.log("✓ CLI runtime available for workroom mount");
142
+ } else {
143
+ console.warn("⚠ CLI dist not found — workroom will need runtime bundle");
144
+ }
145
+
146
+ console.log("\nWorkroom initialized. Start with: arc402 workroom start");
147
+ console.log(`Policy hash: ${getPolicyHash()}`);
148
+ });
149
+
150
+ // ── start ─────────────────────────────────────────────────────────────────
151
+ workroom
152
+ .command("start")
153
+ .description("Start the ARC-402 Workroom (always-on governed container with daemon inside).")
154
+ .action(async () => {
155
+ if (!dockerAvailable()) {
156
+ console.error("Docker is not available.");
157
+ process.exit(1);
158
+ }
159
+
160
+ if (containerRunning()) {
161
+ console.log("Workroom is already running.");
162
+ process.exit(0);
163
+ }
164
+
165
+ // Remove stopped container if exists
166
+ if (containerExists()) {
167
+ runCmd("docker", ["rm", "-f", WORKROOM_CONTAINER]);
168
+ }
169
+
170
+ // Build image if needed
171
+ if (!imageExists()) {
172
+ if (!buildImage()) {
173
+ console.error("Failed to build workroom image.");
174
+ process.exit(1);
175
+ }
176
+ }
177
+
178
+ // Resolve secrets from local config
179
+ const machineKey = process.env.ARC402_MACHINE_KEY || "";
180
+ const telegramBot = process.env.TELEGRAM_BOT_TOKEN || "";
181
+ const telegramChat = process.env.TELEGRAM_CHAT_ID || "";
182
+
183
+ if (!machineKey) {
184
+ console.error("ARC402_MACHINE_KEY not set in environment.");
185
+ console.error("Export it before starting: export ARC402_MACHINE_KEY=0x...");
186
+ process.exit(1);
187
+ }
188
+
189
+ // CLI runtime path
190
+ const cliRoot = path.resolve(__dirname, "..", "..", "..");
191
+
192
+ console.log("Starting ARC-402 Workroom...");
193
+
194
+ const args = [
195
+ "run", "-d",
196
+ "--name", WORKROOM_CONTAINER,
197
+ "--restart", "unless-stopped",
198
+ "--cap-add", "NET_ADMIN", // Required for iptables
199
+ // Mount config (read-write for daemon state/logs)
200
+ "-v", `${ARC402_DIR}:/workroom/.arc402:rw`,
201
+ // Mount CLI runtime (read-only)
202
+ "-v", `${cliRoot}:/workroom/runtime:ro`,
203
+ // Mount jobs directory
204
+ "-v", `${path.join(ARC402_DIR, "jobs")}:/workroom/jobs:rw`,
205
+ // Mount worker directory (identity, memory, skills, knowledge)
206
+ "-v", `${path.join(ARC402_DIR, "worker")}:/workroom/worker:rw`,
207
+ // Mount Arena data directory (feed index, profile cache, state, queue)
208
+ "-v", `${ARENA_DATA_DIR}:/workroom/arena:rw`,
209
+ // Inject secrets as env vars
210
+ "-e", `ARC402_MACHINE_KEY=${machineKey}`,
211
+ "-e", `TELEGRAM_BOT_TOKEN=${telegramBot}`,
212
+ "-e", `TELEGRAM_CHAT_ID=${telegramChat}`,
213
+ "-e", `ARC402_DAEMON_PROCESS=1`,
214
+ "-e", `ARC402_DAEMON_FOREGROUND=1`,
215
+ // Expose relay port
216
+ "-p", "4402:4402",
217
+ WORKROOM_IMAGE,
218
+ ];
219
+
220
+ const result = spawnSync("docker", args, { stdio: "inherit" });
221
+ if (result.status !== 0) {
222
+ console.error("Failed to start workroom container.");
223
+ process.exit(1);
224
+ }
225
+
226
+ // Wait briefly and check health
227
+ spawnSync("sleep", ["2"]);
228
+
229
+ if (containerRunning()) {
230
+ console.log("\n✓ ARC-402 Workroom is running");
231
+ console.log(` Container: ${WORKROOM_CONTAINER}`);
232
+ console.log(` Policy hash: ${getPolicyHash()}`);
233
+ console.log(` Relay port: 4402`);
234
+ console.log(` Logs: arc402 workroom logs`);
235
+ } else {
236
+ console.error("Workroom started but exited immediately. Check logs:");
237
+ console.error(" docker logs arc402-workroom");
238
+ process.exit(1);
239
+ }
240
+ });
241
+
242
+ // ── stop ──────────────────────────────────────────────────────────────────
243
+ workroom
244
+ .command("stop")
245
+ .description("Stop the ARC-402 Workroom.")
246
+ .action(async () => {
247
+ if (!containerRunning()) {
248
+ console.log("Workroom is not running.");
249
+ return;
250
+ }
251
+ console.log("Stopping ARC-402 Workroom...");
252
+ runCmd("docker", ["stop", WORKROOM_CONTAINER]);
253
+ console.log("✓ Workroom stopped");
254
+ });
255
+
256
+ // ── status ────────────────────────────────────────────────────────────────
257
+ workroom
258
+ .command("status")
259
+ .description("Show ARC-402 Workroom health, policy, and active state.")
260
+ .action(async () => {
261
+ console.log("ARC-402 Workroom Status");
262
+ console.log("───────────────────────");
263
+
264
+ // Docker
265
+ if (!dockerAvailable()) {
266
+ console.log("Docker: ❌ not available");
267
+ return;
268
+ }
269
+ console.log("Docker: ✓ available");
270
+
271
+ // Image
272
+ console.log(`Image: ${imageExists() ? "✓ " + WORKROOM_IMAGE : "❌ not built (run: arc402 workroom init)"}`);
273
+
274
+ // Container
275
+ if (containerRunning()) {
276
+ console.log(`Container: ✓ running (${WORKROOM_CONTAINER})`);
277
+
278
+ // Get container uptime
279
+ const inspect = runCmd("docker", ["inspect", WORKROOM_CONTAINER, "--format", "{{.State.StartedAt}}"]);
280
+ if (inspect.ok) {
281
+ const started = new Date(inspect.stdout.trim());
282
+ const uptime = Math.floor((Date.now() - started.getTime()) / 1000);
283
+ const h = Math.floor(uptime / 3600);
284
+ const m = Math.floor((uptime % 3600) / 60);
285
+ console.log(`Uptime: ${h}h ${m}m`);
286
+ }
287
+
288
+ // Get iptables rule count from inside container
289
+ const rules = runCmd("docker", ["exec", WORKROOM_CONTAINER, "iptables", "-L", "OUTPUT", "-n", "--line-numbers"]);
290
+ if (rules.ok) {
291
+ const ruleCount = rules.stdout.split("\n").filter(l => l.match(/^\d+/)).length;
292
+ console.log(`Network rules: ${ruleCount} iptables rules enforced`);
293
+ }
294
+ } else if (containerExists()) {
295
+ console.log(`Container: ⚠ stopped (run: arc402 workroom start)`);
296
+ } else {
297
+ console.log(`Container: ❌ not created (run: arc402 workroom init)`);
298
+ }
299
+
300
+ // Policy
301
+ console.log(`Policy file: ${fs.existsSync(POLICY_FILE) ? "✓ " + POLICY_FILE : "❌ missing"}`);
302
+ console.log(`Policy hash: ${getPolicyHash()}`);
303
+
304
+ // Arena
305
+ const arenaExists = fs.existsSync(ARENA_DATA_DIR);
306
+ const arenaPolicy = fs.existsSync(ARENA_POLICY_FILE);
307
+ console.log(`Arena data: ${arenaExists ? "✓ " + ARENA_DATA_DIR : "❌ missing (run: arc402 workroom init)"}`);
308
+ console.log(`Arena policy: ${arenaPolicy ? "✓ loaded" : "❌ missing"}`);
309
+
310
+ // Arena queue (pending approvals)
311
+ if (arenaExists) {
312
+ const queueDir = path.join(ARENA_DATA_DIR, "queue");
313
+ if (fs.existsSync(queueDir)) {
314
+ const pending = fs.readdirSync(queueDir).filter(f => f.endsWith(".json")).length;
315
+ if (pending > 0) {
316
+ console.log(`Arena queue: ⚠ ${pending} action(s) awaiting approval`);
317
+ } else {
318
+ console.log(`Arena queue: ✓ empty`);
319
+ }
320
+ }
321
+ }
322
+ });
323
+
324
+ // ── logs ──────────────────────────────────────────────────────────────────
325
+ workroom
326
+ .command("logs")
327
+ .description("Tail workroom daemon logs.")
328
+ .option("--follow", "Stream live log output")
329
+ .option("-n, --lines <n>", "Number of lines", "50")
330
+ .action(async (opts) => {
331
+ const args = ["logs"];
332
+ if (opts.follow) args.push("-f");
333
+ args.push("--tail", opts.lines);
334
+ args.push(WORKROOM_CONTAINER);
335
+
336
+ spawnSync("docker", args, { stdio: "inherit" });
337
+ });
338
+
339
+ // ── shell ─────────────────────────────────────────────────────────────────
340
+ workroom
341
+ .command("shell")
342
+ .description("Open a shell inside the workroom for debugging.")
343
+ .action(async () => {
344
+ if (!containerRunning()) {
345
+ console.error("Workroom is not running.");
346
+ process.exit(1);
347
+ }
348
+ spawnSync("docker", ["exec", "-it", WORKROOM_CONTAINER, "/bin/bash"], {
349
+ stdio: "inherit",
350
+ });
351
+ });
352
+
353
+ // ── doctor ────────────────────────────────────────────────────────────────
354
+ workroom
355
+ .command("doctor")
356
+ .description("Diagnose workroom health: Docker, image, container, network, policy, daemon.")
357
+ .action(async () => {
358
+ console.log("ARC-402 Workroom Doctor");
359
+ console.log("───────────────────────");
360
+
361
+ const checks: Array<{ label: string; pass: boolean; detail: string }> = [];
362
+
363
+ // Docker
364
+ const docker = dockerAvailable();
365
+ checks.push({ label: "Docker", pass: docker, detail: docker ? "available" : "not available — install Docker Desktop" });
366
+
367
+ // Image
368
+ const img = imageExists();
369
+ checks.push({ label: "Image", pass: img, detail: img ? WORKROOM_IMAGE : "not built — run: arc402 workroom init" });
370
+
371
+ // Container
372
+ const running = containerRunning();
373
+ checks.push({ label: "Container", pass: running, detail: running ? "running" : "not running — run: arc402 workroom start" });
374
+
375
+ // Policy
376
+ const policyExists = fs.existsSync(POLICY_FILE);
377
+ checks.push({ label: "Policy file", pass: policyExists, detail: policyExists ? POLICY_FILE : "missing" });
378
+
379
+ // daemon.toml
380
+ const daemonCfg = fs.existsSync(DAEMON_TOML);
381
+ checks.push({ label: "daemon.toml", pass: daemonCfg, detail: daemonCfg ? "found" : "missing — run: arc402 daemon init" });
382
+
383
+ // Machine key env
384
+ const mk = !!process.env.ARC402_MACHINE_KEY;
385
+ checks.push({ label: "Machine key env", pass: mk, detail: mk ? "set" : "ARC402_MACHINE_KEY not in environment" });
386
+
387
+ // Network connectivity (if running)
388
+ if (running) {
389
+ const rpcTest = runCmd("docker", ["exec", WORKROOM_CONTAINER, "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "5", "https://mainnet.base.org"]);
390
+ const rpcOk = rpcTest.ok && rpcTest.stdout.trim() !== "000";
391
+ checks.push({ label: "Base RPC from workroom", pass: rpcOk, detail: rpcOk ? `HTTP ${rpcTest.stdout.trim()}` : "FAILED — network policy may be blocking RPC" });
392
+ }
393
+
394
+ // Print results
395
+ for (const c of checks) {
396
+ const icon = c.pass ? "✓" : "✗";
397
+ const color = c.pass ? "" : " ← FIX";
398
+ console.log(` ${icon} ${c.label}: ${c.detail}${color}`);
399
+ }
400
+
401
+ const failures = checks.filter(c => !c.pass);
402
+ if (failures.length === 0) {
403
+ console.log("\n✓ All checks passed. Workroom is healthy.");
404
+ } else {
405
+ console.log(`\n✗ ${failures.length} issue(s) found.`);
406
+ }
407
+ });
408
+
409
+ // ── policy (delegate to existing openshell policy commands) ───────────────
410
+ workroom
411
+ .command("policy")
412
+ .description("Manage workroom network policy. Delegates to the existing policy UX.")
413
+ .action(() => {
414
+ console.log("Use the policy subcommands:");
415
+ console.log(" arc402 workroom policy list");
416
+ console.log(" arc402 workroom policy preset <name>");
417
+ console.log(" arc402 workroom policy peer add <host>");
418
+ console.log(" arc402 workroom policy test <host>");
419
+ console.log(" arc402 workroom policy hash");
420
+ console.log(" arc402 workroom policy reload");
421
+ console.log("\nFor now, these delegate to 'arc402 openshell policy' commands.");
422
+ console.log("Full native workroom policy management coming in next release.");
423
+ });
424
+
425
+ // ── policy hash ──────────────────────────────────────────────────────────
426
+ const policyCmd = workroom.command("policy-hash")
427
+ .description("Get the SHA-256 hash of the current workroom policy (for AgentRegistry).")
428
+ .action(async () => {
429
+ console.log(getPolicyHash());
430
+ });
431
+
432
+ // ── policy test ──────────────────────────────────────────────────────────
433
+ workroom
434
+ .command("policy-test <host>")
435
+ .description("Test if a specific host is reachable from inside the workroom.")
436
+ .action(async (host) => {
437
+ if (!containerRunning()) {
438
+ console.error("Workroom is not running. Start it first: arc402 workroom start");
439
+ process.exit(1);
440
+ }
441
+ console.log(`Testing connectivity to ${host} from inside workroom...`);
442
+ const result = runCmd("docker", [
443
+ "exec", WORKROOM_CONTAINER,
444
+ "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "5",
445
+ `https://${host}`,
446
+ ]);
447
+ if (result.ok && result.stdout.trim() !== "000") {
448
+ console.log(`✓ ${host} is reachable (HTTP ${result.stdout.trim()})`);
449
+ } else {
450
+ console.log(`✗ ${host} is NOT reachable from the workroom`);
451
+ console.log(" This host may not be in the workroom policy.");
452
+ console.log(" Add it with: arc402 openshell policy add <name> <host>");
453
+ }
454
+ });
455
+
456
+ // ── worker ─────────────────────────────────────────────────────────────────
457
+ const worker = workroom.command("worker").description("Manage the workroom worker — the agent identity that executes hired tasks.");
458
+
459
+ worker
460
+ .command("init")
461
+ .description("Initialize the workroom worker identity and configuration.")
462
+ .option("--name <name>", "Worker display name", "Worker")
463
+ .option("--model <model>", "Preferred LLM model for task execution")
464
+ .action(async (opts) => {
465
+ const workerDir = path.join(ARC402_DIR, "worker");
466
+ const memoryDir = path.join(workerDir, "memory");
467
+ const skillsDir = path.join(workerDir, "skills");
468
+
469
+ fs.mkdirSync(memoryDir, { recursive: true });
470
+ fs.mkdirSync(skillsDir, { recursive: true });
471
+
472
+ // Generate default worker SOUL.md
473
+ const soulPath = path.join(workerDir, "SOUL.md");
474
+ if (!fs.existsSync(soulPath)) {
475
+ fs.writeFileSync(soulPath, `# Worker Identity — ${opts.name}
476
+
477
+ You are a professional worker operating under an ARC-402 governed workroom.
478
+
479
+ ## Your role
480
+ - Execute hired tasks within governance bounds
481
+ - Produce high-quality deliverables on deadline
482
+ - Follow the task specification precisely
483
+ - Report issues early if the task cannot be completed as specified
484
+
485
+ ## What you have access to
486
+ - The task specification from the hiring agreement
487
+ - Skills relevant to your registered capabilities
488
+ - Accumulated learnings from previous jobs (in memory/learnings.md)
489
+ - Network access only to policy-approved hosts
490
+
491
+ ## What you do NOT have access to
492
+ - The operator's personal conversations or memory
493
+ - The operator's other agents or their state
494
+ - Network hosts not in the workroom policy
495
+ - Files outside the workroom
496
+
497
+ ## How you learn
498
+ After completing each job, reflect on:
499
+ - What techniques worked well
500
+ - What patterns you noticed in the task
501
+ - What domain knowledge you acquired
502
+ - What you would do differently next time
503
+
504
+ Write these learnings concisely. They will be available on your next job.
505
+
506
+ ## Professional standards
507
+ - Deliver on time or communicate blockers before the deadline
508
+ - Never fabricate data or claim work was done when it wasn't
509
+ - If the task is unclear, produce the best interpretation and document assumptions
510
+ - Every deliverable must be verifiable against the task spec
511
+ `);
512
+ console.log(`✓ Worker SOUL.md created: ${soulPath}`);
513
+ } else {
514
+ console.log(`✓ Worker SOUL.md already exists: ${soulPath}`);
515
+ }
516
+
517
+ // Generate default MEMORY.md
518
+ const memoryPath = path.join(workerDir, "MEMORY.md");
519
+ if (!fs.existsSync(memoryPath)) {
520
+ fs.writeFileSync(memoryPath, `# Worker Memory
521
+
522
+ *Last updated: ${new Date().toISOString().split("T")[0]}*
523
+
524
+ ## Job count: 0
525
+ ## Total earned: 0 ETH
526
+
527
+ ## Learnings
528
+
529
+ No jobs completed yet. Learnings will accumulate here as the worker completes hired tasks.
530
+ `);
531
+ console.log(`✓ Worker MEMORY.md created: ${memoryPath}`);
532
+ }
533
+
534
+ // Generate learnings.md
535
+ const learningsPath = path.join(memoryDir, "learnings.md");
536
+ if (!fs.existsSync(learningsPath)) {
537
+ fs.writeFileSync(learningsPath, `# Accumulated Learnings
538
+
539
+ *Distilled from completed jobs. Available to the worker on every new task.*
540
+
541
+ ---
542
+
543
+ No learnings yet. Complete your first hired task to start accumulating expertise.
544
+ `);
545
+ console.log(`✓ Learnings file created: ${learningsPath}`);
546
+ }
547
+
548
+ // Worker config
549
+ const configPath = path.join(workerDir, "config.json");
550
+ if (!fs.existsSync(configPath)) {
551
+ const config = {
552
+ name: opts.name,
553
+ model: opts.model || "default",
554
+ capabilities: [],
555
+ created: new Date().toISOString(),
556
+ job_count: 0,
557
+ total_earned_eth: "0",
558
+ };
559
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
560
+ console.log(`✓ Worker config created: ${configPath}`);
561
+ }
562
+
563
+ console.log(`\nWorker initialized at: ${workerDir}`);
564
+ console.log("Next: customize the worker SOUL.md and add skills.");
565
+ console.log(" arc402 workroom worker set-soul <file>");
566
+ console.log(" arc402 workroom worker set-skills <dir>");
567
+ });
568
+
569
+ worker
570
+ .command("status")
571
+ .description("Show worker identity, job count, learnings, and configuration.")
572
+ .action(async () => {
573
+ const workerDir = path.join(ARC402_DIR, "worker");
574
+ const configPath = path.join(workerDir, "config.json");
575
+
576
+ if (!fs.existsSync(configPath)) {
577
+ console.error("Worker not initialized. Run: arc402 workroom worker init");
578
+ process.exit(1);
579
+ }
580
+
581
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
582
+ const memoryDir = path.join(workerDir, "memory");
583
+ const jobFiles = fs.existsSync(memoryDir)
584
+ ? fs.readdirSync(memoryDir).filter(f => f.startsWith("job-")).length
585
+ : 0;
586
+ const learningsPath = path.join(memoryDir, "learnings.md");
587
+ const learningsSize = fs.existsSync(learningsPath)
588
+ ? fs.statSync(learningsPath).size
589
+ : 0;
590
+ const skillsDir = path.join(workerDir, "skills");
591
+ const skillCount = fs.existsSync(skillsDir)
592
+ ? fs.readdirSync(skillsDir).length
593
+ : 0;
594
+
595
+ console.log("ARC-402 Workroom Worker");
596
+ console.log("───────────────────────");
597
+ console.log(`Name: ${config.name}`);
598
+ console.log(`Model: ${config.model}`);
599
+ console.log(`Created: ${config.created}`);
600
+ console.log(`Jobs done: ${config.job_count}`);
601
+ console.log(`Job memories: ${jobFiles}`);
602
+ console.log(`Learnings: ${learningsSize > 200 ? Math.round(learningsSize / 1024) + " KB" : "empty"}`);
603
+ console.log(`Skills: ${skillCount}`);
604
+ console.log(`Total earned: ${config.total_earned_eth} ETH`);
605
+ });
606
+
607
+ worker
608
+ .command("set-soul <file>")
609
+ .description("Upload a custom worker SOUL.md.")
610
+ .action(async (file) => {
611
+ if (!fs.existsSync(file)) {
612
+ console.error(`File not found: ${file}`);
613
+ process.exit(1);
614
+ }
615
+ const dest = path.join(ARC402_DIR, "worker", "SOUL.md");
616
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
617
+ fs.copyFileSync(file, dest);
618
+ console.log(`✓ Worker SOUL.md updated from: ${file}`);
619
+ });
620
+
621
+ worker
622
+ .command("set-skills <dir>")
623
+ .description("Copy skills into the workroom worker.")
624
+ .action(async (dir) => {
625
+ if (!fs.existsSync(dir)) {
626
+ console.error(`Directory not found: ${dir}`);
627
+ process.exit(1);
628
+ }
629
+ const dest = path.join(ARC402_DIR, "worker", "skills");
630
+ fs.mkdirSync(dest, { recursive: true });
631
+ // Copy all files from source to dest
632
+ const files = fs.readdirSync(dir);
633
+ for (const f of files) {
634
+ const src = path.join(dir, f);
635
+ const dst = path.join(dest, f);
636
+ if (fs.statSync(src).isFile()) {
637
+ fs.copyFileSync(src, dst);
638
+ } else if (fs.statSync(src).isDirectory()) {
639
+ // Recursive copy for skill directories
640
+ fs.cpSync(src, dst, { recursive: true });
641
+ }
642
+ }
643
+ console.log(`✓ ${files.length} items copied to worker skills`);
644
+ });
645
+
646
+ worker
647
+ .command("set-knowledge <dir>")
648
+ .description("Mount a knowledge directory into the workroom. Contains reference materials, training data, domain docs — anything the worker needs to deliver its services.")
649
+ .action(async (dir) => {
650
+ if (!fs.existsSync(dir)) {
651
+ console.error(`Directory not found: ${dir}`);
652
+ process.exit(1);
653
+ }
654
+ const dest = path.join(ARC402_DIR, "worker", "knowledge");
655
+ fs.mkdirSync(dest, { recursive: true });
656
+ const files = fs.readdirSync(dir);
657
+ let count = 0;
658
+ for (const f of files) {
659
+ const src = path.join(dir, f);
660
+ const dst = path.join(dest, f);
661
+ if (fs.statSync(src).isFile()) {
662
+ fs.copyFileSync(src, dst);
663
+ count++;
664
+ } else if (fs.statSync(src).isDirectory()) {
665
+ fs.cpSync(src, dst, { recursive: true });
666
+ count++;
667
+ }
668
+ }
669
+ console.log(`✓ ${count} items copied to worker knowledge`);
670
+ console.log(` Path: ${dest}`);
671
+ console.log(` The worker can reference these files during hired tasks.`);
672
+ console.log(` To update: run this command again with the updated directory.`);
673
+ });
674
+
675
+ worker
676
+ .command("knowledge")
677
+ .description("List the worker's knowledge directory contents.")
678
+ .action(async () => {
679
+ const knowledgeDir = path.join(ARC402_DIR, "worker", "knowledge");
680
+ if (!fs.existsSync(knowledgeDir)) {
681
+ console.log("No knowledge directory. Add one with: arc402 workroom worker set-knowledge <dir>");
682
+ return;
683
+ }
684
+ const files = fs.readdirSync(knowledgeDir, { recursive: true, withFileTypes: false }) as string[];
685
+ if (files.length === 0) {
686
+ console.log("Knowledge directory is empty.");
687
+ return;
688
+ }
689
+ console.log(`Worker knowledge (${files.length} items):\n`);
690
+ for (const f of fs.readdirSync(knowledgeDir)) {
691
+ const stat = fs.statSync(path.join(knowledgeDir, f));
692
+ const size = stat.isDirectory() ? "dir" : `${(stat.size / 1024).toFixed(1)} KB`;
693
+ console.log(` ${f.padEnd(40)} ${size}`);
694
+ }
695
+ });
696
+
697
+ worker
698
+ .command("memory")
699
+ .description("Show the worker's accumulated learnings.")
700
+ .action(async () => {
701
+ const learningsPath = path.join(ARC402_DIR, "worker", "memory", "learnings.md");
702
+ if (!fs.existsSync(learningsPath)) {
703
+ console.log("No learnings yet. Complete a hired task first.");
704
+ return;
705
+ }
706
+ console.log(fs.readFileSync(learningsPath, "utf-8"));
707
+ });
708
+
709
+ worker
710
+ .command("memory-reset")
711
+ .description("Clear the worker's accumulated memory (start fresh).")
712
+ .action(async () => {
713
+ const memoryDir = path.join(ARC402_DIR, "worker", "memory");
714
+ if (fs.existsSync(memoryDir)) {
715
+ const files = fs.readdirSync(memoryDir);
716
+ for (const f of files) fs.unlinkSync(path.join(memoryDir, f));
717
+ fs.writeFileSync(path.join(memoryDir, "learnings.md"), `# Accumulated Learnings\n\n*Reset: ${new Date().toISOString()}*\n\nNo learnings yet.\n`);
718
+ }
719
+ // Reset job count in config
720
+ const configPath = path.join(ARC402_DIR, "worker", "config.json");
721
+ if (fs.existsSync(configPath)) {
722
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
723
+ config.job_count = 0;
724
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
725
+ }
726
+ console.log("✓ Worker memory cleared. Starting fresh.");
727
+ });
728
+
729
+ // ── token usage ──────────────────────────────────────────────────────────
730
+ workroom
731
+ .command("token-usage [agreementId]")
732
+ .description("Show token usage for a specific agreement or across all jobs.")
733
+ .action(async (agreementId) => {
734
+ const { readUsageReport, formatUsageReport } = require("../daemon/token-metering");
735
+
736
+ if (agreementId) {
737
+ const usage = readUsageReport(agreementId);
738
+ if (!usage) {
739
+ console.log(`No token usage data for agreement: ${agreementId}`);
740
+ return;
741
+ }
742
+ console.log(formatUsageReport(usage));
743
+ } else {
744
+ // Aggregate across all receipts
745
+ const receiptsDir = path.join(ARC402_DIR, "receipts");
746
+ if (!fs.existsSync(receiptsDir)) {
747
+ console.log("No receipts yet.");
748
+ return;
749
+ }
750
+ const files = fs.readdirSync(receiptsDir).filter((f: string) => f.endsWith(".json"));
751
+ let totalInput = 0;
752
+ let totalOutput = 0;
753
+ let totalCost = 0;
754
+ let jobsWithUsage = 0;
755
+
756
+ for (const f of files) {
757
+ try {
758
+ const receipt = JSON.parse(fs.readFileSync(path.join(receiptsDir, f), "utf-8"));
759
+ if (receipt.token_usage) {
760
+ totalInput += receipt.token_usage.total_input || 0;
761
+ totalOutput += receipt.token_usage.total_output || 0;
762
+ totalCost += receipt.token_usage.estimated_cost_usd || 0;
763
+ jobsWithUsage++;
764
+ }
765
+ } catch { /* skip */ }
766
+ }
767
+
768
+ if (jobsWithUsage === 0) {
769
+ console.log("No token usage data in any receipts yet.");
770
+ return;
771
+ }
772
+
773
+ console.log("Aggregate Token Usage");
774
+ console.log("─────────────────────");
775
+ console.log(`Jobs with data: ${jobsWithUsage}`);
776
+ console.log(`Total tokens: ${(totalInput + totalOutput).toLocaleString()} (${totalInput.toLocaleString()} in / ${totalOutput.toLocaleString()} out)`);
777
+ console.log(`Est. total cost: $${totalCost.toFixed(4)}`);
778
+ if (jobsWithUsage > 0) {
779
+ console.log(`Avg per job: $${(totalCost / jobsWithUsage).toFixed(4)}`);
780
+ }
781
+ }
782
+ });
783
+
784
+ // ── receipts + earnings ──────────────────────────────────────────────────
785
+ workroom
786
+ .command("receipts")
787
+ .description("List all execution receipts from completed jobs.")
788
+ .action(async () => {
789
+ const receiptsDir = path.join(ARC402_DIR, "receipts");
790
+ if (!fs.existsSync(receiptsDir)) {
791
+ console.log("No receipts yet.");
792
+ return;
793
+ }
794
+ const files = fs.readdirSync(receiptsDir).filter(f => f.endsWith(".json")).sort();
795
+ if (files.length === 0) {
796
+ console.log("No receipts yet.");
797
+ return;
798
+ }
799
+ console.log(`${files.length} execution receipt(s):\n`);
800
+ for (const f of files) {
801
+ try {
802
+ const receipt = JSON.parse(fs.readFileSync(path.join(receiptsDir, f), "utf-8"));
803
+ const id = receipt.agreement_id || f.replace(".json", "");
804
+ const time = receipt.completed_at || "unknown";
805
+ const hash = receipt.deliverable_hash ? receipt.deliverable_hash.slice(0, 10) + "..." : "—";
806
+ console.log(` ${id} ${time} deliverable: ${hash}`);
807
+ } catch {
808
+ console.log(` ${f} (unreadable)`);
809
+ }
810
+ }
811
+ });
812
+
813
+ workroom
814
+ .command("receipt <agreementId>")
815
+ .description("Show full execution receipt for a specific job.")
816
+ .action(async (agreementId) => {
817
+ const receiptPath = path.join(ARC402_DIR, "receipts", `${agreementId}.json`);
818
+ if (!fs.existsSync(receiptPath)) {
819
+ console.error(`No receipt found for agreement: ${agreementId}`);
820
+ process.exit(1);
821
+ }
822
+ console.log(fs.readFileSync(receiptPath, "utf-8"));
823
+ });
824
+
825
+ workroom
826
+ .command("earnings")
827
+ .description("Show total earnings from completed jobs.")
828
+ .option("--period <period>", "Time period (e.g. 7d, 30d, all)", "all")
829
+ .action(async (opts) => {
830
+ const configPath = path.join(ARC402_DIR, "worker", "config.json");
831
+ if (!fs.existsSync(configPath)) {
832
+ console.log("No worker configured. Run: arc402 workroom worker init");
833
+ return;
834
+ }
835
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
836
+ console.log("ARC-402 Earnings");
837
+ console.log("────────────────");
838
+ console.log(`Total earned: ${config.total_earned_eth} ETH`);
839
+ console.log(`Jobs completed: ${config.job_count}`);
840
+ if (config.job_count > 0) {
841
+ const avg = (parseFloat(config.total_earned_eth) / config.job_count).toFixed(6);
842
+ console.log(`Average/job: ${avg} ETH`);
843
+ }
844
+ });
845
+
846
+ workroom
847
+ .command("history")
848
+ .description("Show job history with outcomes and earnings.")
849
+ .action(async () => {
850
+ const memoryDir = path.join(ARC402_DIR, "worker", "memory");
851
+ if (!fs.existsSync(memoryDir)) {
852
+ console.log("No job history yet.");
853
+ return;
854
+ }
855
+ const jobFiles = fs.readdirSync(memoryDir).filter(f => f.startsWith("job-")).sort();
856
+ if (jobFiles.length === 0) {
857
+ console.log("No job history yet.");
858
+ return;
859
+ }
860
+ console.log(`${jobFiles.length} completed job(s):\n`);
861
+ for (const f of jobFiles) {
862
+ const content = fs.readFileSync(path.join(memoryDir, f), "utf-8");
863
+ const firstLine = content.split("\n").find(l => l.startsWith("#")) || f;
864
+ console.log(` ${f.replace(".md", "")} ${firstLine.replace(/^#+\s*/, "")}`);
865
+ }
866
+ });
867
+
868
+ // ── policy reload ────────────────────────────────────────────────────────
869
+ workroom
870
+ .command("policy-reload")
871
+ .description("Re-read the policy file and update iptables rules inside the running workroom.")
872
+ .action(async () => {
873
+ if (!containerRunning()) {
874
+ console.error("Workroom is not running.");
875
+ process.exit(1);
876
+ }
877
+ console.log("Reloading workroom policy...");
878
+ // Trigger DNS refresh manually (which re-reads policy and updates iptables)
879
+ const result = runCmd("docker", [
880
+ "exec", WORKROOM_CONTAINER,
881
+ "bash", "-c", "/dns-refresh.sh /workroom/.arc402/openshell-policy.yaml &",
882
+ ]);
883
+ if (result.ok) {
884
+ console.log("✓ Policy reload triggered");
885
+ } else {
886
+ console.error("Failed to reload policy");
887
+ }
888
+ });
889
+ }