@poolzin/pool-bot 2026.3.22 → 2026.3.24
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/CHANGELOG.md +111 -0
- package/dist/.buildstamp +1 -1
- package/dist/acp/bindings-store.js +209 -0
- package/dist/acp/control-plane/runtime-cache.js +54 -0
- package/dist/acp/control-plane/runtime-options.js +215 -0
- package/dist/acp/control-plane/session-actor-queue.js +36 -0
- package/dist/acp/policy.js +52 -0
- package/dist/acp/runtime/errors.js +47 -0
- package/dist/acp/runtime/registry.js +86 -0
- package/dist/acp/runtime/types.js +1 -0
- package/dist/acp/translator.js +97 -0
- package/dist/agents/btw.js +280 -0
- package/dist/agents/failover-error.js +145 -47
- package/dist/agents/fast-mode.js +24 -0
- package/dist/agents/live-model-errors.js +23 -0
- package/dist/agents/model-auth-env-vars.js +44 -0
- package/dist/agents/model-auth-markers.js +69 -0
- package/dist/agents/models-config.providers.discovery.js +180 -0
- package/dist/agents/models-config.providers.static.js +480 -0
- package/dist/auto-reply/reply/typing-policy.js +15 -0
- package/dist/browser/browser-profile-manager.js +319 -0
- package/dist/browser/cdp-proxy-bypass.js +129 -0
- package/dist/browser/cdp-timeouts.js +41 -0
- package/dist/browser/chrome-extension-validator.js +406 -0
- package/dist/browser/chrome-mcp-snapshot.js +222 -0
- package/dist/browser/chrome-mcp.js +421 -0
- package/dist/browser/chrome-mcp.snapshot.js +133 -0
- package/dist/browser/errors.js +67 -0
- package/dist/browser/form-fields.js +22 -0
- package/dist/browser/output-atomic.js +44 -0
- package/dist/browser/profile-capabilities.js +47 -0
- package/dist/browser/safe-filename.js +25 -0
- package/dist/browser/snapshot-roles.js +60 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/account-snapshot-fields.js +176 -0
- package/dist/channels/draft-stream-controls.js +89 -0
- package/dist/channels/inbound-debounce-policy.js +28 -0
- package/dist/channels/typing-lifecycle.js +39 -0
- package/dist/cli/program/command-registry.js +52 -0
- package/dist/commands/agent-binding.js +123 -0
- package/dist/commands/agents.commands.bind.js +280 -0
- package/dist/commands/backup-shared.js +186 -0
- package/dist/commands/backup-verify.js +236 -0
- package/dist/commands/backup.js +166 -0
- package/dist/commands/channel-account-context.js +15 -0
- package/dist/commands/channel-account.js +190 -0
- package/dist/commands/gateway-install-token.js +117 -0
- package/dist/commands/oauth-tls-preflight.js +121 -0
- package/dist/commands/ollama-setup.js +402 -0
- package/dist/commands/security-owner-only.js +86 -0
- package/dist/commands/self-hosted-provider-setup.js +207 -0
- package/dist/commands/session-store-targets.js +12 -0
- package/dist/commands/sessions-cleanup.js +97 -0
- package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
- package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/cron-filters.js +150 -0
- package/dist/cron/heartbeat-policy.js +26 -0
- package/dist/gateway/device-pairing-security.js +197 -0
- package/dist/gateway/event-deduplication.js +167 -0
- package/dist/gateway/hooks-mapping.js +46 -7
- package/dist/gateway/run-tracker.js +253 -0
- package/dist/gateway/server-methods/nodes.js +14 -0
- package/dist/gateway/websocket-preauth-security.js +188 -0
- package/dist/hooks/module-loader.js +28 -0
- package/dist/infra/agent-command-binding.js +144 -0
- package/dist/infra/backup.js +328 -0
- package/dist/infra/channel-account-context.js +173 -0
- package/dist/infra/errors.js +53 -13
- package/dist/infra/exec-approvals-security.js +217 -0
- package/dist/infra/security/command-analyzer.js +257 -0
- package/dist/infra/session-cleanup.js +143 -0
- package/dist/plugins/loader.js +16 -8
- package/dist/security/external-content.js +51 -1
- package/dist/sessions/session-costs.js +228 -0
- package/dist/shared/param-key.js +16 -0
- package/dist/shared/poll-params.js +58 -0
- package/dist/shared/polls.js +55 -0
- package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
- package/docs/FEATURES.md +523 -0
- package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
- package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
- package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
- package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
- package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
- package/docs/MIKRODASH-ANALYSIS.md +412 -0
- package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
- package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
- package/docs/PHASE-7-SUMMARY.md +144 -0
- package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
- package/docs/PROJECT-FINAL-STATUS.md +237 -0
- package/docs/README.md +116 -0
- package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
- package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
- package/docs/channels/googlechat.md +235 -206
- package/docs/channels/irc.md +332 -0
- package/docs/channels/nostr.md +255 -168
- package/docs/components/command-palette.md +166 -0
- package/docs/components/login-gate.md +219 -0
- package/docs/getting-started/installation.md +191 -0
- package/docs/getting-started/introduction.md +120 -0
- package/docs/improvements/USAGE-GUIDE.md +359 -0
- package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
- package/docs/reference/deadcode-detection.md +72 -0
- package/extensions/acpx/node_modules/.bin/acpx +21 -0
- package/extensions/agency-agents/node_modules/.bin/vite +4 -4
- package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
- package/extensions/googlechat/node_modules/.bin/tsc +21 -0
- package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
- package/extensions/googlechat/node_modules/.bin/vitest +21 -0
- package/extensions/googlechat/package.json +11 -28
- package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
- package/extensions/googlechat/src/googlechat-channel.ts +120 -0
- package/extensions/googlechat/src/index.ts +14 -0
- package/extensions/irc/node_modules/.bin/tsc +21 -0
- package/extensions/irc/node_modules/.bin/tsserver +21 -0
- package/extensions/irc/node_modules/.bin/vitest +21 -0
- package/extensions/irc/package.json +16 -8
- package/extensions/irc/src/index.ts +14 -0
- package/extensions/irc/src/irc-channel.test.ts +43 -0
- package/extensions/irc/src/irc-channel.ts +191 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
- package/extensions/keyed-async-queue/package.json +20 -0
- package/extensions/keyed-async-queue/src/index.ts +14 -0
- package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
- package/extensions/keyed-async-queue/src/queue.ts +200 -0
- package/extensions/memory-core/node_modules/.bin/tsc +21 -0
- package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
- package/extensions/memory-core/node_modules/.bin/vitest +21 -0
- package/extensions/memory-core/package.json +11 -8
- package/extensions/memory-core/src/index.ts +14 -0
- package/extensions/memory-core/src/memory-manager.test.ts +124 -0
- package/extensions/memory-core/src/memory-manager.ts +186 -0
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/nostr/node_modules/.bin/vitest +21 -0
- package/extensions/nostr/package.json +15 -24
- package/extensions/nostr/src/index.ts +14 -0
- package/extensions/nostr/src/nostr-channel.test.ts +55 -0
- package/extensions/nostr/src/nostr-channel.ts +228 -0
- package/extensions/page-agent/node_modules/.bin/vitest +2 -2
- package/extensions/test-utils/node_modules/.bin/jiti +21 -0
- package/extensions/test-utils/node_modules/.bin/playwright +21 -0
- package/extensions/test-utils/node_modules/.bin/tsx +21 -0
- package/extensions/test-utils/node_modules/.bin/vite +21 -0
- package/extensions/test-utils/node_modules/.bin/vitest +21 -0
- package/extensions/test-utils/node_modules/.bin/yaml +21 -0
- package/extensions/xyops/node_modules/.bin/vitest +2 -2
- package/package.json +2 -1
- package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
- package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
- package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Command
|
|
3
|
+
*
|
|
4
|
+
* Create and restore backups of Pool Bot data.
|
|
5
|
+
*/
|
|
6
|
+
import { createBackup, restoreBackup, listBackups, deleteBackup, getBackupDir, } from "../infra/backup.js";
|
|
7
|
+
function formatDuration(ms) {
|
|
8
|
+
if (ms < 1000)
|
|
9
|
+
return `${ms}ms`;
|
|
10
|
+
const seconds = Math.floor(ms / 1000);
|
|
11
|
+
if (seconds < 60)
|
|
12
|
+
return `${seconds}s`;
|
|
13
|
+
const minutes = Math.floor(seconds / 60);
|
|
14
|
+
const remainingSeconds = seconds % 60;
|
|
15
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
16
|
+
}
|
|
17
|
+
export function registerBackupCommand(program) {
|
|
18
|
+
const backupCmd = program.command("backup");
|
|
19
|
+
// Create backup
|
|
20
|
+
backupCmd
|
|
21
|
+
.command("create")
|
|
22
|
+
.description("Create a new backup")
|
|
23
|
+
.option("--no-sessions", "Exclude sessions from backup")
|
|
24
|
+
.option("--no-config", "Exclude config from backup")
|
|
25
|
+
.option("--no-credentials", "Exclude credentials from backup")
|
|
26
|
+
.option("--output <dir>", "Output directory for backup")
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
console.log("🎱 Pool Bot - Creating backup...\n");
|
|
29
|
+
const result = await createBackup({
|
|
30
|
+
includeSessions: options.sessions !== false,
|
|
31
|
+
includeConfig: options.config !== false,
|
|
32
|
+
includeCredentials: options.credentials !== false,
|
|
33
|
+
destinationDir: options.output,
|
|
34
|
+
});
|
|
35
|
+
if (result.success) {
|
|
36
|
+
console.log("✅ Backup created successfully!\n");
|
|
37
|
+
console.log(`📦 Backup Path: ${result.backupPath}`);
|
|
38
|
+
console.log(`⏱️ Duration: ${formatDuration(result.duration)}`);
|
|
39
|
+
console.log(`💾 Total Size: ${(result.totalSize / 1024 / 1024).toFixed(2)} MB`);
|
|
40
|
+
console.log(`\n📊 Contents:`);
|
|
41
|
+
if (result.metadata.includesSessions) {
|
|
42
|
+
console.log(` - Sessions: ${result.sessionsBackedUp || 0} files`);
|
|
43
|
+
}
|
|
44
|
+
if (result.metadata.includesConfig) {
|
|
45
|
+
console.log(` - Config: ${result.configBackedUp ? "Yes" : "No"}`);
|
|
46
|
+
}
|
|
47
|
+
if (result.metadata.includesCredentials) {
|
|
48
|
+
console.log(` - Credentials: ${result.credentialsBackedUp ? "Yes" : "No"}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.error("❌ Backup failed!\n");
|
|
53
|
+
console.error(`Error: ${result.error}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// Restore backup
|
|
58
|
+
backupCmd
|
|
59
|
+
.command("restore <backupPath>")
|
|
60
|
+
.description("Restore from a backup")
|
|
61
|
+
.option("--no-sessions", "Do not restore sessions")
|
|
62
|
+
.option("--no-config", "Do not restore config")
|
|
63
|
+
.option("--no-credentials", "Do not restore credentials")
|
|
64
|
+
.option("--dry-run", "Show what would be restored without actually restoring")
|
|
65
|
+
.action(async (backupPath, options) => {
|
|
66
|
+
console.log("🎱 Pool Bot - Restoring backup...\n");
|
|
67
|
+
const result = await restoreBackup({
|
|
68
|
+
backupPath,
|
|
69
|
+
restoreSessions: options.sessions !== false,
|
|
70
|
+
restoreConfig: options.config !== false,
|
|
71
|
+
restoreCredentials: options.credentials !== false,
|
|
72
|
+
dryRun: options.dryRun,
|
|
73
|
+
});
|
|
74
|
+
if (result.success) {
|
|
75
|
+
if (options.dryRun) {
|
|
76
|
+
console.log("🔍 Dry Run - No changes made\n");
|
|
77
|
+
console.log("📊 Would restore:");
|
|
78
|
+
if (result.sessionsRestored) {
|
|
79
|
+
console.log(` - Sessions: ${result.sessionsRestored} files`);
|
|
80
|
+
}
|
|
81
|
+
if (result.configRestored) {
|
|
82
|
+
console.log(` - Config: Yes`);
|
|
83
|
+
}
|
|
84
|
+
if (result.credentialsRestored) {
|
|
85
|
+
console.log(` - Credentials: Yes`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log("✅ Backup restored successfully!\n");
|
|
90
|
+
console.log(`⏱️ Duration: ${formatDuration(result.duration)}`);
|
|
91
|
+
console.log(`\n📊 Restored:`);
|
|
92
|
+
if (result.sessionsRestored) {
|
|
93
|
+
console.log(` - Sessions: ${result.sessionsRestored} files`);
|
|
94
|
+
}
|
|
95
|
+
if (result.configRestored) {
|
|
96
|
+
console.log(` - Config: Yes`);
|
|
97
|
+
}
|
|
98
|
+
if (result.credentialsRestored) {
|
|
99
|
+
console.log(` - Credentials: Yes`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.error("❌ Restore failed!\n");
|
|
105
|
+
console.error(`Error: ${result.error}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// List backups
|
|
110
|
+
backupCmd
|
|
111
|
+
.command("list")
|
|
112
|
+
.description("List available backups")
|
|
113
|
+
.option("--dir <dir>", "Backup directory", getBackupDir())
|
|
114
|
+
.action(async (options) => {
|
|
115
|
+
console.log("🎱 Pool Bot - Available backups:\n");
|
|
116
|
+
const backups = await listBackups(options.dir);
|
|
117
|
+
if (backups.length === 0) {
|
|
118
|
+
console.log("No backups found.\n");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
console.log(`📂 Backup Directory: ${options.dir}\n`);
|
|
122
|
+
console.log("┌─────┬──────────────────────┬──────────────┬──────────┬─────────┐");
|
|
123
|
+
console.log("│ # │ Created │ Sessions │ Size │ Path │");
|
|
124
|
+
console.log("├─────┼──────────────────────┼──────────────┼──────────┼─────────┤");
|
|
125
|
+
backups.forEach((backup, index) => {
|
|
126
|
+
const date = new Date(backup.createdAt).toLocaleString();
|
|
127
|
+
const sessions = backup.metadata.sessionCount || 0;
|
|
128
|
+
const size = (backup.size / 1024 / 1024).toFixed(2);
|
|
129
|
+
const pathDisplay = backup.path.length > 40 ? "..." + backup.path.slice(-37) : backup.path;
|
|
130
|
+
console.log(`│ ${String(index + 1).padEnd(3)} │ ${date.padEnd(20)} │ ${String(sessions).padEnd(12)} │ ${(size + " MB").padEnd(8)} │ ${pathDisplay.padEnd(7)} │`);
|
|
131
|
+
});
|
|
132
|
+
console.log("└─────┴──────────────────────┴──────────────┴──────────┴─────────┘\n");
|
|
133
|
+
console.log(`Total: ${backups.length} backup(s)\n`);
|
|
134
|
+
});
|
|
135
|
+
// Delete backup
|
|
136
|
+
backupCmd
|
|
137
|
+
.command("delete <backupPath>")
|
|
138
|
+
.description("Delete a backup")
|
|
139
|
+
.option("--force", "Skip confirmation")
|
|
140
|
+
.action(async (backupPath, options) => {
|
|
141
|
+
if (!options.force) {
|
|
142
|
+
const readline = await import("node:readline");
|
|
143
|
+
const rl = readline.createInterface({
|
|
144
|
+
input: process.stdin,
|
|
145
|
+
output: process.stdout,
|
|
146
|
+
});
|
|
147
|
+
const answer = await new Promise((resolve) => {
|
|
148
|
+
rl.question(`⚠️ Are you sure you want to delete this backup?\n ${backupPath}\n\n Type 'yes' to confirm: `, resolve);
|
|
149
|
+
});
|
|
150
|
+
rl.close();
|
|
151
|
+
if (answer.toLowerCase() !== "yes") {
|
|
152
|
+
console.log("\n❌ Deletion cancelled.\n");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const success = await deleteBackup(backupPath);
|
|
157
|
+
if (success) {
|
|
158
|
+
console.log("✅ Backup deleted successfully.\n");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.error("❌ Failed to delete backup.\n");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return backupCmd;
|
|
166
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
|
2
|
+
export async function resolveDefaultChannelAccountContext(plugin, cfg) {
|
|
3
|
+
const accountIds = plugin.config.listAccountIds(cfg);
|
|
4
|
+
const defaultAccountId = resolveChannelDefaultAccountId({
|
|
5
|
+
plugin,
|
|
6
|
+
cfg,
|
|
7
|
+
accountIds,
|
|
8
|
+
});
|
|
9
|
+
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
|
|
10
|
+
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true;
|
|
11
|
+
const configured = plugin.config.isConfigured
|
|
12
|
+
? await plugin.config.isConfigured(account, cfg)
|
|
13
|
+
: true;
|
|
14
|
+
return { accountIds, defaultAccountId, account, enabled, configured };
|
|
15
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Account Context CLI
|
|
3
|
+
*
|
|
4
|
+
* Manage multiple accounts per channel.
|
|
5
|
+
*/
|
|
6
|
+
import { addChannelAccount, removeChannelAccount, getChannelAccount, getChannelAccounts, getActiveChannelAccount, setActiveChannelAccount, updateChannelAccount, getChannelAccountStats, } from "../infra/channel-account-context.js";
|
|
7
|
+
function formatAccount(account) {
|
|
8
|
+
const status = account.isActive ? "🟢 Active" : "⚪ Inactive";
|
|
9
|
+
const lastUsed = account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleString() : "Never";
|
|
10
|
+
return `${status} | ${account.accountName} | ${account.channelType} | Last: ${lastUsed}`;
|
|
11
|
+
}
|
|
12
|
+
export function registerChannelAccountCommand(program) {
|
|
13
|
+
const accountCmd = program
|
|
14
|
+
.command("channel-account")
|
|
15
|
+
.description("Manage channel accounts (multi-account support)");
|
|
16
|
+
// List accounts
|
|
17
|
+
accountCmd
|
|
18
|
+
.command("list [channelId]")
|
|
19
|
+
.description("List channel accounts")
|
|
20
|
+
.action(async (channelId) => {
|
|
21
|
+
console.log("🎱 Pool Bot - Channel Accounts\n");
|
|
22
|
+
if (channelId) {
|
|
23
|
+
const accounts = await getChannelAccounts(channelId);
|
|
24
|
+
const active = await getActiveChannelAccount(channelId);
|
|
25
|
+
console.log(`Channel: ${channelId}`);
|
|
26
|
+
console.log(`Active Account: ${active ? active.accountName : "None"}\n`);
|
|
27
|
+
if (accounts.length === 0) {
|
|
28
|
+
console.log("No accounts found for this channel.\n");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
accounts.forEach((account) => {
|
|
32
|
+
console.log(formatAccount(account));
|
|
33
|
+
});
|
|
34
|
+
console.log(`\nTotal: ${accounts.length} account(s)\n`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const stats = await getChannelAccountStats();
|
|
38
|
+
console.log("📊 Account Statistics:");
|
|
39
|
+
console.log(` Total Accounts: ${stats.totalAccounts}`);
|
|
40
|
+
console.log(` Channels: ${stats.channelsWithAccounts}`);
|
|
41
|
+
console.log(` Active Accounts: ${stats.activeAccounts}`);
|
|
42
|
+
console.log("\nAccounts by Channel Type:");
|
|
43
|
+
Object.entries(stats.accountsByChannelType).forEach(([type, count]) => {
|
|
44
|
+
console.log(` ${type}: ${count}`);
|
|
45
|
+
});
|
|
46
|
+
console.log();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// Add account
|
|
50
|
+
accountCmd
|
|
51
|
+
.command("add <channelId> <channelType> <accountName>")
|
|
52
|
+
.description("Add a new channel account")
|
|
53
|
+
.option("--metadata <json>", "Additional metadata (JSON)")
|
|
54
|
+
.action(async (channelId, channelType, accountName, options) => {
|
|
55
|
+
console.log("🎱 Pool Bot - Adding Channel Account\n");
|
|
56
|
+
let metadata;
|
|
57
|
+
if (options.metadata) {
|
|
58
|
+
try {
|
|
59
|
+
metadata = JSON.parse(options.metadata);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
console.error("Error: Invalid JSON for metadata\n");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const account = await addChannelAccount({
|
|
67
|
+
channelId,
|
|
68
|
+
channelType,
|
|
69
|
+
accountName,
|
|
70
|
+
metadata,
|
|
71
|
+
});
|
|
72
|
+
console.log(`✅ Account added successfully!\n`);
|
|
73
|
+
console.log(`Account ID: ${account.id}`);
|
|
74
|
+
console.log(`Channel: ${account.channelId}`);
|
|
75
|
+
console.log(`Type: ${account.channelType}`);
|
|
76
|
+
console.log(`Name: ${account.accountName}`);
|
|
77
|
+
console.log(`Status: ${account.isActive ? "Active" : "Inactive"}\n`);
|
|
78
|
+
console.log(`💡 Use "poolbot channel-account switch ${channelId} ${account.id}" to activate\n`);
|
|
79
|
+
});
|
|
80
|
+
// Switch account
|
|
81
|
+
accountCmd
|
|
82
|
+
.command("switch <channelId> <accountId>")
|
|
83
|
+
.description("Switch active account for a channel")
|
|
84
|
+
.action(async (channelId, accountId) => {
|
|
85
|
+
console.log("🎱 Pool Bot - Switching Account\n");
|
|
86
|
+
const success = await setActiveChannelAccount(channelId, accountId);
|
|
87
|
+
if (success) {
|
|
88
|
+
console.log(`✅ Switched to account ${accountId}\n`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(`❌ Account not found: ${accountId}\n`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// Remove account
|
|
96
|
+
accountCmd
|
|
97
|
+
.command("remove <channelId> <accountId>")
|
|
98
|
+
.description("Remove a channel account")
|
|
99
|
+
.action(async (channelId, accountId) => {
|
|
100
|
+
console.log("🎱 Pool Bot - Removing Account\n");
|
|
101
|
+
const success = await removeChannelAccount(channelId, accountId);
|
|
102
|
+
if (success) {
|
|
103
|
+
console.log(`✅ Account removed successfully\n`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error(`❌ Account not found\n`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// Show account
|
|
111
|
+
accountCmd
|
|
112
|
+
.command("show <channelId> <accountId>")
|
|
113
|
+
.description("Show account details")
|
|
114
|
+
.action(async (channelId, accountId) => {
|
|
115
|
+
console.log(`🎱 Pool Bot - Account Details\n`);
|
|
116
|
+
const account = await getChannelAccount(channelId, accountId);
|
|
117
|
+
if (!account) {
|
|
118
|
+
console.error(`❌ Account not found\n`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
console.log(`Account ID: ${account.id}`);
|
|
122
|
+
console.log(`Channel: ${account.channelId}`);
|
|
123
|
+
console.log(`Type: ${account.channelType}`);
|
|
124
|
+
console.log(`Name: ${account.accountName}`);
|
|
125
|
+
console.log(`Status: ${account.isActive ? "Active 🟢" : "Inactive ⚪"}`);
|
|
126
|
+
console.log(`Created: ${new Date(account.createdAt).toLocaleString()}`);
|
|
127
|
+
console.log(`Updated: ${new Date(account.updatedAt).toLocaleString()}`);
|
|
128
|
+
console.log(`Last Used: ${account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleString() : "Never"}`);
|
|
129
|
+
if (account.metadata && Object.keys(account.metadata).length > 0) {
|
|
130
|
+
console.log("\nMetadata:");
|
|
131
|
+
Object.entries(account.metadata).forEach(([key, value]) => {
|
|
132
|
+
console.log(` ${key}: ${value}`);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
});
|
|
137
|
+
// Update account
|
|
138
|
+
accountCmd
|
|
139
|
+
.command("update <channelId> <accountId>")
|
|
140
|
+
.description("Update account details")
|
|
141
|
+
.option("--name <name>", "New account name")
|
|
142
|
+
.option("--metadata <json>", "New metadata (JSON, merges with existing)")
|
|
143
|
+
.action(async (channelId, accountId, options) => {
|
|
144
|
+
console.log("🎱 Pool Bot - Updating Account\n");
|
|
145
|
+
let metadata;
|
|
146
|
+
if (options.metadata) {
|
|
147
|
+
try {
|
|
148
|
+
metadata = JSON.parse(options.metadata);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
console.error("Error: Invalid JSON for metadata\n");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const account = await updateChannelAccount({
|
|
156
|
+
channelId,
|
|
157
|
+
accountId,
|
|
158
|
+
accountName: options.name,
|
|
159
|
+
metadata,
|
|
160
|
+
});
|
|
161
|
+
if (!account) {
|
|
162
|
+
console.error(`❌ Account not found\n`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
console.log(`✅ Account updated successfully\n`);
|
|
166
|
+
});
|
|
167
|
+
// Stats
|
|
168
|
+
accountCmd
|
|
169
|
+
.command("stats")
|
|
170
|
+
.description("Show account statistics")
|
|
171
|
+
.action(async () => {
|
|
172
|
+
console.log("🎱 Pool Bot - Account Statistics\n");
|
|
173
|
+
const stats = await getChannelAccountStats();
|
|
174
|
+
console.log("📊 Statistics:");
|
|
175
|
+
console.log(` Total Accounts: ${stats.totalAccounts}`);
|
|
176
|
+
console.log(` Channels with Accounts: ${stats.channelsWithAccounts}`);
|
|
177
|
+
console.log(` Active Accounts: ${stats.activeAccounts}`);
|
|
178
|
+
console.log("\nAccounts by Channel Type:");
|
|
179
|
+
if (Object.keys(stats.accountsByChannelType).length === 0) {
|
|
180
|
+
console.log(" No accounts yet");
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
Object.entries(stats.accountsByChannelType).forEach(([type, count]) => {
|
|
184
|
+
console.log(` ${type}: ${count}`);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
console.log();
|
|
188
|
+
});
|
|
189
|
+
return accountCmd;
|
|
190
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { formatCliCommand } from "../cli/command-format.js";
|
|
2
|
+
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
|
3
|
+
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
|
4
|
+
import { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
|
|
5
|
+
import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
|
|
6
|
+
import { resolveGatewayAuth } from "../gateway/auth.js";
|
|
7
|
+
import { readGatewayTokenEnv } from "../gateway/credentials.js";
|
|
8
|
+
import { secretRefKey } from "../secrets/ref-contract.js";
|
|
9
|
+
import { resolveSecretRefValues } from "../secrets/resolve.js";
|
|
10
|
+
import { randomToken } from "./onboard-helpers.js";
|
|
11
|
+
function formatAmbiguousGatewayAuthModeReason() {
|
|
12
|
+
return [
|
|
13
|
+
"gateway.auth.token and gateway.auth.password are both configured while gateway.auth.mode is unset.",
|
|
14
|
+
`Set ${formatCliCommand("openclaw config set gateway.auth.mode token")} or ${formatCliCommand("openclaw config set gateway.auth.mode password")}.`,
|
|
15
|
+
].join(" ");
|
|
16
|
+
}
|
|
17
|
+
export async function resolveGatewayInstallToken(options) {
|
|
18
|
+
const cfg = options.config;
|
|
19
|
+
const warnings = [];
|
|
20
|
+
const tokenRef = resolveSecretInputRef({
|
|
21
|
+
value: cfg.gateway?.auth?.token,
|
|
22
|
+
defaults: cfg.secrets?.defaults,
|
|
23
|
+
}).ref;
|
|
24
|
+
const tokenRefConfigured = Boolean(tokenRef);
|
|
25
|
+
const configToken = tokenRef || typeof cfg.gateway?.auth?.token !== "string"
|
|
26
|
+
? undefined
|
|
27
|
+
: cfg.gateway.auth.token.trim() || undefined;
|
|
28
|
+
const explicitToken = options.explicitToken?.trim() || undefined;
|
|
29
|
+
const envToken = readGatewayTokenEnv(options.env);
|
|
30
|
+
if (hasAmbiguousGatewayAuthModeConfig(cfg)) {
|
|
31
|
+
return {
|
|
32
|
+
token: undefined,
|
|
33
|
+
tokenRefConfigured,
|
|
34
|
+
unavailableReason: formatAmbiguousGatewayAuthModeReason(),
|
|
35
|
+
warnings,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const resolvedAuth = resolveGatewayAuth({
|
|
39
|
+
authConfig: cfg.gateway?.auth,
|
|
40
|
+
tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
|
|
41
|
+
});
|
|
42
|
+
const needsToken = shouldRequireGatewayTokenForInstall(cfg, options.env) && !resolvedAuth.allowTailscale;
|
|
43
|
+
let token = explicitToken || configToken || (tokenRef ? undefined : envToken);
|
|
44
|
+
let unavailableReason;
|
|
45
|
+
if (tokenRef && !token && needsToken) {
|
|
46
|
+
try {
|
|
47
|
+
const resolved = await resolveSecretRefValues([tokenRef], {
|
|
48
|
+
config: cfg,
|
|
49
|
+
env: options.env,
|
|
50
|
+
});
|
|
51
|
+
const value = resolved.get(secretRefKey(tokenRef));
|
|
52
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
53
|
+
throw new Error("gateway.auth.token resolved to an empty or non-string value.");
|
|
54
|
+
}
|
|
55
|
+
warnings.push("gateway.auth.token is SecretRef-managed; install will not persist a resolved token in service environment. Ensure the SecretRef is resolvable in the daemon runtime context.");
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
unavailableReason = `gateway.auth.token SecretRef is configured but unresolved (${String(err)}).`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const allowAutoGenerate = options.autoGenerateWhenMissing ?? false;
|
|
62
|
+
const persistGeneratedToken = options.persistGeneratedToken ?? false;
|
|
63
|
+
if (!token && needsToken && !tokenRef && allowAutoGenerate) {
|
|
64
|
+
token = randomToken();
|
|
65
|
+
warnings.push(persistGeneratedToken
|
|
66
|
+
? "No gateway token found. Auto-generated one and saving to config."
|
|
67
|
+
: "No gateway token found. Auto-generated one for this run without saving to config.");
|
|
68
|
+
if (persistGeneratedToken) {
|
|
69
|
+
// Persist token in config so daemon and CLI share a stable credential source.
|
|
70
|
+
try {
|
|
71
|
+
const snapshot = await readConfigFileSnapshot();
|
|
72
|
+
if (snapshot.exists && !snapshot.valid) {
|
|
73
|
+
warnings.push("Warning: config file exists but is invalid; skipping token persistence.");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const baseConfig = snapshot.exists ? snapshot.config : {};
|
|
77
|
+
const existingTokenRef = resolveSecretInputRef({
|
|
78
|
+
value: baseConfig.gateway?.auth?.token,
|
|
79
|
+
defaults: baseConfig.secrets?.defaults,
|
|
80
|
+
}).ref;
|
|
81
|
+
const baseConfigToken = existingTokenRef || typeof baseConfig.gateway?.auth?.token !== "string"
|
|
82
|
+
? undefined
|
|
83
|
+
: baseConfig.gateway.auth.token.trim() || undefined;
|
|
84
|
+
if (!existingTokenRef && !baseConfigToken) {
|
|
85
|
+
await writeConfigFile({
|
|
86
|
+
...baseConfig,
|
|
87
|
+
gateway: {
|
|
88
|
+
...baseConfig.gateway,
|
|
89
|
+
auth: {
|
|
90
|
+
...baseConfig.gateway?.auth,
|
|
91
|
+
mode: baseConfig.gateway?.auth?.mode ?? "token",
|
|
92
|
+
token,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else if (baseConfigToken) {
|
|
98
|
+
token = baseConfigToken;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
token = undefined;
|
|
102
|
+
warnings.push("Warning: gateway.auth.token is SecretRef-managed; skipping plaintext token persistence.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
warnings.push(`Warning: could not persist token to config: ${String(err)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
token,
|
|
113
|
+
tokenRefConfigured,
|
|
114
|
+
unavailableReason,
|
|
115
|
+
warnings,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { formatCliCommand } from "../cli/command-format.js";
|
|
3
|
+
import { note } from "../terminal/note.js";
|
|
4
|
+
const TLS_CERT_ERROR_CODES = new Set([
|
|
5
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
6
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
7
|
+
"CERT_HAS_EXPIRED",
|
|
8
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
9
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
10
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
11
|
+
]);
|
|
12
|
+
const TLS_CERT_ERROR_PATTERNS = [
|
|
13
|
+
/unable to get local issuer certificate/i,
|
|
14
|
+
/unable to verify the first certificate/i,
|
|
15
|
+
/self[- ]signed certificate/i,
|
|
16
|
+
/certificate has expired/i,
|
|
17
|
+
];
|
|
18
|
+
const OPENAI_AUTH_PROBE_URL = "https://auth.openai.com/oauth/authorize?response_type=code&client_id=openclaw-preflight&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email";
|
|
19
|
+
function asRecord(value) {
|
|
20
|
+
return value && typeof value === "object" ? value : null;
|
|
21
|
+
}
|
|
22
|
+
function extractFailure(error) {
|
|
23
|
+
const root = asRecord(error);
|
|
24
|
+
const rootCause = asRecord(root?.cause);
|
|
25
|
+
const code = typeof rootCause?.code === "string" ? rootCause.code : undefined;
|
|
26
|
+
const message = typeof rootCause?.message === "string"
|
|
27
|
+
? rootCause.message
|
|
28
|
+
: typeof root?.message === "string"
|
|
29
|
+
? root.message
|
|
30
|
+
: String(error);
|
|
31
|
+
const isTlsCertError = (code ? TLS_CERT_ERROR_CODES.has(code) : false) ||
|
|
32
|
+
TLS_CERT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
33
|
+
return {
|
|
34
|
+
code,
|
|
35
|
+
message,
|
|
36
|
+
kind: isTlsCertError ? "tls-cert" : "network",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveHomebrewPrefixFromExecPath(execPath) {
|
|
40
|
+
const marker = `${path.sep}Cellar${path.sep}`;
|
|
41
|
+
const idx = execPath.indexOf(marker);
|
|
42
|
+
if (idx > 0) {
|
|
43
|
+
return execPath.slice(0, idx);
|
|
44
|
+
}
|
|
45
|
+
const envPrefix = process.env.HOMEBREW_PREFIX?.trim();
|
|
46
|
+
return envPrefix ? envPrefix : null;
|
|
47
|
+
}
|
|
48
|
+
function resolveCertBundlePath() {
|
|
49
|
+
const prefix = resolveHomebrewPrefixFromExecPath(process.execPath);
|
|
50
|
+
if (!prefix) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return path.join(prefix, "etc", "openssl@3", "cert.pem");
|
|
54
|
+
}
|
|
55
|
+
function hasOpenAICodexOAuthProfile(cfg) {
|
|
56
|
+
const profiles = cfg.auth?.profiles;
|
|
57
|
+
if (!profiles) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return Object.values(profiles).some((profile) => profile.provider === "openai-codex" && profile.mode === "oauth");
|
|
61
|
+
}
|
|
62
|
+
function shouldRunOpenAIOAuthTlsPrerequisites(params) {
|
|
63
|
+
if (params.deep === true) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return hasOpenAICodexOAuthProfile(params.cfg);
|
|
67
|
+
}
|
|
68
|
+
export async function runOpenAIOAuthTlsPreflight(options) {
|
|
69
|
+
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
70
|
+
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
71
|
+
try {
|
|
72
|
+
await fetchImpl(OPENAI_AUTH_PROBE_URL, {
|
|
73
|
+
method: "GET",
|
|
74
|
+
redirect: "manual",
|
|
75
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
76
|
+
});
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const failure = extractFailure(error);
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
kind: failure.kind,
|
|
84
|
+
code: failure.code,
|
|
85
|
+
message: failure.message,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function formatOpenAIOAuthTlsPreflightFix(result) {
|
|
90
|
+
if (result.kind !== "tls-cert") {
|
|
91
|
+
return [
|
|
92
|
+
"OpenAI OAuth prerequisites check failed due to a network error before the browser flow.",
|
|
93
|
+
`Cause: ${result.message}`,
|
|
94
|
+
"Verify DNS/firewall/proxy access to auth.openai.com and retry.",
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
const certBundlePath = resolveCertBundlePath();
|
|
98
|
+
const lines = [
|
|
99
|
+
"OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
|
|
100
|
+
`Cause: ${result.code ? `${result.code} (${result.message})` : result.message}`,
|
|
101
|
+
"",
|
|
102
|
+
"Fix (Homebrew Node/OpenSSL):",
|
|
103
|
+
`- ${formatCliCommand("brew postinstall ca-certificates")}`,
|
|
104
|
+
`- ${formatCliCommand("brew postinstall openssl@3")}`,
|
|
105
|
+
];
|
|
106
|
+
if (certBundlePath) {
|
|
107
|
+
lines.push(`- Verify cert bundle exists: ${certBundlePath}`);
|
|
108
|
+
}
|
|
109
|
+
lines.push("- Retry the OAuth login flow.");
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
export async function noteOpenAIOAuthTlsPrerequisites(params) {
|
|
113
|
+
if (!shouldRunOpenAIOAuthTlsPrerequisites(params)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const result = await runOpenAIOAuthTlsPreflight({ timeoutMs: 4000 });
|
|
117
|
+
if (result.ok || result.kind !== "tls-cert") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
note(formatOpenAIOAuthTlsPreflightFix(result), "OAuth TLS prerequisites");
|
|
121
|
+
}
|