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