agent-relay 1.0.21 → 1.1.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/dist/bridge/shadow-cli.d.ts +17 -0
- package/dist/bridge/shadow-cli.d.ts.map +1 -0
- package/dist/bridge/shadow-cli.js +75 -0
- package/dist/bridge/shadow-cli.js.map +1 -0
- package/dist/bridge/shadow-config.d.ts +87 -0
- package/dist/bridge/shadow-config.d.ts.map +1 -0
- package/dist/bridge/shadow-config.js +134 -0
- package/dist/bridge/shadow-config.js.map +1 -0
- package/dist/bridge/spawner.d.ts +15 -1
- package/dist/bridge/spawner.d.ts.map +1 -1
- package/dist/bridge/spawner.js +164 -4
- package/dist/bridge/spawner.js.map +1 -1
- package/dist/bridge/types.d.ts +55 -0
- package/dist/bridge/types.d.ts.map +1 -1
- package/dist/cli/index.js +796 -11
- package/dist/cli/index.js.map +1 -1
- package/dist/cloud/api/auth.d.ts +19 -0
- package/dist/cloud/api/auth.d.ts.map +1 -0
- package/dist/cloud/api/auth.js +216 -0
- package/dist/cloud/api/auth.js.map +1 -0
- package/dist/cloud/api/billing.d.ts +17 -0
- package/dist/cloud/api/billing.d.ts.map +1 -0
- package/dist/cloud/api/billing.js +353 -0
- package/dist/cloud/api/billing.js.map +1 -0
- package/dist/cloud/api/coordinators.d.ts +8 -0
- package/dist/cloud/api/coordinators.d.ts.map +1 -0
- package/dist/cloud/api/coordinators.js +347 -0
- package/dist/cloud/api/coordinators.js.map +1 -0
- package/dist/cloud/api/daemons.d.ts +12 -0
- package/dist/cloud/api/daemons.d.ts.map +1 -0
- package/dist/cloud/api/daemons.js +320 -0
- package/dist/cloud/api/daemons.js.map +1 -0
- package/dist/cloud/api/middleware/planLimits.d.ts +36 -0
- package/dist/cloud/api/middleware/planLimits.d.ts.map +1 -0
- package/dist/cloud/api/middleware/planLimits.js +164 -0
- package/dist/cloud/api/middleware/planLimits.js.map +1 -0
- package/dist/cloud/api/onboarding.d.ts +8 -0
- package/dist/cloud/api/onboarding.d.ts.map +1 -0
- package/dist/cloud/api/onboarding.js +407 -0
- package/dist/cloud/api/onboarding.js.map +1 -0
- package/dist/cloud/api/providers.d.ts +7 -0
- package/dist/cloud/api/providers.d.ts.map +1 -0
- package/dist/cloud/api/providers.js +435 -0
- package/dist/cloud/api/providers.js.map +1 -0
- package/dist/cloud/api/repos.d.ts +7 -0
- package/dist/cloud/api/repos.d.ts.map +1 -0
- package/dist/cloud/api/repos.js +314 -0
- package/dist/cloud/api/repos.js.map +1 -0
- package/dist/cloud/api/teams.d.ts +7 -0
- package/dist/cloud/api/teams.d.ts.map +1 -0
- package/dist/cloud/api/teams.js +279 -0
- package/dist/cloud/api/teams.js.map +1 -0
- package/dist/cloud/api/usage.d.ts +7 -0
- package/dist/cloud/api/usage.d.ts.map +1 -0
- package/dist/cloud/api/usage.js +98 -0
- package/dist/cloud/api/usage.js.map +1 -0
- package/dist/cloud/api/workspaces.d.ts +7 -0
- package/dist/cloud/api/workspaces.d.ts.map +1 -0
- package/dist/cloud/api/workspaces.js +510 -0
- package/dist/cloud/api/workspaces.js.map +1 -0
- package/dist/cloud/billing/index.d.ts +9 -0
- package/dist/cloud/billing/index.d.ts.map +1 -0
- package/dist/cloud/billing/index.js +9 -0
- package/dist/cloud/billing/index.js.map +1 -0
- package/dist/cloud/billing/plans.d.ts +39 -0
- package/dist/cloud/billing/plans.d.ts.map +1 -0
- package/dist/cloud/billing/plans.js +232 -0
- package/dist/cloud/billing/plans.js.map +1 -0
- package/dist/cloud/billing/service.d.ts +80 -0
- package/dist/cloud/billing/service.d.ts.map +1 -0
- package/dist/cloud/billing/service.js +388 -0
- package/dist/cloud/billing/service.js.map +1 -0
- package/dist/cloud/billing/types.d.ts +135 -0
- package/dist/cloud/billing/types.d.ts.map +1 -0
- package/dist/cloud/billing/types.js +7 -0
- package/dist/cloud/billing/types.js.map +1 -0
- package/dist/cloud/config.d.ts +59 -0
- package/dist/cloud/config.d.ts.map +1 -0
- package/dist/cloud/config.js +83 -0
- package/dist/cloud/config.js.map +1 -0
- package/dist/cloud/db/drizzle.d.ts +132 -0
- package/dist/cloud/db/drizzle.d.ts.map +1 -0
- package/dist/cloud/db/drizzle.js +613 -0
- package/dist/cloud/db/drizzle.js.map +1 -0
- package/dist/cloud/db/index.d.ts +30 -0
- package/dist/cloud/db/index.d.ts.map +1 -0
- package/dist/cloud/db/index.js +44 -0
- package/dist/cloud/db/index.js.map +1 -0
- package/dist/cloud/db/schema.d.ts +1792 -0
- package/dist/cloud/db/schema.d.ts.map +1 -0
- package/dist/cloud/db/schema.js +234 -0
- package/dist/cloud/db/schema.js.map +1 -0
- package/dist/cloud/index.d.ts +11 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +37 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/cloud/provisioner/index.d.ts +51 -0
- package/dist/cloud/provisioner/index.d.ts.map +1 -0
- package/dist/cloud/provisioner/index.js +676 -0
- package/dist/cloud/provisioner/index.js.map +1 -0
- package/dist/cloud/server.d.ts +16 -0
- package/dist/cloud/server.d.ts.map +1 -0
- package/dist/cloud/server.js +190 -0
- package/dist/cloud/server.js.map +1 -0
- package/dist/cloud/services/coordinator.d.ts +62 -0
- package/dist/cloud/services/coordinator.d.ts.map +1 -0
- package/dist/cloud/services/coordinator.js +389 -0
- package/dist/cloud/services/coordinator.js.map +1 -0
- package/dist/cloud/services/planLimits.d.ts +110 -0
- package/dist/cloud/services/planLimits.d.ts.map +1 -0
- package/dist/cloud/services/planLimits.js +254 -0
- package/dist/cloud/services/planLimits.js.map +1 -0
- package/dist/cloud/vault/index.d.ts +76 -0
- package/dist/cloud/vault/index.d.ts.map +1 -0
- package/dist/cloud/vault/index.js +219 -0
- package/dist/cloud/vault/index.js.map +1 -0
- package/dist/daemon/agent-manager.d.ts +87 -0
- package/dist/daemon/agent-manager.d.ts.map +1 -0
- package/dist/daemon/agent-manager.js +412 -0
- package/dist/daemon/agent-manager.js.map +1 -0
- package/dist/daemon/agent-registry.d.ts +2 -0
- package/dist/daemon/agent-registry.d.ts.map +1 -1
- package/dist/daemon/agent-registry.js +3 -0
- package/dist/daemon/agent-registry.js.map +1 -1
- package/dist/daemon/api.d.ts +69 -0
- package/dist/daemon/api.d.ts.map +1 -0
- package/dist/daemon/api.js +425 -0
- package/dist/daemon/api.js.map +1 -0
- package/dist/daemon/cloud-sync.d.ts +101 -0
- package/dist/daemon/cloud-sync.d.ts.map +1 -0
- package/dist/daemon/cloud-sync.js +261 -0
- package/dist/daemon/cloud-sync.js.map +1 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +6 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/orchestrator.d.ts +155 -0
- package/dist/daemon/orchestrator.d.ts.map +1 -0
- package/dist/daemon/orchestrator.js +736 -0
- package/dist/daemon/orchestrator.js.map +1 -0
- package/dist/daemon/router.d.ts +24 -0
- package/dist/daemon/router.d.ts.map +1 -1
- package/dist/daemon/router.js +71 -1
- package/dist/daemon/router.js.map +1 -1
- package/dist/daemon/server.d.ts +37 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +191 -16
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/types.d.ts +127 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +6 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/daemon/workspace-manager.d.ts +75 -0
- package/dist/daemon/workspace-manager.d.ts.map +1 -0
- package/dist/daemon/workspace-manager.js +289 -0
- package/dist/daemon/workspace-manager.js.map +1 -0
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/_next/static/chunks/693-7b3301d8f6bc5014.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/713-f78477eb185f1f4d.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/766-e53e1cfe39b0b5b5.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/900-037c64bfd797fb2a.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/app/page-e3d9e1f4466b9bae.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/history/page-b6edd4dde8d08194.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-e68825a81db67ba1.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/page-cc108bf68c8a657f.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-d80e03a5297f95b6.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +1 -0
- package/dist/dashboard/out/_next/static/chunks/{main-e0a1f53fe0617a63.js → main-c2f423b9c9f4591b.js} +1 -1
- package/dist/dashboard/out/_next/static/chunks/{webpack-c81f7fd28659d64f.js → webpack-a5acc2831d094776.js} +1 -1
- package/dist/dashboard/out/_next/static/css/79b80143647a07d7.css +1 -0
- package/dist/dashboard/out/_next/static/css/8cf277370ad48cfe.css +1 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
- package/dist/dashboard/out/alt-logos/logo.svg +38 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
- package/dist/dashboard/out/app.html +14 -0
- package/dist/dashboard/out/app.txt +7 -0
- package/dist/dashboard/out/history.html +1 -0
- package/dist/dashboard/out/history.txt +7 -0
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +2 -2
- package/dist/dashboard/out/metrics.html +1 -515
- package/dist/dashboard/out/metrics.txt +2 -2
- package/dist/dashboard/out/pricing.html +13 -0
- package/dist/dashboard/out/pricing.txt +7 -0
- package/dist/dashboard-server/metrics.d.ts.map +1 -1
- package/dist/dashboard-server/metrics.js +3 -2
- package/dist/dashboard-server/metrics.js.map +1 -1
- package/dist/dashboard-server/server.d.ts.map +1 -1
- package/dist/dashboard-server/server.js +1279 -56
- package/dist/dashboard-server/server.js.map +1 -1
- package/dist/protocol/types.d.ts +10 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/resiliency/context-persistence.d.ts +140 -0
- package/dist/resiliency/context-persistence.d.ts.map +1 -0
- package/dist/resiliency/context-persistence.js +397 -0
- package/dist/resiliency/context-persistence.js.map +1 -0
- package/dist/resiliency/health-monitor.d.ts +97 -0
- package/dist/resiliency/health-monitor.d.ts.map +1 -0
- package/dist/resiliency/health-monitor.js +291 -0
- package/dist/resiliency/health-monitor.js.map +1 -0
- package/dist/resiliency/index.d.ts +63 -0
- package/dist/resiliency/index.d.ts.map +1 -0
- package/dist/resiliency/index.js +63 -0
- package/dist/resiliency/index.js.map +1 -0
- package/dist/resiliency/logger.d.ts +114 -0
- package/dist/resiliency/logger.d.ts.map +1 -0
- package/dist/resiliency/logger.js +250 -0
- package/dist/resiliency/logger.js.map +1 -0
- package/dist/resiliency/metrics.d.ts +115 -0
- package/dist/resiliency/metrics.d.ts.map +1 -0
- package/dist/resiliency/metrics.js +239 -0
- package/dist/resiliency/metrics.js.map +1 -0
- package/dist/resiliency/provider-context.d.ts +100 -0
- package/dist/resiliency/provider-context.d.ts.map +1 -0
- package/dist/resiliency/provider-context.js +360 -0
- package/dist/resiliency/provider-context.js.map +1 -0
- package/dist/resiliency/supervisor.d.ts +109 -0
- package/dist/resiliency/supervisor.d.ts.map +1 -0
- package/dist/resiliency/supervisor.js +337 -0
- package/dist/resiliency/supervisor.js.map +1 -0
- package/dist/storage/adapter.d.ts +2 -0
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/adapter.js +12 -2
- package/dist/storage/adapter.js.map +1 -1
- package/dist/storage/sqlite-adapter.d.ts.map +1 -1
- package/dist/storage/sqlite-adapter.js +18 -14
- package/dist/storage/sqlite-adapter.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/wrapper/client.d.ts +16 -1
- package/dist/wrapper/client.d.ts.map +1 -1
- package/dist/wrapper/client.js +32 -1
- package/dist/wrapper/client.js.map +1 -1
- package/dist/wrapper/parser.d.ts +3 -0
- package/dist/wrapper/parser.d.ts.map +1 -1
- package/dist/wrapper/parser.js +121 -18
- package/dist/wrapper/parser.js.map +1 -1
- package/dist/wrapper/pty-wrapper.d.ts +28 -1
- package/dist/wrapper/pty-wrapper.d.ts.map +1 -1
- package/dist/wrapper/pty-wrapper.js +166 -30
- package/dist/wrapper/pty-wrapper.js.map +1 -1
- package/dist/wrapper/tmux-wrapper.d.ts +5 -0
- package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
- package/dist/wrapper/tmux-wrapper.js +58 -18
- package/dist/wrapper/tmux-wrapper.js.map +1 -1
- package/docs/CLOUD-ARCHITECTURE.md +652 -0
- package/docs/CLOUD-ONBOARDING-DESIGN.md +1983 -0
- package/docs/TESTING_PRESENCE_FEATURES.md +327 -0
- package/docs/agent-relay-snippet.md +107 -4
- package/docs/guides/CLOUD.md +236 -0
- package/docs/guides/LOCAL.md +535 -0
- package/docs/guides/SELF-HOSTED.md +494 -0
- package/docs/proposals/shadow-as-subagent.md +765 -0
- package/docs/proposals/slack-bot-integration.md +1457 -0
- package/package.json +33 -4
- package/dist/dashboard/out/_next/static/chunks/app/layout-c9d8c5d95e48c6bf.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-8aa9936bc6c771ab.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-49055e5d2b5e34ec.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-bae2e535de00de50.js +0 -1
- package/dist/dashboard/out/_next/static/css/50ed6996e3df7bdd.css +0 -1
- /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{gZXwjIKGDKJ0hiTH-HMeJ → 6HHWb2ZmnJ4OSm0zUP7h4}/_ssgManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{117-3bef7b19f3e60751.js → 117-b2cd8d6485aacf2b.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{648-6cf686106c891ad3.js → 648-8f3f26864ce515e5.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/app/_not-found/{page-8ff6572bc7c9bc61.js → page-0b990dbb71d72a98.js} +0 -0
- /package/dist/dashboard/out/_next/static/chunks/{fd9d1056-26bd8d656b496dba.js → fd9d1056-bf46c09eb57e019c.js} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -17,6 +17,8 @@ import { RelayClient } from '../wrapper/client.js';
|
|
|
17
17
|
import { generateAgentName } from '../utils/name-generator.js';
|
|
18
18
|
import { getTmuxPath } from '../utils/tmux-resolver.js';
|
|
19
19
|
import { readWorkersMetadata, getWorkerLogsDir } from '../bridge/spawner.js';
|
|
20
|
+
import { getShadowForAgent } from '../bridge/shadow-config.js';
|
|
21
|
+
import { selectShadowCli } from '../bridge/shadow-cli.js';
|
|
20
22
|
import { checkForUpdatesInBackground, checkForUpdates } from '../utils/update-checker.js';
|
|
21
23
|
import fs from 'node:fs';
|
|
22
24
|
import path from 'node:path';
|
|
@@ -53,6 +55,8 @@ program
|
|
|
53
55
|
.option('-n, --name <name>', 'Agent name (auto-generated if not set)')
|
|
54
56
|
.option('-q, --quiet', 'Disable debug output', false)
|
|
55
57
|
.option('--prefix <pattern>', 'Relay prefix pattern (default: ->relay:)')
|
|
58
|
+
.option('--shadow <name>', 'Spawn a shadow agent with this name that monitors the primary')
|
|
59
|
+
.option('--shadow-role <role>', 'Shadow role: reviewer, auditor, or triggers (comma-separated: SESSION_END,CODE_WRITTEN,REVIEW_REQUEST,EXPLICIT_ASK,ALL_MESSAGES)')
|
|
56
60
|
.argument('[command...]', 'Command to wrap (e.g., claude)')
|
|
57
61
|
.action(async (commandParts, options) => {
|
|
58
62
|
// If no command provided, show help
|
|
@@ -125,6 +129,76 @@ program
|
|
|
125
129
|
process.exit(0);
|
|
126
130
|
});
|
|
127
131
|
await wrapper.start();
|
|
132
|
+
let shadowName;
|
|
133
|
+
let shadowRole;
|
|
134
|
+
let speakOn;
|
|
135
|
+
let shadowCli;
|
|
136
|
+
let shadowPrompt;
|
|
137
|
+
const rolePresets = {
|
|
138
|
+
reviewer: ['CODE_WRITTEN', 'REVIEW_REQUEST', 'EXPLICIT_ASK'],
|
|
139
|
+
auditor: ['SESSION_END', 'EXPLICIT_ASK'],
|
|
140
|
+
active: ['ALL_MESSAGES'],
|
|
141
|
+
};
|
|
142
|
+
if (options.shadow) {
|
|
143
|
+
// CLI flags provided
|
|
144
|
+
shadowName = options.shadow;
|
|
145
|
+
const role = options.shadowRole || 'EXPLICIT_ASK';
|
|
146
|
+
shadowRole = role;
|
|
147
|
+
if (rolePresets[role.toLowerCase()]) {
|
|
148
|
+
speakOn = rolePresets[role.toLowerCase()];
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
speakOn = role.split(',').map((s) => s.trim().toUpperCase());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Check config file for shadow configuration
|
|
156
|
+
const shadowConfig = getShadowForAgent(paths.projectRoot, agentName);
|
|
157
|
+
if (shadowConfig) {
|
|
158
|
+
shadowName = shadowConfig.shadowName;
|
|
159
|
+
shadowRole = shadowConfig.roleName;
|
|
160
|
+
speakOn = shadowConfig.speakOn;
|
|
161
|
+
shadowCli = shadowConfig.cli;
|
|
162
|
+
shadowPrompt = shadowConfig.prompt;
|
|
163
|
+
console.error(`Shadow config: ${shadowName} (from .agent-relay.json)`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Spawn shadow if configured
|
|
167
|
+
if (shadowName && speakOn) {
|
|
168
|
+
// Decide how to run the shadow (subagent for Claude/OpenCode primaries)
|
|
169
|
+
let shadowSelection = null;
|
|
170
|
+
try {
|
|
171
|
+
shadowSelection = await selectShadowCli(mainCommand, { preferredShadowCli: shadowCli });
|
|
172
|
+
console.error(`[shadow] Mode: ${shadowSelection.mode} via ${shadowSelection.command || shadowSelection.cli} (primary: ${mainCommand})`);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.error(`[shadow] Shadow CLI selection failed: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
// Subagent mode: do not spawn a separate shadow process
|
|
178
|
+
if (shadowSelection?.mode === 'subagent') {
|
|
179
|
+
console.error(`[shadow] ${shadowName} will run as ${shadowSelection.cli} subagent inside ${agentName}; no separate process spawned`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.error(`Shadow: ${shadowName} (shadowing ${agentName}, speakOn: ${speakOn.join(',')})`);
|
|
183
|
+
// Wait for primary to register before spawning shadow
|
|
184
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
185
|
+
// Build shadow task prompt
|
|
186
|
+
const defaultPrompt = `You are a shadow agent monitoring "${agentName}". You receive copies of their messages. Your role: ${shadowRole || 'observer'}. Stay passive unless your triggers activate.`;
|
|
187
|
+
const shadowTask = shadowPrompt || defaultPrompt;
|
|
188
|
+
const result = await spawner.spawn({
|
|
189
|
+
name: shadowName,
|
|
190
|
+
cli: shadowSelection?.command || shadowCli || mainCommand,
|
|
191
|
+
task: shadowTask,
|
|
192
|
+
shadowOf: agentName,
|
|
193
|
+
shadowSpeakOn: speakOn,
|
|
194
|
+
});
|
|
195
|
+
if (result.success) {
|
|
196
|
+
console.error(`Shadow ${shadowName} started [pid: ${result.pid}]`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.error(`Failed to spawn shadow ${shadowName}: ${result.error}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
128
202
|
});
|
|
129
203
|
// up - Start daemon + dashboard
|
|
130
204
|
program
|
|
@@ -134,7 +208,67 @@ program
|
|
|
134
208
|
.option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
|
|
135
209
|
.option('--spawn', 'Force spawn all agents from teams.json')
|
|
136
210
|
.option('--no-spawn', 'Do not auto-spawn agents (just start daemon)')
|
|
211
|
+
.option('--watch', 'Auto-restart daemon on crash (supervisor mode)')
|
|
212
|
+
.option('--max-restarts <n>', 'Max restarts in 60s before giving up (default: 5)', '5')
|
|
137
213
|
.action(async (options) => {
|
|
214
|
+
// If --watch is specified, run in supervisor mode
|
|
215
|
+
if (options.watch) {
|
|
216
|
+
const { spawn } = await import('node:child_process');
|
|
217
|
+
const maxRestarts = parseInt(options.maxRestarts, 10) || 5;
|
|
218
|
+
const restartWindow = 60_000; // 60 seconds
|
|
219
|
+
const restartTimes = [];
|
|
220
|
+
let child = null;
|
|
221
|
+
let shuttingDown = false;
|
|
222
|
+
const startDaemon = () => {
|
|
223
|
+
// Build args without --watch to prevent infinite recursion
|
|
224
|
+
const args = ['up'];
|
|
225
|
+
if (options.dashboard === false)
|
|
226
|
+
args.push('--no-dashboard');
|
|
227
|
+
if (options.port)
|
|
228
|
+
args.push('--port', options.port);
|
|
229
|
+
if (options.spawn === true)
|
|
230
|
+
args.push('--spawn');
|
|
231
|
+
if (options.spawn === false)
|
|
232
|
+
args.push('--no-spawn');
|
|
233
|
+
console.log(`[supervisor] Starting daemon...`);
|
|
234
|
+
child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
235
|
+
stdio: 'inherit',
|
|
236
|
+
env: { ...process.env, AGENT_RELAY_SUPERVISED: '1' },
|
|
237
|
+
});
|
|
238
|
+
child.on('exit', (code, signal) => {
|
|
239
|
+
if (shuttingDown) {
|
|
240
|
+
process.exit(0);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
restartTimes.push(now);
|
|
245
|
+
// Remove restarts outside the window
|
|
246
|
+
while (restartTimes.length > 0 && restartTimes[0] < now - restartWindow) {
|
|
247
|
+
restartTimes.shift();
|
|
248
|
+
}
|
|
249
|
+
if (restartTimes.length >= maxRestarts) {
|
|
250
|
+
console.error(`[supervisor] Daemon crashed ${maxRestarts} times in ${restartWindow / 1000}s, giving up`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const exitReason = signal ? `signal ${signal}` : `code ${code}`;
|
|
254
|
+
console.log(`[supervisor] Daemon exited (${exitReason}), restarting in 2s... (${restartTimes.length}/${maxRestarts} restarts)`);
|
|
255
|
+
setTimeout(startDaemon, 2000);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
process.on('SIGINT', () => {
|
|
259
|
+
console.log('\n[supervisor] Stopping...');
|
|
260
|
+
shuttingDown = true;
|
|
261
|
+
if (child)
|
|
262
|
+
child.kill('SIGINT');
|
|
263
|
+
});
|
|
264
|
+
process.on('SIGTERM', () => {
|
|
265
|
+
shuttingDown = true;
|
|
266
|
+
if (child)
|
|
267
|
+
child.kill('SIGTERM');
|
|
268
|
+
});
|
|
269
|
+
startDaemon();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
138
272
|
const { ensureProjectDir } = await import('../utils/project-namespace.js');
|
|
139
273
|
const { loadTeamsConfig } = await import('../bridge/teams-config.js');
|
|
140
274
|
const { AgentSpawner } = await import('../bridge/spawner.js');
|
|
@@ -157,6 +291,34 @@ program
|
|
|
157
291
|
});
|
|
158
292
|
// Create spawner for auto-spawn (will be initialized after dashboard starts)
|
|
159
293
|
let spawner = null;
|
|
294
|
+
// Track if we're already shutting down to prevent double-cleanup
|
|
295
|
+
let isShuttingDown = false;
|
|
296
|
+
const gracefulShutdown = async (reason) => {
|
|
297
|
+
if (isShuttingDown)
|
|
298
|
+
return;
|
|
299
|
+
isShuttingDown = true;
|
|
300
|
+
console.log(`\n[daemon] ${reason}, shutting down...`);
|
|
301
|
+
try {
|
|
302
|
+
if (spawner)
|
|
303
|
+
await spawner.releaseAll();
|
|
304
|
+
await daemon.stop();
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
console.error('[daemon] Error during shutdown:', err);
|
|
308
|
+
}
|
|
309
|
+
process.exit(1);
|
|
310
|
+
};
|
|
311
|
+
// Handle uncaught exceptions - log and exit (supervisor will restart)
|
|
312
|
+
process.on('uncaughtException', (err) => {
|
|
313
|
+
console.error('[daemon] Uncaught exception:', err);
|
|
314
|
+
gracefulShutdown('Uncaught exception');
|
|
315
|
+
});
|
|
316
|
+
// Handle unhandled promise rejections
|
|
317
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
318
|
+
console.error('[daemon] Unhandled rejection at:', promise, 'reason:', reason);
|
|
319
|
+
// Don't exit on unhandled rejections - just log them
|
|
320
|
+
// Most are recoverable (e.g., failed message delivery)
|
|
321
|
+
});
|
|
160
322
|
process.on('SIGINT', async () => {
|
|
161
323
|
console.log('\nStopping...');
|
|
162
324
|
if (spawner) {
|
|
@@ -189,6 +351,13 @@ program
|
|
|
189
351
|
projectRoot: paths.projectRoot,
|
|
190
352
|
});
|
|
191
353
|
console.log(`Dashboard: http://localhost:${dashboardPort}`);
|
|
354
|
+
// Hook daemon log output to dashboard WebSocket
|
|
355
|
+
daemon.onLogOutput = (agentName, data, _timestamp) => {
|
|
356
|
+
const broadcast = global.__broadcastLogOutput;
|
|
357
|
+
if (broadcast) {
|
|
358
|
+
broadcast(agentName, data);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
192
361
|
}
|
|
193
362
|
// Determine if we should auto-spawn agents
|
|
194
363
|
// --spawn: force spawn
|
|
@@ -288,9 +457,11 @@ program
|
|
|
288
457
|
.command('agents')
|
|
289
458
|
.description('List connected agents and spawned workers')
|
|
290
459
|
.option('--all', 'Include internal/CLI agents')
|
|
460
|
+
.option('--remote', 'Include agents from other linked machines (requires cloud link)')
|
|
291
461
|
.option('--json', 'Output as JSON')
|
|
292
462
|
.action(async (options) => {
|
|
293
463
|
const { getProjectPaths } = await import('../utils/project-namespace.js');
|
|
464
|
+
const os = await import('node:os');
|
|
294
465
|
const paths = getProjectPaths();
|
|
295
466
|
const agentsPath = path.join(paths.teamDir, 'agents.json');
|
|
296
467
|
// Load registered agents
|
|
@@ -311,6 +482,7 @@ program
|
|
|
311
482
|
lastSeen: agent.lastSeen,
|
|
312
483
|
team: worker?.team,
|
|
313
484
|
pid: worker?.pid,
|
|
485
|
+
location: 'local',
|
|
314
486
|
});
|
|
315
487
|
});
|
|
316
488
|
// Add workers not in registry (orphaned or not yet registered)
|
|
@@ -323,9 +495,51 @@ program
|
|
|
323
495
|
cli: worker.cli || '-',
|
|
324
496
|
team: worker.team,
|
|
325
497
|
pid: worker.pid,
|
|
498
|
+
location: 'local',
|
|
326
499
|
});
|
|
327
500
|
}
|
|
328
501
|
});
|
|
502
|
+
// Include remote agents if --remote flag is set
|
|
503
|
+
if (options.remote) {
|
|
504
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
505
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
506
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
507
|
+
if (fs.existsSync(configPath)) {
|
|
508
|
+
try {
|
|
509
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
510
|
+
const response = await fetch(`${config.cloudUrl}/api/daemons/agents`, {
|
|
511
|
+
method: 'POST',
|
|
512
|
+
headers: {
|
|
513
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
514
|
+
'Content-Type': 'application/json',
|
|
515
|
+
},
|
|
516
|
+
body: JSON.stringify({ agents: [] }),
|
|
517
|
+
});
|
|
518
|
+
if (response.ok) {
|
|
519
|
+
const data = await response.json();
|
|
520
|
+
// Add remote agents (exclude local ones by name)
|
|
521
|
+
const localNames = new Set(combined.map(a => a.name));
|
|
522
|
+
for (const agent of data.allAgents) {
|
|
523
|
+
if (!localNames.has(agent.name)) {
|
|
524
|
+
combined.push({
|
|
525
|
+
name: agent.name,
|
|
526
|
+
status: agent.status.toUpperCase(),
|
|
527
|
+
cli: '-',
|
|
528
|
+
location: agent.daemonName,
|
|
529
|
+
daemonId: agent.daemonId,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
console.error('[warn] Failed to fetch remote agents:', err.message);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
console.error('[warn] Cloud not linked. Run `agent-relay cloud link` to see remote agents.');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
329
543
|
if (options.json) {
|
|
330
544
|
console.log(JSON.stringify(combined, null, 2));
|
|
331
545
|
return;
|
|
@@ -335,21 +549,39 @@ program
|
|
|
335
549
|
console.log(`No agents found. Ensure the daemon is running and agents are connected${hint}.`);
|
|
336
550
|
return;
|
|
337
551
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
552
|
+
const hasRemote = combined.some(a => a.location !== 'local');
|
|
553
|
+
if (hasRemote) {
|
|
554
|
+
console.log('NAME STATUS CLI LOCATION');
|
|
555
|
+
console.log('─'.repeat(55));
|
|
556
|
+
combined.forEach((agent) => {
|
|
557
|
+
const name = agent.name.padEnd(15);
|
|
558
|
+
const status = agent.status.padEnd(8);
|
|
559
|
+
const cli = agent.cli.padEnd(9);
|
|
560
|
+
const location = agent.location ?? 'local';
|
|
561
|
+
console.log(`${name} ${status} ${cli} ${location}`);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
console.log('NAME STATUS CLI TEAM');
|
|
566
|
+
console.log('─'.repeat(50));
|
|
567
|
+
combined.forEach((agent) => {
|
|
568
|
+
const name = agent.name.padEnd(15);
|
|
569
|
+
const status = agent.status.padEnd(8);
|
|
570
|
+
const cli = agent.cli.padEnd(9);
|
|
571
|
+
const team = agent.team ?? '-';
|
|
572
|
+
console.log(`${name} ${status} ${cli} ${team}`);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
347
575
|
if (workers.length > 0) {
|
|
348
576
|
console.log('');
|
|
349
577
|
console.log('Commands:');
|
|
350
578
|
console.log(' agent-relay agents:logs <name> - View spawned agent output');
|
|
351
579
|
console.log(' agent-relay agents:kill <name> - Kill a spawned agent');
|
|
352
580
|
}
|
|
581
|
+
if (!options.remote) {
|
|
582
|
+
console.log('');
|
|
583
|
+
console.log('Tip: Use --remote to include agents from other linked machines.');
|
|
584
|
+
}
|
|
353
585
|
});
|
|
354
586
|
// who - Show currently active agents (online within last 30s)
|
|
355
587
|
program
|
|
@@ -541,6 +773,7 @@ program
|
|
|
541
773
|
.description('Bridge multiple projects as orchestrator')
|
|
542
774
|
.argument('[projects...]', 'Project paths to bridge')
|
|
543
775
|
.option('--cli <tool>', 'CLI tool override for all projects')
|
|
776
|
+
.option('--architect [cli]', 'Spawn an architect agent to coordinate all projects (default: claude)')
|
|
544
777
|
.action(async (projectPaths, options) => {
|
|
545
778
|
const { resolveProjects, validateDaemons } = await import('../bridge/config.js');
|
|
546
779
|
const { MultiProjectClient } = await import('../bridge/multi-project-client.js');
|
|
@@ -646,9 +879,106 @@ program
|
|
|
646
879
|
console.log('Connected to all projects.');
|
|
647
880
|
console.log('');
|
|
648
881
|
console.log('Cross-project messaging:');
|
|
649
|
-
console.log('
|
|
650
|
-
console.log('
|
|
882
|
+
console.log(' ->relay:projectId:agent Message');
|
|
883
|
+
console.log(' ->relay:*:lead Broadcast to all leads');
|
|
651
884
|
console.log('');
|
|
885
|
+
// Spawn architect agent if --architect flag is set
|
|
886
|
+
let architectWrapper = null;
|
|
887
|
+
if (options.architect !== undefined) {
|
|
888
|
+
const { TmuxWrapper } = await import('../wrapper/tmux-wrapper.js');
|
|
889
|
+
// Determine CLI to use (default to claude)
|
|
890
|
+
const architectCli = typeof options.architect === 'string' ? options.architect : 'claude';
|
|
891
|
+
// Use first project as the base for the architect
|
|
892
|
+
const baseProject = valid[0];
|
|
893
|
+
const basePaths = getProjectPaths(baseProject.path);
|
|
894
|
+
// Build project context for the architect
|
|
895
|
+
const projectContext = valid.map(p => `- ${p.id}: ${p.path} (Lead: ${p.leadName})`).join('\n');
|
|
896
|
+
// Create architect system prompt
|
|
897
|
+
const architectPrompt = `You are the Architect, a cross-project coordinator overseeing multiple codebases.
|
|
898
|
+
|
|
899
|
+
## Connected Projects
|
|
900
|
+
${projectContext}
|
|
901
|
+
|
|
902
|
+
## Your Role
|
|
903
|
+
- Coordinate high-level work across all projects
|
|
904
|
+
- Assign tasks to project leads
|
|
905
|
+
- Ensure consistency and resolve cross-project dependencies
|
|
906
|
+
- Review overall architecture decisions
|
|
907
|
+
|
|
908
|
+
## Cross-Project Messaging
|
|
909
|
+
|
|
910
|
+
Use this syntax to message agents in specific projects:
|
|
911
|
+
|
|
912
|
+
\`\`\`
|
|
913
|
+
->relay:${valid[0].id}:${valid[0].leadName} <<<
|
|
914
|
+
Your message to this project's lead>>>
|
|
915
|
+
|
|
916
|
+
->relay:${valid.length > 1 ? valid[1].id : valid[0].id}:* <<<
|
|
917
|
+
Broadcast to all agents in a project>>>
|
|
918
|
+
|
|
919
|
+
->relay:*:* <<<
|
|
920
|
+
Broadcast to ALL agents in ALL projects>>>
|
|
921
|
+
\`\`\`
|
|
922
|
+
|
|
923
|
+
Format: \`->relay:project-id:agent-name\`
|
|
924
|
+
|
|
925
|
+
## Getting Started
|
|
926
|
+
1. Check in with each project lead to understand current status
|
|
927
|
+
2. Identify cross-project dependencies
|
|
928
|
+
3. Coordinate work across teams
|
|
929
|
+
|
|
930
|
+
Start by greeting the project leads and asking for status updates.`;
|
|
931
|
+
console.log('Spawning Architect agent...');
|
|
932
|
+
console.log(` CLI: ${architectCli}`);
|
|
933
|
+
console.log(` Base project: ${baseProject.path}`);
|
|
934
|
+
console.log('');
|
|
935
|
+
// Determine command and args based on CLI
|
|
936
|
+
let command;
|
|
937
|
+
let args = [];
|
|
938
|
+
if (architectCli === 'claude' || architectCli.startsWith('claude:')) {
|
|
939
|
+
command = 'claude';
|
|
940
|
+
args = ['--dangerously-skip-permissions'];
|
|
941
|
+
// Add model if specified (e.g., claude:opus)
|
|
942
|
+
if (architectCli.includes(':')) {
|
|
943
|
+
const model = architectCli.split(':')[1];
|
|
944
|
+
args.push('--model', model);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
else if (architectCli === 'codex') {
|
|
948
|
+
command = 'codex';
|
|
949
|
+
args = ['--dangerously-skip-permissions'];
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
command = architectCli;
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
architectWrapper = new TmuxWrapper({
|
|
956
|
+
name: 'Architect',
|
|
957
|
+
command,
|
|
958
|
+
args,
|
|
959
|
+
socketPath: basePaths.socketPath,
|
|
960
|
+
debug: false,
|
|
961
|
+
useInbox: true,
|
|
962
|
+
inboxDir: basePaths.dataDir,
|
|
963
|
+
});
|
|
964
|
+
await architectWrapper.start();
|
|
965
|
+
// Wait for agent to be ready, then inject the prompt
|
|
966
|
+
setTimeout(async () => {
|
|
967
|
+
try {
|
|
968
|
+
await architectWrapper.injectMessage(architectPrompt);
|
|
969
|
+
console.log('Architect agent started and initialized.');
|
|
970
|
+
console.log('Attach to session: tmux attach -t relay-Architect');
|
|
971
|
+
console.log('');
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
console.error('Failed to inject architect prompt:', err);
|
|
975
|
+
}
|
|
976
|
+
}, 3000);
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
console.error('Failed to spawn Architect agent:', err);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
652
982
|
// Handle messages from projects
|
|
653
983
|
client.onMessage = (projectId, from, payload, messageId) => {
|
|
654
984
|
console.log(`[${projectId}] ${from}: ${payload.body.substring(0, 80)}...`);
|
|
@@ -985,6 +1315,40 @@ program
|
|
|
985
1315
|
}
|
|
986
1316
|
}
|
|
987
1317
|
});
|
|
1318
|
+
// release - Release a spawned agent via API (works from any context, no terminal required)
|
|
1319
|
+
program
|
|
1320
|
+
.command('release')
|
|
1321
|
+
.description('Release a spawned agent via API (no terminal required)')
|
|
1322
|
+
.argument('<name>', 'Agent name to release')
|
|
1323
|
+
.option('--port <port>', 'Dashboard port', DEFAULT_DASHBOARD_PORT)
|
|
1324
|
+
.action(async (name, options) => {
|
|
1325
|
+
const port = options.port || DEFAULT_DASHBOARD_PORT;
|
|
1326
|
+
try {
|
|
1327
|
+
const response = await fetch(`http://localhost:${port}/api/spawned/${encodeURIComponent(name)}`, {
|
|
1328
|
+
method: 'DELETE',
|
|
1329
|
+
});
|
|
1330
|
+
const result = await response.json();
|
|
1331
|
+
if (result.success) {
|
|
1332
|
+
console.log(`Released agent: ${name}`);
|
|
1333
|
+
process.exit(0);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
console.error(`Failed to release ${name}: ${result.error || 'Unknown error'}`);
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
catch (err) {
|
|
1341
|
+
// If API call fails, try to provide helpful error message
|
|
1342
|
+
if (err.code === 'ECONNREFUSED') {
|
|
1343
|
+
console.error(`Cannot connect to dashboard at port ${port}. Is the daemon running?`);
|
|
1344
|
+
console.log(`Run 'agent-relay up' to start the daemon.`);
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
console.error(`Failed to release ${name}: ${err.message}`);
|
|
1348
|
+
}
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
988
1352
|
// agents:kill - Kill a spawned agent by PID
|
|
989
1353
|
program
|
|
990
1354
|
.command('agents:kill')
|
|
@@ -1039,5 +1403,426 @@ program
|
|
|
1039
1403
|
}
|
|
1040
1404
|
}
|
|
1041
1405
|
});
|
|
1406
|
+
// ============================================================================
|
|
1407
|
+
// Cloud commands
|
|
1408
|
+
// ============================================================================
|
|
1409
|
+
const cloudCommand = program
|
|
1410
|
+
.command('cloud')
|
|
1411
|
+
.description('Cloud account and sync commands');
|
|
1412
|
+
cloudCommand
|
|
1413
|
+
.command('link')
|
|
1414
|
+
.description('Link this machine to your Agent Relay Cloud account')
|
|
1415
|
+
.option('--name <name>', 'Name for this machine')
|
|
1416
|
+
.option('--cloud-url <url>', 'Cloud API URL', process.env.AGENT_RELAY_CLOUD_URL || 'https://api.agent-relay.com')
|
|
1417
|
+
.action(async (options) => {
|
|
1418
|
+
const os = await import('node:os');
|
|
1419
|
+
const crypto = await import('node:crypto');
|
|
1420
|
+
const readline = await import('node:readline');
|
|
1421
|
+
const cloudUrl = options.cloudUrl;
|
|
1422
|
+
const machineName = options.name || os.hostname();
|
|
1423
|
+
// Generate machine ID
|
|
1424
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
1425
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
1426
|
+
const machineIdPath = path.join(dataDir, 'machine-id');
|
|
1427
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
1428
|
+
let machineId;
|
|
1429
|
+
if (fs.existsSync(machineIdPath)) {
|
|
1430
|
+
machineId = fs.readFileSync(machineIdPath, 'utf-8').trim();
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
machineId = `${os.hostname()}-${crypto.randomBytes(8).toString('hex')}`;
|
|
1434
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
1435
|
+
fs.writeFileSync(machineIdPath, machineId);
|
|
1436
|
+
}
|
|
1437
|
+
console.log('');
|
|
1438
|
+
console.log('🔗 Agent Relay Cloud - Link Machine');
|
|
1439
|
+
console.log('');
|
|
1440
|
+
console.log(`Machine: ${machineName}`);
|
|
1441
|
+
console.log(`ID: ${machineId}`);
|
|
1442
|
+
console.log('');
|
|
1443
|
+
// Generate a temporary code for the browser auth flow
|
|
1444
|
+
const tempCode = crypto.randomBytes(16).toString('hex');
|
|
1445
|
+
// Store temp code for callback
|
|
1446
|
+
const tempCodePath = path.join(dataDir, '.link-code');
|
|
1447
|
+
fs.writeFileSync(tempCodePath, tempCode);
|
|
1448
|
+
const authUrl = `${cloudUrl.replace('/api', '')}/cloud/link?code=${tempCode}&machine=${encodeURIComponent(machineId)}&name=${encodeURIComponent(machineName)}`;
|
|
1449
|
+
console.log('Open this URL in your browser to authenticate:');
|
|
1450
|
+
console.log('');
|
|
1451
|
+
console.log(` ${authUrl}`);
|
|
1452
|
+
console.log('');
|
|
1453
|
+
// Try to open browser automatically
|
|
1454
|
+
try {
|
|
1455
|
+
const openCommand = process.platform === 'darwin' ? 'open' :
|
|
1456
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1457
|
+
await execAsync(`${openCommand} "${authUrl}"`);
|
|
1458
|
+
console.log('(Browser opened automatically)');
|
|
1459
|
+
}
|
|
1460
|
+
catch {
|
|
1461
|
+
console.log('(Copy the URL above and paste it in your browser)');
|
|
1462
|
+
}
|
|
1463
|
+
console.log('');
|
|
1464
|
+
console.log('After authenticating, paste your API key here:');
|
|
1465
|
+
const rl = readline.createInterface({
|
|
1466
|
+
input: process.stdin,
|
|
1467
|
+
output: process.stdout,
|
|
1468
|
+
});
|
|
1469
|
+
const apiKey = await new Promise((resolve) => {
|
|
1470
|
+
rl.question('API Key: ', (answer) => {
|
|
1471
|
+
rl.close();
|
|
1472
|
+
resolve(answer.trim());
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
if (!apiKey || !apiKey.startsWith('ar_live_')) {
|
|
1476
|
+
console.error('');
|
|
1477
|
+
console.error('Invalid API key format. Expected ar_live_...');
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
// Verify the API key works
|
|
1481
|
+
console.log('');
|
|
1482
|
+
console.log('Verifying API key...');
|
|
1483
|
+
try {
|
|
1484
|
+
const response = await fetch(`${cloudUrl}/api/daemons/heartbeat`, {
|
|
1485
|
+
method: 'POST',
|
|
1486
|
+
headers: {
|
|
1487
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1488
|
+
'Content-Type': 'application/json',
|
|
1489
|
+
},
|
|
1490
|
+
body: JSON.stringify({
|
|
1491
|
+
agents: [],
|
|
1492
|
+
metrics: { linkedAt: new Date().toISOString() },
|
|
1493
|
+
}),
|
|
1494
|
+
});
|
|
1495
|
+
if (!response.ok) {
|
|
1496
|
+
const error = await response.text();
|
|
1497
|
+
console.error(`Failed to verify API key: ${error}`);
|
|
1498
|
+
process.exit(1);
|
|
1499
|
+
}
|
|
1500
|
+
// Save config
|
|
1501
|
+
const config = {
|
|
1502
|
+
apiKey,
|
|
1503
|
+
cloudUrl,
|
|
1504
|
+
machineId,
|
|
1505
|
+
machineName,
|
|
1506
|
+
linkedAt: new Date().toISOString(),
|
|
1507
|
+
};
|
|
1508
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1509
|
+
fs.chmodSync(configPath, 0o600); // Secure the file
|
|
1510
|
+
// Clean up temp code
|
|
1511
|
+
if (fs.existsSync(tempCodePath)) {
|
|
1512
|
+
fs.unlinkSync(tempCodePath);
|
|
1513
|
+
}
|
|
1514
|
+
console.log('');
|
|
1515
|
+
console.log('✓ Machine linked successfully!');
|
|
1516
|
+
console.log('');
|
|
1517
|
+
console.log('Your daemon will now sync with Agent Relay Cloud.');
|
|
1518
|
+
console.log('Run `agent-relay up` to start with cloud sync enabled.');
|
|
1519
|
+
console.log('');
|
|
1520
|
+
}
|
|
1521
|
+
catch (err) {
|
|
1522
|
+
console.error(`Failed to connect to cloud: ${err.message}`);
|
|
1523
|
+
process.exit(1);
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
cloudCommand
|
|
1527
|
+
.command('unlink')
|
|
1528
|
+
.description('Unlink this machine from Agent Relay Cloud')
|
|
1529
|
+
.action(async () => {
|
|
1530
|
+
const os = await import('node:os');
|
|
1531
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
1532
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
1533
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
1534
|
+
if (!fs.existsSync(configPath)) {
|
|
1535
|
+
console.log('This machine is not linked to Agent Relay Cloud.');
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
// Read current config
|
|
1539
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1540
|
+
// Delete config file
|
|
1541
|
+
fs.unlinkSync(configPath);
|
|
1542
|
+
console.log('');
|
|
1543
|
+
console.log('✓ Machine unlinked from Agent Relay Cloud');
|
|
1544
|
+
console.log('');
|
|
1545
|
+
console.log(`Machine ID: ${config.machineId}`);
|
|
1546
|
+
console.log(`Was linked since: ${config.linkedAt}`);
|
|
1547
|
+
console.log('');
|
|
1548
|
+
console.log('Note: The API key has been removed locally. To fully revoke access,');
|
|
1549
|
+
console.log('visit your Agent Relay Cloud dashboard and remove this machine.');
|
|
1550
|
+
console.log('');
|
|
1551
|
+
});
|
|
1552
|
+
cloudCommand
|
|
1553
|
+
.command('status')
|
|
1554
|
+
.description('Show cloud sync status')
|
|
1555
|
+
.action(async () => {
|
|
1556
|
+
const os = await import('node:os');
|
|
1557
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
1558
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
1559
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
1560
|
+
if (!fs.existsSync(configPath)) {
|
|
1561
|
+
console.log('');
|
|
1562
|
+
console.log('Cloud sync: Not configured');
|
|
1563
|
+
console.log('');
|
|
1564
|
+
console.log('Run `agent-relay cloud link` to connect to Agent Relay Cloud.');
|
|
1565
|
+
console.log('');
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1569
|
+
console.log('');
|
|
1570
|
+
console.log('Cloud sync: Enabled');
|
|
1571
|
+
console.log('');
|
|
1572
|
+
console.log(` Machine: ${config.machineName}`);
|
|
1573
|
+
console.log(` ID: ${config.machineId}`);
|
|
1574
|
+
console.log(` Cloud URL: ${config.cloudUrl}`);
|
|
1575
|
+
console.log(` Linked: ${new Date(config.linkedAt).toLocaleString()}`);
|
|
1576
|
+
console.log('');
|
|
1577
|
+
// Check if daemon is running and connected
|
|
1578
|
+
const { getProjectPaths } = await import('../utils/project-namespace.js');
|
|
1579
|
+
const paths = getProjectPaths();
|
|
1580
|
+
if (fs.existsSync(paths.socketPath)) {
|
|
1581
|
+
console.log(' Daemon: Running');
|
|
1582
|
+
// Try to get cloud sync status from daemon
|
|
1583
|
+
try {
|
|
1584
|
+
const response = await fetch(`${config.cloudUrl}/api/daemons/heartbeat`, {
|
|
1585
|
+
method: 'POST',
|
|
1586
|
+
headers: {
|
|
1587
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
1588
|
+
'Content-Type': 'application/json',
|
|
1589
|
+
},
|
|
1590
|
+
body: JSON.stringify({ agents: [], metrics: {} }),
|
|
1591
|
+
});
|
|
1592
|
+
if (response.ok) {
|
|
1593
|
+
console.log(' Cloud connection: Online');
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
console.log(' Cloud connection: Error (API key may be invalid)');
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
catch (err) {
|
|
1600
|
+
console.log(` Cloud connection: Offline (${err.message})`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
console.log(' Daemon: Not running');
|
|
1605
|
+
console.log(' Cloud connection: Offline (daemon not started)');
|
|
1606
|
+
}
|
|
1607
|
+
console.log('');
|
|
1608
|
+
});
|
|
1609
|
+
cloudCommand
|
|
1610
|
+
.command('sync')
|
|
1611
|
+
.description('Manually sync credentials from cloud')
|
|
1612
|
+
.action(async () => {
|
|
1613
|
+
const os = await import('node:os');
|
|
1614
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
1615
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
1616
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
1617
|
+
if (!fs.existsSync(configPath)) {
|
|
1618
|
+
console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
|
|
1619
|
+
process.exit(1);
|
|
1620
|
+
}
|
|
1621
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1622
|
+
console.log('Syncing credentials from cloud...');
|
|
1623
|
+
try {
|
|
1624
|
+
const response = await fetch(`${config.cloudUrl}/api/daemons/credentials`, {
|
|
1625
|
+
headers: {
|
|
1626
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
1627
|
+
},
|
|
1628
|
+
});
|
|
1629
|
+
if (!response.ok) {
|
|
1630
|
+
const error = await response.text();
|
|
1631
|
+
console.error(`Failed to sync: ${error}`);
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
const data = await response.json();
|
|
1635
|
+
console.log('');
|
|
1636
|
+
console.log(`Synced ${data.credentials.length} provider credentials:`);
|
|
1637
|
+
for (const cred of data.credentials) {
|
|
1638
|
+
console.log(` - ${cred.provider}`);
|
|
1639
|
+
}
|
|
1640
|
+
// Save credentials locally for daemon to use
|
|
1641
|
+
const credentialsPath = path.join(dataDir, 'cloud-credentials.json');
|
|
1642
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(data.credentials, null, 2));
|
|
1643
|
+
fs.chmodSync(credentialsPath, 0o600);
|
|
1644
|
+
console.log('');
|
|
1645
|
+
console.log('✓ Credentials synced successfully');
|
|
1646
|
+
console.log('');
|
|
1647
|
+
}
|
|
1648
|
+
catch (err) {
|
|
1649
|
+
console.error(`Failed to sync: ${err.message}`);
|
|
1650
|
+
process.exit(1);
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
cloudCommand
|
|
1654
|
+
.command('agents')
|
|
1655
|
+
.description('List agents across all linked machines')
|
|
1656
|
+
.option('--json', 'Output as JSON')
|
|
1657
|
+
.action(async (options) => {
|
|
1658
|
+
const os = await import('node:os');
|
|
1659
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
1660
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
1661
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
1662
|
+
if (!fs.existsSync(configPath)) {
|
|
1663
|
+
console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
|
|
1664
|
+
process.exit(1);
|
|
1665
|
+
}
|
|
1666
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1667
|
+
try {
|
|
1668
|
+
// Get agents from cloud
|
|
1669
|
+
const response = await fetch(`${config.cloudUrl}/api/daemons/agents`, {
|
|
1670
|
+
method: 'POST',
|
|
1671
|
+
headers: {
|
|
1672
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
1673
|
+
'Content-Type': 'application/json',
|
|
1674
|
+
},
|
|
1675
|
+
body: JSON.stringify({ agents: [] }), // Report no agents, just fetch list
|
|
1676
|
+
});
|
|
1677
|
+
if (!response.ok) {
|
|
1678
|
+
const error = await response.text();
|
|
1679
|
+
console.error(`Failed to fetch agents: ${error}`);
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
}
|
|
1682
|
+
const data = await response.json();
|
|
1683
|
+
if (options.json) {
|
|
1684
|
+
console.log(JSON.stringify(data.allAgents, null, 2));
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (!data.allAgents.length) {
|
|
1688
|
+
console.log('No agents found across linked machines.');
|
|
1689
|
+
console.log('Make sure daemons are running on linked machines.');
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
console.log('');
|
|
1693
|
+
console.log('Agents across all linked machines:');
|
|
1694
|
+
console.log('');
|
|
1695
|
+
console.log('NAME STATUS DAEMON MACHINE');
|
|
1696
|
+
console.log('─'.repeat(65));
|
|
1697
|
+
// Group by daemon
|
|
1698
|
+
const byDaemon = new Map();
|
|
1699
|
+
for (const agent of data.allAgents) {
|
|
1700
|
+
const existing = byDaemon.get(agent.daemonName) || [];
|
|
1701
|
+
existing.push(agent);
|
|
1702
|
+
byDaemon.set(agent.daemonName, existing);
|
|
1703
|
+
}
|
|
1704
|
+
for (const [daemonName, agents] of byDaemon.entries()) {
|
|
1705
|
+
for (const agent of agents) {
|
|
1706
|
+
const name = agent.name.padEnd(15);
|
|
1707
|
+
const status = agent.status.padEnd(8);
|
|
1708
|
+
const daemon = daemonName.padEnd(18);
|
|
1709
|
+
const machine = agent.machineId.substring(0, 20);
|
|
1710
|
+
console.log(`${name} ${status} ${daemon} ${machine}`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
console.log('');
|
|
1714
|
+
console.log(`Total: ${data.allAgents.length} agents on ${byDaemon.size} machines`);
|
|
1715
|
+
console.log('');
|
|
1716
|
+
}
|
|
1717
|
+
catch (err) {
|
|
1718
|
+
console.error(`Failed to fetch agents: ${err.message}`);
|
|
1719
|
+
process.exit(1);
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
cloudCommand
|
|
1723
|
+
.command('send')
|
|
1724
|
+
.description('Send a message to an agent on any linked machine')
|
|
1725
|
+
.argument('<agent>', 'Target agent name')
|
|
1726
|
+
.argument('<message>', 'Message to send')
|
|
1727
|
+
.option('--from <name>', 'Sender name', 'cli')
|
|
1728
|
+
.action(async (agent, message, options) => {
|
|
1729
|
+
const os = await import('node:os');
|
|
1730
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
1731
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
1732
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
1733
|
+
if (!fs.existsSync(configPath)) {
|
|
1734
|
+
console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
|
|
1735
|
+
process.exit(1);
|
|
1736
|
+
}
|
|
1737
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1738
|
+
console.log(`Sending message to ${agent}...`);
|
|
1739
|
+
try {
|
|
1740
|
+
// First, find which daemon the agent is on
|
|
1741
|
+
const agentsResponse = await fetch(`${config.cloudUrl}/api/daemons/agents`, {
|
|
1742
|
+
method: 'POST',
|
|
1743
|
+
headers: {
|
|
1744
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
1745
|
+
'Content-Type': 'application/json',
|
|
1746
|
+
},
|
|
1747
|
+
body: JSON.stringify({ agents: [] }),
|
|
1748
|
+
});
|
|
1749
|
+
if (!agentsResponse.ok) {
|
|
1750
|
+
const error = await agentsResponse.text();
|
|
1751
|
+
console.error(`Failed to find agent: ${error}`);
|
|
1752
|
+
process.exit(1);
|
|
1753
|
+
}
|
|
1754
|
+
const agentsData = await agentsResponse.json();
|
|
1755
|
+
const targetAgent = agentsData.allAgents.find(a => a.name === agent);
|
|
1756
|
+
if (!targetAgent) {
|
|
1757
|
+
console.error(`Agent "${agent}" not found.`);
|
|
1758
|
+
console.log('Available agents:');
|
|
1759
|
+
for (const a of agentsData.allAgents) {
|
|
1760
|
+
console.log(` - ${a.name} (on ${a.daemonName})`);
|
|
1761
|
+
}
|
|
1762
|
+
process.exit(1);
|
|
1763
|
+
}
|
|
1764
|
+
// Send the message
|
|
1765
|
+
const sendResponse = await fetch(`${config.cloudUrl}/api/daemons/message`, {
|
|
1766
|
+
method: 'POST',
|
|
1767
|
+
headers: {
|
|
1768
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
1769
|
+
'Content-Type': 'application/json',
|
|
1770
|
+
},
|
|
1771
|
+
body: JSON.stringify({
|
|
1772
|
+
targetDaemonId: targetAgent.daemonId,
|
|
1773
|
+
targetAgent: agent,
|
|
1774
|
+
message: {
|
|
1775
|
+
from: options.from,
|
|
1776
|
+
content: message,
|
|
1777
|
+
},
|
|
1778
|
+
}),
|
|
1779
|
+
});
|
|
1780
|
+
if (!sendResponse.ok) {
|
|
1781
|
+
const error = await sendResponse.text();
|
|
1782
|
+
console.error(`Failed to send message: ${error}`);
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
console.log('');
|
|
1786
|
+
console.log(`✓ Message sent to ${agent} on ${targetAgent.daemonName}`);
|
|
1787
|
+
console.log('');
|
|
1788
|
+
}
|
|
1789
|
+
catch (err) {
|
|
1790
|
+
console.error(`Failed to send message: ${err.message}`);
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
cloudCommand
|
|
1795
|
+
.command('daemons')
|
|
1796
|
+
.description('List all linked daemon instances')
|
|
1797
|
+
.option('--json', 'Output as JSON')
|
|
1798
|
+
.action(async (options) => {
|
|
1799
|
+
const os = await import('node:os');
|
|
1800
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
|
|
1801
|
+
path.join(os.homedir(), '.local', 'share', 'agent-relay');
|
|
1802
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
1803
|
+
if (!fs.existsSync(configPath)) {
|
|
1804
|
+
console.error('Not linked to cloud. Run `agent-relay cloud link` first.');
|
|
1805
|
+
process.exit(1);
|
|
1806
|
+
}
|
|
1807
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1808
|
+
try {
|
|
1809
|
+
// Get daemons list (requires browser auth, so we use a workaround)
|
|
1810
|
+
// For now, just show what we know about our own daemon
|
|
1811
|
+
console.log('');
|
|
1812
|
+
console.log('Linked Daemon:');
|
|
1813
|
+
console.log('');
|
|
1814
|
+
console.log(` Machine: ${config.machineName}`);
|
|
1815
|
+
console.log(` ID: ${config.machineId}`);
|
|
1816
|
+
console.log(` Cloud: ${config.cloudUrl}`);
|
|
1817
|
+
console.log(` Linked: ${new Date(config.linkedAt).toLocaleString()}`);
|
|
1818
|
+
console.log('');
|
|
1819
|
+
console.log('Note: To see all linked daemons, visit your cloud dashboard.');
|
|
1820
|
+
console.log('');
|
|
1821
|
+
}
|
|
1822
|
+
catch (err) {
|
|
1823
|
+
console.error(`Failed: ${err.message}`);
|
|
1824
|
+
process.exit(1);
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1042
1827
|
program.parse();
|
|
1043
1828
|
//# sourceMappingURL=index.js.map
|