@otto-code/cli 0.8.12 → 0.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/cli.js +1 -5
  2. package/dist/commands/daemon/index.js +1 -0
  3. package/dist/commands/daemon/local-daemon.js +3 -0
  4. package/dist/commands/daemon/pair.d.ts +27 -1
  5. package/dist/commands/daemon/pair.js +133 -46
  6. package/dist/commands/daemon/start.js +1 -0
  7. package/dist/commands/daemon/status.d.ts +11 -0
  8. package/dist/commands/daemon/status.js +47 -22
  9. package/dist/commands/hub/authority.d.ts +17 -0
  10. package/dist/commands/hub/authority.js +18 -0
  11. package/dist/commands/hub/connect.d.ts +21 -0
  12. package/dist/commands/hub/connect.js +34 -0
  13. package/dist/commands/hub/credentials.d.ts +21 -0
  14. package/dist/commands/hub/credentials.js +143 -0
  15. package/dist/commands/hub/daemon-client.d.ts +27 -0
  16. package/dist/commands/hub/daemon-client.js +14 -0
  17. package/dist/commands/hub/deploy-bundle.d.ts +16 -0
  18. package/dist/commands/hub/deploy-bundle.js +248 -0
  19. package/dist/commands/hub/deploy.d.ts +40 -0
  20. package/dist/commands/hub/deploy.js +90 -0
  21. package/dist/commands/hub/disconnect.d.ts +16 -0
  22. package/dist/commands/hub/disconnect.js +24 -0
  23. package/dist/commands/hub/error.d.ts +6 -0
  24. package/dist/commands/hub/error.js +9 -0
  25. package/dist/commands/hub/help.d.ts +3 -0
  26. package/dist/commands/hub/help.js +5 -0
  27. package/dist/commands/hub/hub-client/index.d.ts +19 -0
  28. package/dist/commands/hub/hub-client/index.js +99 -0
  29. package/dist/commands/hub/hub-client/internal/contracts.d.ts +86 -0
  30. package/dist/commands/hub/hub-client/internal/contracts.js +62 -0
  31. package/dist/commands/hub/hub-client/internal/problem.d.ts +3 -0
  32. package/dist/commands/hub/hub-client/internal/problem.js +85 -0
  33. package/dist/commands/hub/hub-client/internal/transport.d.ts +15 -0
  34. package/dist/commands/hub/hub-client/internal/transport.js +30 -0
  35. package/dist/commands/hub/index.d.ts +15 -25
  36. package/dist/commands/hub/index.js +67 -73
  37. package/dist/commands/hub/init-plan.d.ts +50 -0
  38. package/dist/commands/hub/init-plan.js +136 -0
  39. package/dist/commands/hub/init.d.ts +20 -0
  40. package/dist/commands/hub/init.js +415 -0
  41. package/dist/commands/hub/{device-authorization.d.ts → login-flow.d.ts} +12 -15
  42. package/dist/commands/hub/login-flow.js +78 -0
  43. package/dist/commands/hub/login.d.ts +21 -0
  44. package/dist/commands/hub/login.js +34 -0
  45. package/dist/commands/hub/logout.d.ts +32 -0
  46. package/dist/commands/hub/logout.js +71 -0
  47. package/dist/commands/hub/origin.d.ts +2 -0
  48. package/dist/commands/hub/origin.js +27 -0
  49. package/dist/commands/hub/projects.d.ts +24 -0
  50. package/dist/commands/hub/projects.js +49 -0
  51. package/dist/commands/hub/reporter.d.ts +10 -0
  52. package/dist/commands/hub/reporter.js +11 -0
  53. package/dist/commands/hub/status-output.d.ts +13 -0
  54. package/dist/commands/hub/status-output.js +30 -0
  55. package/dist/commands/onboard.js +20 -40
  56. package/dist/commands/provider/diagnostic.d.ts +12 -0
  57. package/dist/commands/provider/diagnostic.js +31 -0
  58. package/dist/commands/provider/index.js +5 -0
  59. package/dist/commands/schedule/types.d.ts +1 -4
  60. package/dist/commands/workspace/index.js +10 -0
  61. package/dist/commands/workspace/rename.d.ts +22 -0
  62. package/dist/commands/workspace/rename.js +61 -0
  63. package/dist/output/types.d.ts +1 -1
  64. package/dist/run.js +8 -1
  65. package/dist/utils/client.js +4 -1
  66. package/package.json +5 -5
  67. package/dist/commands/chat/create.d.ts +0 -9
  68. package/dist/commands/chat/create.js +0 -23
  69. package/dist/commands/chat/delete.d.ts +0 -6
  70. package/dist/commands/chat/delete.js +0 -20
  71. package/dist/commands/chat/index.d.ts +0 -3
  72. package/dist/commands/chat/index.js +0 -47
  73. package/dist/commands/chat/inspect.d.ts +0 -6
  74. package/dist/commands/chat/inspect.js +0 -20
  75. package/dist/commands/chat/ls.d.ts +0 -6
  76. package/dist/commands/chat/ls.js +0 -20
  77. package/dist/commands/chat/post.d.ts +0 -9
  78. package/dist/commands/chat/post.js +0 -28
  79. package/dist/commands/chat/read.d.ts +0 -11
  80. package/dist/commands/chat/read.js +0 -40
  81. package/dist/commands/chat/schema.d.ts +0 -36
  82. package/dist/commands/chat/schema.js +0 -81
  83. package/dist/commands/chat/shared.d.ts +0 -19
  84. package/dist/commands/chat/shared.js +0 -118
  85. package/dist/commands/chat/wait.d.ts +0 -9
  86. package/dist/commands/chat/wait.js +0 -45
  87. package/dist/commands/hub/cloud-device-authorization.d.ts +0 -45
  88. package/dist/commands/hub/cloud-device-authorization.js +0 -92
  89. package/dist/commands/hub/device-authorization.js +0 -87
  90. package/dist/commands/loop/index.d.ts +0 -3
  91. package/dist/commands/loop/index.js +0 -18
  92. package/dist/commands/loop/inspect.d.ts +0 -13
  93. package/dist/commands/loop/inspect.js +0 -100
  94. package/dist/commands/loop/logs.d.ts +0 -8
  95. package/dist/commands/loop/logs.js +0 -69
  96. package/dist/commands/loop/ls.d.ts +0 -18
  97. package/dist/commands/loop/ls.js +0 -62
  98. package/dist/commands/loop/run.d.ts +0 -30
  99. package/dist/commands/loop/run.js +0 -162
  100. package/dist/commands/loop/stop.d.ts +0 -15
  101. package/dist/commands/loop/stop.js +0 -56
  102. package/dist/commands/loop/types.d.ts +0 -138
  103. package/dist/commands/loop/types.js +0 -2
package/dist/cli.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import { Command, Option } from "commander";
2
2
  import { createAgentCommand } from "./commands/agent/index.js";
3
3
  import { createDaemonCommand } from "./commands/daemon/index.js";
4
- import { createChatCommand } from "./commands/chat/index.js";
5
- import { createLoopCommand } from "./commands/loop/index.js";
6
4
  import { createPermitCommand } from "./commands/permit/index.js";
7
5
  import { createProviderCommand } from "./commands/provider/index.js";
8
6
  import { createScheduleCommand } from "./commands/schedule/index.js";
@@ -91,6 +89,7 @@ export function createCli() {
91
89
  .option("--force", "Send SIGKILL if graceful stop times out")
92
90
  .option("--listen <listen>", "Listen target for restarted daemon (host:port, port, or unix socket)")
93
91
  .option("--port <port>", "Port for restarted daemon listen target")
92
+ .option("--relay", "Enable relay on restarted daemon")
94
93
  .option("--no-relay", "Disable relay on restarted daemon")
95
94
  .option("--no-mcp", "Disable Agent MCP on restarted daemon")
96
95
  .option("--hostnames <hosts>", 'Daemon hostnames (comma-separated, e.g. "myhost,.example.com" or "true" for any)')
@@ -108,13 +107,10 @@ export function createCli() {
108
107
  program.addCommand(createDaemonCommand());
109
108
  program.addCommand(createHubCommand());
110
109
  // Chat commands
111
- program.addCommand(createChatCommand());
112
110
  // Terminal commands
113
111
  program.addCommand(createTerminalCommand());
114
112
  // Workspace script commands
115
113
  program.addCommand(createScriptCommand());
116
- // Loop commands
117
- program.addCommand(createLoopCommand());
118
114
  // Schedule commands
119
115
  program.addCommand(createScheduleCommand());
120
116
  program.addCommand(createHeartbeatCommand());
@@ -33,6 +33,7 @@ export function createDaemonCommand() {
33
33
  .option("--force", "Send SIGKILL if graceful stop times out")
34
34
  .option("--listen <listen>", "Listen target for restarted daemon (host:port, port, or unix socket)")
35
35
  .option("--port <port>", "Port for restarted daemon listen target")
36
+ .option("--relay", "Enable relay on restarted daemon")
36
37
  .option("--no-relay", "Disable relay on restarted daemon")
37
38
  .option("--no-mcp", "Disable Agent MCP on restarted daemon")
38
39
  .option("--no-inject-mcp", "Disable auto-injecting the Otto MCP into created agents")
@@ -31,6 +31,9 @@ function envWithHome(home) {
31
31
  }
32
32
  function buildRunnerArgs(options) {
33
33
  const args = [];
34
+ if (options.relay === true) {
35
+ args.push("--relay");
36
+ }
34
37
  if (options.relay === false) {
35
38
  args.push("--no-relay");
36
39
  }
@@ -2,8 +2,34 @@ import { Command } from "commander";
2
2
  interface PairOptions {
3
3
  home?: string;
4
4
  json?: boolean;
5
+ relay?: boolean;
6
+ }
7
+ export interface PairCommandDependencies {
8
+ resolveOffer: typeof resolveLocalPairingOffer;
9
+ confirmRelay: typeof confirmRelayPairing;
10
+ printDirectGuidance: typeof printDirectConnectionGuidance;
11
+ isInteractive: () => boolean;
12
+ output: PairCommandOutput;
13
+ }
14
+ export interface PairCommandOutput {
15
+ columns: number | undefined;
16
+ writeStdout(message: string): void;
17
+ writeStderr(message: string): void;
18
+ setExitCode(code: number): void;
19
+ success(message: string): void;
20
+ }
21
+ export interface PairingOffer {
22
+ relayEnabled: boolean;
23
+ url: string | null;
24
+ qr: string | null;
5
25
  }
6
26
  export declare function pairCommand(): Command;
7
- export declare function runPairCommand(options: PairOptions): Promise<void>;
27
+ export declare function resolveLocalPairingOffer(options: {
28
+ ottoHome: string;
29
+ enableRelay?: boolean;
30
+ }): Promise<PairingOffer>;
31
+ export declare function confirmRelayPairing(): Promise<boolean>;
32
+ export declare function printDirectConnectionGuidance(): void;
33
+ export declare function runPairCommand(options: PairOptions, dependencyOverrides?: Partial<PairCommandDependencies>): Promise<void>;
8
34
  export {};
9
35
  //# sourceMappingURL=pair.d.ts.map
@@ -1,52 +1,53 @@
1
+ import { confirm, isCancel, log } from "@clack/prompts";
1
2
  import { Command } from "commander";
2
3
  import chalk from "chalk";
3
- import { generateLocalPairingOffer, loadConfig, resolveOttoHome } from "@otto-code/server";
4
+ import { generateLocalPairingOffer, getOrCreateServerId, loadConfig, resolveOttoHome, } from "@otto-code/server";
4
5
  import { tryConnectToDaemon } from "../../utils/client.js";
5
- import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
6
+ import { resolveLocalDaemonState } from "./local-daemon.js";
6
7
  import { addJsonOption } from "../../utils/command-options.js";
7
8
  import { formatPairingInstructions } from "../../output/pairing.js";
8
9
  const PAIRING_DAEMON_RPC_TIMEOUT_MS = 1500;
10
+ const RELAY_DOCS_URL = "https://otto-code.me/docs/security";
11
+ function createProcessOutput() {
12
+ return {
13
+ columns: process.stdout.columns,
14
+ writeStdout(message) {
15
+ process.stdout.write(message);
16
+ },
17
+ writeStderr(message) {
18
+ process.stderr.write(message);
19
+ },
20
+ setExitCode(code) {
21
+ process.exitCode = code;
22
+ },
23
+ success(message) {
24
+ log.success(message);
25
+ },
26
+ };
27
+ }
9
28
  export function pairCommand() {
10
29
  return addJsonOption(new Command("pair").description("Print the daemon pairing QR code and link"))
11
30
  .option("--home <path>", "Otto home directory (default: ~/.otto)")
31
+ .option("--relay", "Enable relay without prompting")
12
32
  .action(async (_options, command) => {
13
33
  await runPairCommand(command.optsWithGlobals());
14
34
  });
15
35
  }
16
- export async function runPairCommand(options) {
17
- if (options.home) {
18
- process.env.OTTO_HOME = options.home;
36
+ export async function resolveLocalPairingOffer(options) {
37
+ const state = resolveLocalDaemonState({ home: options.ottoHome });
38
+ const serverId = getOrCreateServerId(state.home);
39
+ const daemonOffer = await resolveDaemonPairingOffer(state.listen, serverId, options.enableRelay);
40
+ if (daemonOffer)
41
+ return daemonOffer;
42
+ if (state.running) {
43
+ throw new Error("The running daemon did not provide a pairing offer. Check daemon connectivity or update the daemon.");
19
44
  }
20
- const ottoHome = resolveOttoHome();
21
- const state = resolveLocalDaemonState({ home: ottoHome });
22
- const host = resolveTcpHostFromListen(state.listen);
23
- // Try to get the pairing offer from the running daemon first.
24
- if (host) {
25
- const client = await tryConnectToDaemon({ host, timeout: 1500 });
26
- if (client) {
27
- const supportsDaemonStatusRpc = client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
28
- if (supportsDaemonStatusRpc) {
29
- try {
30
- const offer = await client.getDaemonPairingOffer({
31
- timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
32
- });
33
- await client.close().catch(() => { });
34
- outputPairingResult({ relayEnabled: offer.relayEnabled, url: offer.url, qr: offer.qr ?? null }, options);
35
- return;
36
- }
37
- catch {
38
- // COMPAT(daemon-rpc-rollout): fall back to CLI-side pairing generation while
39
- // old daemons lack daemonStatusRpc. Remove once the daemon floor is past
40
- // v0.1.76; pairing should come from daemon.get_pairing_offer.
41
- }
42
- }
43
- await client.close().catch(() => { });
44
- }
45
+ const config = loadConfig(options.ottoHome);
46
+ if (options.enableRelay && !config.relayEnabled) {
47
+ throw new Error("Start the daemon before enabling relay for pairing.");
45
48
  }
46
- // Fall back to local pairing offer generation.
47
- const config = loadConfig(ottoHome);
48
- const pairing = await generateLocalPairingOffer({
49
- ottoHome,
49
+ return generateLocalPairingOffer({
50
+ ottoHome: options.ottoHome,
50
51
  relayEnabled: config.relayEnabled,
51
52
  relayEndpoint: config.relayEndpoint,
52
53
  relayPublicEndpoint: config.relayPublicEndpoint,
@@ -55,26 +56,112 @@ export async function runPairCommand(options) {
55
56
  appBaseUrl: config.appBaseUrl,
56
57
  includeQr: true,
57
58
  });
58
- outputPairingResult(pairing, options);
59
59
  }
60
- function outputPairingResult(pairing, options) {
60
+ async function resolveDaemonPairingOffer(listen, expectedServerId, enableRelay) {
61
+ const client = await tryConnectToDaemon({
62
+ host: listen,
63
+ timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
64
+ });
65
+ if (!client)
66
+ return null;
67
+ try {
68
+ const serverInfo = client.getLastServerInfoMessage();
69
+ if (serverInfo?.serverId.trim() !== expectedServerId) {
70
+ throw new Error("The reachable daemon belongs to a different Otto home. Check --home or the daemon listen configuration.");
71
+ }
72
+ if (serverInfo?.features?.daemonStatusRpc !== true) {
73
+ throw new Error("Update the Otto daemon before pairing from this command.");
74
+ }
75
+ let offer = await client.getDaemonPairingOffer({
76
+ timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
77
+ });
78
+ if (!offer.relayEnabled && enableRelay) {
79
+ if (serverInfo.features.relayConfig !== true) {
80
+ throw new Error("Update the Otto daemon before enabling relay from this command.");
81
+ }
82
+ await client.patchDaemonConfig({ relay: { enabled: true } });
83
+ offer = await client.getDaemonPairingOffer({
84
+ timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
85
+ });
86
+ }
87
+ return {
88
+ relayEnabled: offer.relayEnabled,
89
+ url: offer.url || null,
90
+ qr: offer.qr ?? null,
91
+ };
92
+ }
93
+ finally {
94
+ await client.close().catch(() => undefined);
95
+ }
96
+ }
97
+ export async function confirmRelayPairing() {
98
+ log.message("Your connection is end-to-end encrypted. Otto cannot read your code or messages.");
99
+ log.message(`Learn how it works: ${RELAY_DOCS_URL}`);
100
+ const answer = await confirm({
101
+ message: "Enable relay to pair a device?",
102
+ initialValue: false,
103
+ });
104
+ return !isCancel(answer) && answer;
105
+ }
106
+ export function printDirectConnectionGuidance() {
107
+ console.log("Daemon is running with relay off.");
108
+ console.log("To connect another device directly, use the daemon's TCP address over your LAN, Tailscale, or another VPN.");
109
+ console.log(`Learn more: ${RELAY_DOCS_URL}#direct-connections`);
110
+ }
111
+ export async function runPairCommand(options, dependencyOverrides = {}) {
112
+ if (options.home)
113
+ process.env.OTTO_HOME = options.home;
114
+ const dependencies = {
115
+ resolveOffer: resolveLocalPairingOffer,
116
+ confirmRelay: confirmRelayPairing,
117
+ printDirectGuidance: printDirectConnectionGuidance,
118
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
119
+ output: createProcessOutput(),
120
+ ...dependencyOverrides,
121
+ };
122
+ const ottoHome = resolveOttoHome();
123
+ let pairing = await dependencies.resolveOffer({
124
+ ottoHome,
125
+ enableRelay: options.relay === true,
126
+ });
127
+ const canPrompt = dependencies.isInteractive() && options.json !== true;
128
+ if (!pairing.relayEnabled && canPrompt) {
129
+ const shouldEnable = await dependencies.confirmRelay();
130
+ if (!shouldEnable) {
131
+ dependencies.printDirectGuidance();
132
+ dependencies.output.writeStderr(`${chalk.yellow("No pairing QR was created.")}\n`);
133
+ dependencies.output.setExitCode(1);
134
+ return;
135
+ }
136
+ pairing = await dependencies.resolveOffer({ ottoHome, enableRelay: true });
137
+ dependencies.output.success("Relay enabled");
138
+ }
139
+ outputPairingResult(pairing, options, dependencies.output);
140
+ }
141
+ function outputPairingResult(pairing, options, output) {
61
142
  if (!pairing.relayEnabled || !pairing.url) {
62
- console.error(chalk.red("Relay pairing is disabled for this daemon config."));
63
- console.error(chalk.yellow("Enable relay and run this command again."));
64
- process.exit(1);
143
+ if (options.json) {
144
+ output.writeStderr(`${JSON.stringify({
145
+ code: "RELAY_DISABLED",
146
+ message: "Relay pairing is disabled for this daemon.",
147
+ action: "Run otto daemon pair --relay --json to enable it explicitly.",
148
+ })}\n`);
149
+ }
150
+ else {
151
+ output.writeStderr(`${chalk.red("Relay pairing is disabled for this daemon.")}\n`);
152
+ output.writeStderr(`${chalk.yellow("Run otto daemon pair --relay to enable it.")}\n`);
153
+ }
154
+ output.setExitCode(1);
155
+ return;
65
156
  }
66
157
  if (options.json) {
67
- process.stdout.write(`${JSON.stringify({
68
- relayEnabled: pairing.relayEnabled,
69
- url: pairing.url,
70
- qr: pairing.qr,
71
- }, null, 2)}\n`);
158
+ output.writeStdout(`${JSON.stringify({ relayEnabled: pairing.relayEnabled, url: pairing.url, qr: pairing.qr }, null, 2)}\n`);
72
159
  return;
73
160
  }
74
- process.stdout.write(formatPairingInstructions({
161
+ output.writeStdout(formatPairingInstructions({
75
162
  url: pairing.url,
76
163
  qr: pairing.qr,
77
- columns: process.stdout.columns,
164
+ columns: output.columns,
78
165
  }));
79
166
  }
80
167
  //# sourceMappingURL=pair.js.map
@@ -9,6 +9,7 @@ export function startCommand() {
9
9
  .option("--port <port>", "Port to listen on (default: 6868)")
10
10
  .option("--home <path>", "Otto home directory (default: ~/.otto)")
11
11
  .option("--foreground", "Run in foreground (don't daemonize)")
12
+ .option("--relay", "Enable relay connection")
12
13
  .option("--no-relay", "Disable relay connection")
13
14
  .option("--relay-use-tls", "Use wss:// for the relay connection and pairing offers")
14
15
  .option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
@@ -4,6 +4,17 @@ interface StatusRow {
4
4
  key: string;
5
5
  value: string;
6
6
  }
7
+ interface RelayStatusConfig {
8
+ enabled: boolean;
9
+ endpoint: string;
10
+ publicEndpoint: string;
11
+ useTls: boolean;
12
+ publicUseTls: boolean;
13
+ }
14
+ export declare function selectRelayStatus(input: {
15
+ persisted: RelayStatusConfig;
16
+ live?: RelayStatusConfig;
17
+ }): string;
7
18
  export type StatusResult = ListResult<StatusRow>;
8
19
  export declare function runStatusCommand(options: CommandOptions, _command: Command): Promise<StatusResult>;
9
20
  export {};
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { getOrCreateServerId, findExecutable, execCommand } from "@otto-code/server";
3
3
  import { connectToDaemon } from "../../utils/client.js";
4
- import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
4
+ import { resolveLocalDaemonState } from "./local-daemon.js";
5
5
  import { resolveNodePathFromPid } from "./runtime-toolchain.js";
6
6
  const DAEMON_STATUS_PROBE_TIMEOUT_MS = 1500;
7
7
  const require = createRequire(import.meta.url);
@@ -203,12 +203,19 @@ async function probeDaemonOverWebsocket(args) {
203
203
  version: p.available ? null : (p.error ?? null),
204
204
  source: "daemon",
205
205
  }));
206
+ const relayStatus = statusPayload.relay == null
207
+ ? undefined
208
+ : selectRelayStatus({
209
+ persisted: relayConfigFromLocalState(state),
210
+ live: statusPayload.relay,
211
+ });
206
212
  if (!state.running) {
207
213
  return {
208
214
  connectedDaemon: "reachable",
209
215
  daemonVersion: statusPayload.version ?? daemonVersion,
210
216
  daemonNodeOverride: statusPayload.nodePath,
211
217
  daemonProviders,
218
+ relayStatus,
212
219
  note: state.pidInfo
213
220
  ? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
214
221
  : `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
@@ -219,6 +226,7 @@ async function probeDaemonOverWebsocket(args) {
219
226
  daemonVersion: statusPayload.version ?? daemonVersion,
220
227
  daemonNodeOverride: statusPayload.nodePath,
221
228
  daemonProviders,
229
+ relayStatus,
222
230
  };
223
231
  }
224
232
  catch {
@@ -242,6 +250,7 @@ function applyProbeToStatus(input) {
242
250
  daemonNode: probe.daemonNodeOverride ?? input.daemonNode,
243
251
  daemonVersion: probe.daemonVersion !== undefined ? probe.daemonVersion : input.daemonVersion,
244
252
  daemonProviders: probe.daemonProviders ?? input.daemonProviders,
253
+ relayStatus: probe.relayStatus ?? input.relayStatus,
245
254
  note: probe.note ? appendNote(input.note, probe.note) : input.note,
246
255
  };
247
256
  }
@@ -264,16 +273,26 @@ async function resolveDaemonNodeLabel(state) {
264
273
  const fromPid = await resolveNodePathFromPid(state.pidInfo.pid);
265
274
  return fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
266
275
  }
267
- function formatRelayStatus(state) {
268
- if (!state.relayEnabled)
276
+ function relayConfigFromLocalState(state) {
277
+ return {
278
+ enabled: state.relayEnabled,
279
+ endpoint: state.relayEndpoint,
280
+ publicEndpoint: state.relayEndpoint,
281
+ useTls: state.relayUseTls,
282
+ publicUseTls: state.relayPublicUseTls,
283
+ };
284
+ }
285
+ export function selectRelayStatus(input) {
286
+ const relay = input.live ?? input.persisted;
287
+ if (!relay.enabled)
269
288
  return "disabled";
270
- const scheme = state.relayPublicUseTls ? "wss" : "ws";
271
- return `${scheme}://${state.relayEndpoint}`;
289
+ const scheme = relay.publicUseTls ? "wss" : "ws";
290
+ return `${scheme}://${relay.publicEndpoint}`;
272
291
  }
273
292
  export async function runStatusCommand(options, _command) {
274
293
  const home = typeof options.home === "string" ? options.home : undefined;
275
294
  const state = resolveLocalDaemonState({ home });
276
- const host = resolveTcpHostFromListen(state.listen);
295
+ const daemonTarget = state.listen.trim();
277
296
  const owner = resolveOwnerLabel(state.pidInfo?.uid, state.pidInfo?.hostname);
278
297
  let daemonNode = await resolveDaemonNodeLabel(state);
279
298
  const cliNode = process.execPath;
@@ -281,26 +300,32 @@ export async function runStatusCommand(options, _command) {
281
300
  let connectedDaemon = "not_probed";
282
301
  let daemonVersion = null;
283
302
  let daemonProviders;
303
+ let relayStatus = selectRelayStatus({ persisted: relayConfigFromLocalState(state) });
284
304
  let note;
285
305
  if (!state.running && state.stalePidFile && state.pidInfo) {
286
306
  localDaemon = "stale_pid";
287
307
  note = `Stale PID file found for PID ${state.pidInfo.pid}`;
288
308
  }
289
- if (host) {
290
- const probe = await probeDaemonOverWebsocket({ host, state });
291
- ({ connectedDaemon, localDaemon, daemonNode, daemonVersion, daemonProviders, note } =
292
- applyProbeToStatus({
293
- probe,
294
- connectedDaemon,
295
- localDaemon,
296
- daemonNode,
297
- daemonVersion,
298
- daemonProviders,
299
- note,
300
- }));
301
- }
302
- else {
303
- note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
309
+ if (daemonTarget) {
310
+ const probe = await probeDaemonOverWebsocket({ host: daemonTarget, state });
311
+ ({
312
+ connectedDaemon,
313
+ localDaemon,
314
+ daemonNode,
315
+ daemonVersion,
316
+ daemonProviders,
317
+ relayStatus,
318
+ note,
319
+ } = applyProbeToStatus({
320
+ probe,
321
+ connectedDaemon,
322
+ localDaemon,
323
+ daemonNode,
324
+ daemonVersion,
325
+ daemonProviders,
326
+ relayStatus,
327
+ note,
328
+ }));
304
329
  }
305
330
  const cliVersion = resolveCliVersion();
306
331
  const serverIdResult = resolveServerIdSafely(state.home);
@@ -315,7 +340,7 @@ export async function runStatusCommand(options, _command) {
315
340
  connectedDaemon,
316
341
  home: state.home,
317
342
  listen: state.listen,
318
- relay: formatRelayStatus(state),
343
+ relay: relayStatus,
319
344
  hostname: state.pidInfo?.hostname ?? null,
320
345
  pid: state.pidInfo?.pid ?? null,
321
346
  startedAt: state.pidInfo?.startedAt ?? null,
@@ -0,0 +1,17 @@
1
+ import type { HubCredentialStore } from "./credentials.js";
2
+ export interface HubAuthorityOptions {
3
+ origin?: string;
4
+ apiKey?: string;
5
+ }
6
+ interface ResolveHubInput {
7
+ options: HubAuthorityOptions;
8
+ env: Readonly<Record<string, string | undefined>>;
9
+ credentials: HubCredentialStore;
10
+ }
11
+ export declare const DEFAULT_HUB_ORIGIN = "https://hub.otto-code.me";
12
+ export declare function resolveHubOrigin(input: ResolveHubInput): string;
13
+ export declare function resolveHubCredential(input: ResolveHubInput & {
14
+ origin: string;
15
+ }): string;
16
+ export {};
17
+ //# sourceMappingURL=authority.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { HubCommandError } from "./error.js";
2
+ import { normalizeHubOrigin } from "./origin.js";
3
+ export const DEFAULT_HUB_ORIGIN = "https://hub.otto-code.me";
4
+ export function resolveHubOrigin(input) {
5
+ const configuredOrigin = input.options.origin ?? input.env.OTTO_HUB_URL;
6
+ const selectedOrigin = configuredOrigin ?? input.credentials.active()?.origin ?? DEFAULT_HUB_ORIGIN;
7
+ return normalizeHubOrigin(selectedOrigin);
8
+ }
9
+ export function resolveHubCredential(input) {
10
+ const explicitCredential = input.options.apiKey ?? input.env.OTTO_HUB_API_KEY;
11
+ if (explicitCredential !== undefined)
12
+ return explicitCredential;
13
+ const stored = input.credentials.get(input.origin);
14
+ if (stored !== null)
15
+ return stored.credential;
16
+ throw new HubCommandError("HUB_API_KEY_REQUIRED", `No stored Hub login matches ${input.origin}. Run \`otto hub login ${input.origin}\`, pass --api-key <secret>, or set OTTO_HUB_API_KEY.`);
17
+ }
18
+ //# sourceMappingURL=authority.js.map
@@ -0,0 +1,21 @@
1
+ import type { Command } from "commander";
2
+ import type { HubHttpClient } from "./hub-client/index.js";
3
+ import type { HubCredentialStore } from "./credentials.js";
4
+ import type { HubDaemonConnection } from "./daemon-client.js";
5
+ import { type HubReporter } from "./reporter.js";
6
+ interface HubConnectOptions {
7
+ apiKey?: string;
8
+ host?: string;
9
+ json?: boolean;
10
+ }
11
+ interface HubConnectDependencies {
12
+ env: Readonly<Record<string, string | undefined>>;
13
+ credentials: HubCredentialStore;
14
+ hub: Pick<HubHttpClient, "issueEnrollmentToken">;
15
+ daemon: HubDaemonConnection;
16
+ reporter: HubReporter;
17
+ }
18
+ export declare function runHubConnect(originInput: string | undefined, options: HubConnectOptions, dependencies: HubConnectDependencies): Promise<import("../../output/types.js").ListResult<import("./status-output.js").HubRow>>;
19
+ export declare function addHubConnectCommand(parent: Command, dependencies: HubConnectDependencies): void;
20
+ export {};
21
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { withOutput } from "../../output/index.js";
2
+ import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
3
+ import { resolveHubCredential, resolveHubOrigin } from "./authority.js";
4
+ import { withHubDaemon } from "./daemon-client.js";
5
+ import { hubStatusResult } from "./status-output.js";
6
+ import { reportHubProgress } from "./reporter.js";
7
+ import { addHubResolutionHelp } from "./help.js";
8
+ export async function runHubConnect(originInput, options, dependencies) {
9
+ const resolution = {
10
+ options: { origin: originInput, apiKey: options.apiKey },
11
+ env: dependencies.env,
12
+ credentials: dependencies.credentials,
13
+ };
14
+ const origin = resolveHubOrigin(resolution);
15
+ reportHubProgress(dependencies.reporter, options, `Connecting this daemon to ${origin}`);
16
+ const credential = resolveHubCredential({ ...resolution, origin });
17
+ const token = await dependencies.hub.issueEnrollmentToken(origin, credential);
18
+ return withHubDaemon(dependencies.daemon, options.host, async (daemon) => {
19
+ const response = await daemon.connectHub(origin, token);
20
+ return hubStatusResult(response.status);
21
+ });
22
+ }
23
+ export function addHubConnectCommand(parent, dependencies) {
24
+ addJsonAndDaemonHostOptions(addHubResolutionHelp(parent
25
+ .command("connect")
26
+ .description("Enroll this daemon with a Paseo Hub")
27
+ .argument("[origin]", "Paseo Hub origin")
28
+ .option("--api-key <secret>", "Organization API key"))).action(withOutput(async (...args) => {
29
+ const origin = args[0];
30
+ const options = args.at(-2);
31
+ return runHubConnect(origin, options, dependencies);
32
+ }));
33
+ }
34
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1,21 @@
1
+ export interface StoredHubCredential {
2
+ origin: string;
3
+ credential: string;
4
+ }
5
+ export interface HubCredentialStore {
6
+ active(): StoredHubCredential | null;
7
+ get(origin: string): StoredHubCredential | null;
8
+ save(credential: StoredHubCredential): void;
9
+ logoutActive(): StoredHubCredential | null;
10
+ }
11
+ export declare class PrivateHubCredentialStore implements HubCredentialStore {
12
+ private readonly filePath;
13
+ constructor(env?: Readonly<Record<string, string | undefined>>);
14
+ active(): StoredHubCredential | null;
15
+ get(origin: string): StoredHubCredential | null;
16
+ save(credential: StoredHubCredential): void;
17
+ logoutActive(): StoredHubCredential | null;
18
+ private read;
19
+ private write;
20
+ }
21
+ //# sourceMappingURL=credentials.d.ts.map