@runneth/cli 0.0.0-sha.a3c44ca880b3.production → 0.0.0-sha.c2efcbb60abb.production

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ runneth send [--name default] [--timeout-ms 600000] [--stdin] -- <command...>
40
40
  runneth status [--name default]
41
41
  runneth list
42
42
  runneth close [--name default]
43
- runneth oauth login [--resource <url>] [--client-name "Runneth MCP"] [--scope <scope>] [--no-open]
43
+ runneth oauth login [--resource <url>] [--client-id|--clientid <id>] [--client-secret|--secret <value>] [--client-name "Runneth MCP"] [--scope <scope>] [--no-open]
44
44
  runneth oauth status [--resource <url>]
45
45
  runneth oauth logout [--resource <url>]
46
46
  runneth chat --workspace <workspace-id> [--api <builder-url>] [--resource <url>] [--auth mondrian|builder] [--conversation <id>]
@@ -72,12 +72,16 @@ use a different state directory.
72
72
 
73
73
  ## OAuth
74
74
 
75
- `runneth-cli oauth login` performs OAuth protected-resource discovery, dynamic
76
- client registration as a public MCP-style client, PKCE authorization, and token
77
- exchange through a local loopback callback.
75
+ `runneth-cli oauth login` performs OAuth protected-resource discovery, PKCE
76
+ authorization, and token exchange through a local loopback callback. By default
77
+ it uses dynamic client registration as a public MCP-style client. Provide
78
+ `--client-id`/`--clientid` and `--client-secret`/`--secret`, or set
79
+ `RUNNETH_OAUTH_CLIENT_ID` and `RUNNETH_OAUTH_CLIENT_SECRET`, to use a
80
+ pre-registered confidential client instead.
78
81
 
79
82
  ```bash
80
83
  runneth oauth login --resource http://localhost:4100/mcp
84
+ RUNNETH_OAUTH_CLIENT_ID=client-id RUNNETH_OAUTH_CLIENT_SECRET=client-secret runneth oauth login --resource http://localhost:4100/mcp
81
85
  runneth oauth status --resource http://localhost:4100/mcp
82
86
  ```
83
87
 
@@ -118,7 +122,8 @@ runneth conversation state --workspace "$MONDRIAN_WORKSPACE_ID" --conversation "
118
122
 
119
123
  `send` waits for the assistant response by default. `send` and `state` require an
120
124
  explicit `--conversation`; the CLI does not track an implicit active
121
- conversation for machine commands.
125
+ conversation for machine commands. State reads advertise Runneth stream V2, so
126
+ the same command can inspect read-only task-origin transcripts.
122
127
 
123
128
  Long-lived agents can keep one authenticated process open with JSONL over
124
129
  stdin/stdout:
package/dist/cli.js CHANGED
@@ -1,12 +1,142 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolveBuildDefaultMcpResourceUrl } from "./build-defaults.js";
3
- import { B as readOAuthCredentialStatus, C as setDefaultRunnethSshTarget, D as runRunnethConversationMachineCommand, I as getOAuthAccessToken, L as loginWithOAuth, M as parseRunnethConversationArgs, N as resolveRunnethConversationOptions, P as runRunnethConversation, R as logoutOAuthCredential, S as saveRunnethSshTarget, T as runRunnethVmCopy, U as resolveSocketPath, _ as resolveRunnethSshTarget, a as normalizeSessionName, b as runOpenSsh, d as installRunnethSshAccess, f as normalizeRunnethSshTargetName, l as installRunnethSkills, m as removeRunnethSshTarget, p as readRunnethSshTargetStore, r as ensureDaemon, s as sendRunnethCliRequest, t as RunnethCliDaemon, w as assertRunnethVmCopyTargetsUseSameResource, x as runRunnethSshProxy } from "./src.js";
3
+ import { B as readOAuthCredentialStatus, C as setDefaultRunnethSshTarget, D as runRunnethConversationMachineCommand, G as resolveSocketPath, L as loginWithOAuth, M as parseRunnethConversationArgs, N as resolveRunnethConversationOptions, P as runRunnethConversation, R as logoutOAuthCredential, S as saveRunnethSshTarget, T as runRunnethVmCopy, U as resolveOAuthToken, V as resolveOAuthClientCredentialOptions, _ as resolveRunnethSshTarget, a as normalizeSessionName, b as runOpenSsh, d as installRunnethSshAccess, f as normalizeRunnethSshTargetName, l as installRunnethSkills, m as removeRunnethSshTarget, p as readRunnethSshTargetStore, r as ensureDaemon, s as sendRunnethCliRequest, t as RunnethCliDaemon, w as assertRunnethVmCopyTargetsUseSameResource, x as runRunnethSshProxy } from "./src.js";
4
4
  import { readFile, rm } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import process$1 from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { spawn } from "node:child_process";
9
9
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
10
+ //#region src/copy-progress.ts
11
+ const COPY_PROGRESS_RENDER_INTERVAL_MS = 200;
12
+ const COPY_PROGRESS_RENDER_INTERVAL_NON_TTY_MS = 1e4;
13
+ const formatErrorMessage = (error) => {
14
+ return error instanceof Error ? error.message : String(error);
15
+ };
16
+ const writeOutput = async (output, chunk) => {
17
+ await new Promise((resolve, reject) => {
18
+ let settled = false;
19
+ const rejectWrite = (error) => {
20
+ if (settled) return;
21
+ settled = true;
22
+ reject(error);
23
+ };
24
+ const onError = (error) => {
25
+ rejectWrite(error);
26
+ };
27
+ output.once("error", onError);
28
+ try {
29
+ output.write(chunk, (error) => {
30
+ if (error !== null && error !== void 0) {
31
+ rejectWrite(error);
32
+ return;
33
+ }
34
+ if (settled) return;
35
+ settled = true;
36
+ output.off("error", onError);
37
+ resolve();
38
+ });
39
+ } catch (error) {
40
+ output.off("error", onError);
41
+ rejectWrite(error);
42
+ }
43
+ });
44
+ };
45
+ const formatCopyByteCount = (bytes) => {
46
+ const units = [
47
+ "B",
48
+ "KiB",
49
+ "MiB",
50
+ "GiB",
51
+ "TiB"
52
+ ];
53
+ let unitIndex = 0;
54
+ let value = bytes;
55
+ while (value >= 1024 && unitIndex < units.length - 1) {
56
+ value /= 1024;
57
+ unitIndex += 1;
58
+ }
59
+ const unit = units[unitIndex];
60
+ if (unit === void 0) throw new Error(`Copy byte count unit index out of range: ${String(unitIndex)}`);
61
+ return `${unitIndex === 0 ? String(value) : value.toFixed(value >= 100 ? 0 : 1)} ${unit}`;
62
+ };
63
+ const formatCopyDuration = (seconds) => {
64
+ if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid copy duration: ${String(seconds)}`);
65
+ const totalSeconds = Math.ceil(seconds);
66
+ if (totalSeconds < 60) return `${String(totalSeconds)}s`;
67
+ const minutes = Math.floor(totalSeconds / 60);
68
+ const remainingSeconds = totalSeconds % 60;
69
+ return `${String(minutes)}m${String(remainingSeconds).padStart(2, "0")}s`;
70
+ };
71
+ const createCopyProgressReporter = (input) => {
72
+ let lastRenderAtMs = 0;
73
+ let lastRenderedLine = "";
74
+ let uploadStartedAtMs = null;
75
+ const clearLiveLine = async () => {
76
+ if (input.interactive && lastRenderedLine.length > 0) {
77
+ await writeOutput(input.output, `\r${" ".repeat(lastRenderedLine.length)}\r`);
78
+ lastRenderedLine = "";
79
+ }
80
+ };
81
+ const render = async (line, force, completeLine = false) => {
82
+ const now = Date.now();
83
+ const interval = input.interactive ? COPY_PROGRESS_RENDER_INTERVAL_MS : COPY_PROGRESS_RENDER_INTERVAL_NON_TTY_MS;
84
+ if (!force && now - lastRenderAtMs < interval) return;
85
+ lastRenderAtMs = now;
86
+ if (input.interactive) {
87
+ const padding = Math.max(0, lastRenderedLine.length - line.length);
88
+ await writeOutput(input.output, `\r${line}${" ".repeat(padding)}${completeLine ? "\n" : ""}`);
89
+ lastRenderedLine = completeLine ? "" : line;
90
+ return;
91
+ }
92
+ await writeOutput(input.output, `${line}\n`);
93
+ };
94
+ const requireUploadStartedAtMs = (label) => {
95
+ if (uploadStartedAtMs === null) throw new Error(`Copy progress ${label} arrived before upload-started`);
96
+ return uploadStartedAtMs;
97
+ };
98
+ const report = async (event) => {
99
+ if (event.type === "archive-preparing") {
100
+ await render("Runneth VM copy: preparing source archive", true);
101
+ return;
102
+ }
103
+ if (event.type === "archive-ready") {
104
+ await render(`Runneth VM copy: archived ${formatCopyByteCount(event.bytesArchived)} in ${String(event.entriesArchived)} entries (${formatCopyByteCount(event.archiveByteLength)} archive); preparing destination upload`, true);
105
+ return;
106
+ }
107
+ if (event.type === "upload-started") {
108
+ uploadStartedAtMs = Date.now();
109
+ await render(`Runneth VM copy: uploading 0 B / ${formatCopyByteCount(event.totalBytes)} (0%)`, true);
110
+ return;
111
+ }
112
+ if (event.type === "range-uploaded") {
113
+ const startedAtMs = requireUploadStartedAtMs(`event ${event.type}`);
114
+ const elapsedSeconds = Math.max(.001, (Date.now() - startedAtMs) / 1e3);
115
+ const bytesPerSecond = event.uploadedBytes / elapsedSeconds;
116
+ const percent = Math.min(100, Math.floor(event.uploadedBytes / event.totalBytes * 100));
117
+ const etaSeconds = (event.totalBytes - event.uploadedBytes) / bytesPerSecond;
118
+ await render(`Runneth VM copy: uploading ${formatCopyByteCount(event.uploadedBytes)} / ${formatCopyByteCount(event.totalBytes)} (${String(percent)}%) at ${formatCopyByteCount(bytesPerSecond)}/s, eta ${formatCopyDuration(etaSeconds)}`, event.uploadedBytes >= event.totalBytes);
119
+ return;
120
+ }
121
+ if (event.type === "archive-applying") {
122
+ await render(`Runneth VM copy: uploaded ${formatCopyByteCount(event.archiveByteLength)}; applying archive`, true, true);
123
+ return;
124
+ }
125
+ throw new Error(`Unknown copy progress event: ${JSON.stringify(event)}`);
126
+ };
127
+ return {
128
+ finishFailure: async (error) => {
129
+ try {
130
+ await clearLiveLine();
131
+ } catch (cleanupError) {
132
+ throw new AggregateError([error, cleanupError], `Runneth VM copy failed: ${formatErrorMessage(error)}; progress output cleanup failed: ${formatErrorMessage(cleanupError)}`);
133
+ }
134
+ throw error;
135
+ },
136
+ report
137
+ };
138
+ };
139
+ //#endregion
10
140
  //#region src/ssh-stdio.ts
11
141
  const ALLOWED_PROCESS_SIGNALS = [
12
142
  "SIGHUP",
@@ -547,8 +677,9 @@ const runRunnethSshStdio = async (options) => {
547
677
  //#region src/cli.ts
548
678
  const DEFAULT_TIMEOUT_MS = 6e5;
549
679
  const DEFAULT_OAUTH_CLIENT_NAME = "Runneth MCP";
550
- const DEFAULT_OAUTH_SCOPE = "openid profile email offline_access";
551
680
  const MAX_STDIN_BYTES = 1048576;
681
+ const RUNNETH_OAUTH_CLIENT_ID_ENV = "RUNNETH_OAUTH_CLIENT_ID";
682
+ const RUNNETH_OAUTH_CLIENT_SECRET_ENV = "RUNNETH_OAUTH_CLIENT_SECRET";
552
683
  const isRecord = (value) => {
553
684
  return typeof value === "object" && value !== null && !Array.isArray(value);
554
685
  };
@@ -620,6 +751,12 @@ const parseNumber = (rawValue, label) => {
620
751
  if (!Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
621
752
  return value;
622
753
  };
754
+ const resolveCliOAuthClientCredentialOptions = (parsed) => {
755
+ return resolveOAuthClientCredentialOptions({
756
+ clientId: parsed.clientId ?? process$1.env[RUNNETH_OAUTH_CLIENT_ID_ENV],
757
+ clientSecret: parsed.clientSecret ?? process$1.env[RUNNETH_OAUTH_CLIENT_SECRET_ENV]
758
+ });
759
+ };
623
760
  const parseArgs = (argv) => {
624
761
  const values = [];
625
762
  let cwd;
@@ -696,10 +833,12 @@ const parseSkillsArgs = (argv) => {
696
833
  };
697
834
  const parseOAuthArgs = (argv) => {
698
835
  const values = [];
836
+ let clientId;
699
837
  let clientName = DEFAULT_OAUTH_CLIENT_NAME;
838
+ let clientSecret;
700
839
  let openBrowser = true;
701
840
  let resourceUrl;
702
- let scope = DEFAULT_OAUTH_SCOPE;
841
+ let scope;
703
842
  let timeoutMs = DEFAULT_TIMEOUT_MS;
704
843
  let parseOptions = true;
705
844
  for (let index = 0; index < argv.length; index += 1) {
@@ -719,9 +858,17 @@ const parseOAuthArgs = (argv) => {
719
858
  return value;
720
859
  };
721
860
  switch (token) {
861
+ case "--client-id":
862
+ case "--clientid":
863
+ clientId = readValue(token);
864
+ break;
722
865
  case "--client-name":
723
866
  clientName = readValue(token);
724
867
  break;
868
+ case "--client-secret":
869
+ case "--secret":
870
+ clientSecret = readValue(token);
871
+ break;
725
872
  case "--no-open":
726
873
  openBrowser = false;
727
874
  break;
@@ -738,7 +885,9 @@ const parseOAuthArgs = (argv) => {
738
885
  }
739
886
  }
740
887
  return {
888
+ ...clientId === void 0 ? {} : { clientId },
741
889
  clientName,
890
+ ...clientSecret === void 0 ? {} : { clientSecret },
742
891
  openBrowser,
743
892
  resourceUrl,
744
893
  scope,
@@ -748,12 +897,14 @@ const parseOAuthArgs = (argv) => {
748
897
  };
749
898
  const parseSshArgs = (argv) => {
750
899
  const values = [];
900
+ let clientId;
751
901
  let clientName = DEFAULT_OAUTH_CLIENT_NAME;
902
+ let clientSecret;
752
903
  let identityFilePath;
753
904
  let makeDefault = false;
754
905
  let openBrowser = true;
755
906
  let resourceUrl;
756
- let scope = DEFAULT_OAUTH_SCOPE;
907
+ let scope;
757
908
  let sshHost;
758
909
  let sshUrl;
759
910
  let targetName;
@@ -779,9 +930,17 @@ const parseSshArgs = (argv) => {
779
930
  return value;
780
931
  };
781
932
  switch (token) {
933
+ case "--client-id":
934
+ case "--clientid":
935
+ clientId = readValue(token);
936
+ break;
782
937
  case "--client-name":
783
938
  clientName = readValue(token);
784
939
  break;
940
+ case "--client-secret":
941
+ case "--secret":
942
+ clientSecret = readValue(token);
943
+ break;
785
944
  case "--default":
786
945
  makeDefault = true;
787
946
  break;
@@ -817,7 +976,9 @@ const parseSshArgs = (argv) => {
817
976
  }
818
977
  return {
819
978
  argumentSeparatorSeen,
979
+ ...clientId === void 0 ? {} : { clientId },
820
980
  clientName,
981
+ ...clientSecret === void 0 ? {} : { clientSecret },
821
982
  ...identityFilePath === void 0 ? {} : { identityFilePath },
822
983
  makeDefault,
823
984
  openBrowser,
@@ -833,11 +994,13 @@ const parseSshArgs = (argv) => {
833
994
  };
834
995
  const parseCopyArgs = (argv) => {
835
996
  const values = [];
997
+ let clientId;
836
998
  let clientName = DEFAULT_OAUTH_CLIENT_NAME;
999
+ let clientSecret;
837
1000
  let destinationTargetName;
838
1001
  const ignoredPaths = [];
839
1002
  let openBrowser = true;
840
- let scope = DEFAULT_OAUTH_SCOPE;
1003
+ let scope;
841
1004
  let sourceTargetName;
842
1005
  let timeoutMs = DEFAULT_TIMEOUT_MS;
843
1006
  let parseOptions = true;
@@ -858,9 +1021,17 @@ const parseCopyArgs = (argv) => {
858
1021
  return value;
859
1022
  };
860
1023
  switch (token) {
1024
+ case "--client-id":
1025
+ case "--clientid":
1026
+ clientId = readValue(token);
1027
+ break;
861
1028
  case "--client-name":
862
1029
  clientName = readValue(token);
863
1030
  break;
1031
+ case "--client-secret":
1032
+ case "--secret":
1033
+ clientSecret = readValue(token);
1034
+ break;
864
1035
  case "--from":
865
1036
  sourceTargetName = normalizeRunnethSshTargetName(readValue(token));
866
1037
  break;
@@ -885,7 +1056,9 @@ const parseCopyArgs = (argv) => {
885
1056
  if (sourceTargetName === void 0) throw new Error("runneth-cli copy requires --from <target>");
886
1057
  if (destinationTargetName === void 0) throw new Error("runneth-cli copy requires --to <target>");
887
1058
  return {
1059
+ ...clientId === void 0 ? {} : { clientId },
888
1060
  clientName,
1061
+ ...clientSecret === void 0 ? {} : { clientSecret },
889
1062
  destinationTargetName,
890
1063
  ignoredPaths,
891
1064
  openBrowser,
@@ -1001,12 +1174,12 @@ const resolveSshTargetImportEntries = (input) => {
1001
1174
  });
1002
1175
  };
1003
1176
  const resolveSshOAuthToken = async (parsed, resourceUrl) => {
1004
- if ((await readOAuthCredentialStatus({ resourceUrl })).authenticated) return await getOAuthAccessToken({ resourceUrl });
1005
- return await loginWithOAuth({
1177
+ return await resolveOAuthToken({
1006
1178
  authorizationUrlHandler: (authorizationUrl) => {
1007
1179
  process$1.stderr.write(`Authorize runneth-cli here:\n${authorizationUrl}\n`);
1008
1180
  },
1009
1181
  clientName: parsed.clientName,
1182
+ ...resolveCliOAuthClientCredentialOptions(parsed),
1010
1183
  openBrowser: parsed.openBrowser,
1011
1184
  resourceUrl,
1012
1185
  scope: parsed.scope,
@@ -1016,6 +1189,45 @@ const resolveSshOAuthToken = async (parsed, resourceUrl) => {
1016
1189
  const writeJson = (response) => {
1017
1190
  process$1.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
1018
1191
  };
1192
+ const formatNullableIdentityValue = (value) => {
1193
+ return value === null ? "(none)" : value;
1194
+ };
1195
+ const formatSshSetupAccount = (setup) => {
1196
+ if (setup.apiVersion === 1) return "(unavailable from API version 1)";
1197
+ return `${setup.provider}:${setup.providerAccountId}`;
1198
+ };
1199
+ const writeSshSetupSummary = (setup) => {
1200
+ const identityLines = setup.apiVersion === 1 ? [` account: ${formatSshSetupAccount(setup)}`, " targetIdentity: unavailable from API version 1"] : [
1201
+ ` account: ${formatSshSetupAccount(setup)}`,
1202
+ ` vmUser: ${setup.userId}`,
1203
+ ` organization: ${setup.organizationId}`,
1204
+ ` sandbox: ${setup.sandboxId}`,
1205
+ ` workspace: ${formatNullableIdentityValue(setup.workspaceId)}`,
1206
+ ` defaultConversation: ${formatNullableIdentityValue(setup.defaultConversationId)}`
1207
+ ];
1208
+ process$1.stderr.write([
1209
+ `SSH target ${setup.targetName ?? setup.hostAlias} configured`,
1210
+ ` config: ${setup.configPath}`,
1211
+ ` hostAlias: ${setup.hostAlias}`,
1212
+ ` sshUser: ${setup.sshUser}`,
1213
+ ...identityLines,
1214
+ ""
1215
+ ].join("\n"));
1216
+ };
1217
+ const writeCopyTargetSummary = (input) => {
1218
+ process$1.stderr.write([
1219
+ "Runneth VM copy target binding",
1220
+ ` source: ${input.source.targetName ?? input.source.sshUrl}`,
1221
+ ` sourceResource: ${input.source.resourceUrl}`,
1222
+ ` sourceSsh: ${input.source.sshUrl}`,
1223
+ ` sourcePath: ${input.sourcePath}`,
1224
+ ` destination: ${input.destination.targetName ?? input.destination.sshUrl}`,
1225
+ ` destinationResource: ${input.destination.resourceUrl}`,
1226
+ ` destinationSsh: ${input.destination.sshUrl}`,
1227
+ ` destinationPath: ${input.destinationPath}`,
1228
+ ""
1229
+ ].join("\n"));
1230
+ };
1019
1231
  const runDaemon = async (argv) => {
1020
1232
  const socketPath = parseArgs(argv).values[0] ?? resolveSocketPath();
1021
1233
  await new RunnethCliDaemon().start({ socketPath });
@@ -1091,7 +1303,11 @@ const run = async (argv) => {
1091
1303
  " --workspace <id> Mondrian/Builder workspace id; may also use RUNNETH_WORKSPACE_ID",
1092
1304
  " --token <token> Bearer token override; omitted uses stored OAuth or starts login",
1093
1305
  " --auth <mode> mondrian or builder. Defaults to mondrian",
1306
+ " --client-id, --clientid <id>",
1307
+ " OAuth client id; may use RUNNETH_OAUTH_CLIENT_ID",
1094
1308
  " --client-name <name> OAuth dynamic client name. Defaults to Runneth MCP",
1309
+ " --client-secret, --secret <value>",
1310
+ " OAuth client secret; prefer RUNNETH_OAUTH_CLIENT_SECRET",
1095
1311
  " --conversation <id> Resume an existing conversation",
1096
1312
  " --no-open Print the OAuth URL instead of opening a browser",
1097
1313
  " --scope <scope> OAuth scope. Defaults to openid profile email offline_access",
@@ -1102,6 +1318,7 @@ const run = async (argv) => {
1102
1318
  "",
1103
1319
  "Environment:",
1104
1320
  " RUNNETH_API_URL, RUNNETH_RESOURCE_URL, RUNNETH_WORKSPACE_ID",
1321
+ " RUNNETH_OAUTH_CLIENT_ID, RUNNETH_OAUTH_CLIENT_SECRET",
1105
1322
  " RUNNETH_TOKEN, MONDRIAN_TOKEN, BUILDER_AGENT_CHAT_TOKEN",
1106
1323
  "",
1107
1324
  "Commands inside chat:",
@@ -1128,7 +1345,11 @@ const run = async (argv) => {
1128
1345
  "Usage: runneth-cli oauth <login|status|logout> [--resource <url>] [options]",
1129
1346
  "",
1130
1347
  "Options:",
1348
+ " --client-id, --clientid <id>",
1349
+ " OAuth client id; may use RUNNETH_OAUTH_CLIENT_ID",
1131
1350
  " --client-name <name> Dynamic OAuth client name",
1351
+ " --client-secret, --secret <val>",
1352
+ " OAuth client secret; prefer RUNNETH_OAUTH_CLIENT_SECRET",
1132
1353
  " --no-open Print the authorization URL without opening a browser",
1133
1354
  " --resource <url> OAuth-protected MCP resource URL, defaults to build MONDRIAN_API_URL/mcp",
1134
1355
  " --scope <scope> OAuth scopes",
@@ -1145,6 +1366,7 @@ const run = async (argv) => {
1145
1366
  process$1.stderr.write(`Authorize runneth-cli here:\n${authorizationUrl}\n`);
1146
1367
  },
1147
1368
  clientName: parsed.clientName,
1369
+ ...resolveCliOAuthClientCredentialOptions(parsed),
1148
1370
  openBrowser: parsed.openBrowser,
1149
1371
  resourceUrl,
1150
1372
  scope: parsed.scope,
@@ -1191,7 +1413,11 @@ const run = async (argv) => {
1191
1413
  " --from <target> Source SSH target name",
1192
1414
  " --to <target> Destination SSH target name",
1193
1415
  " --ignore <path> Additional relative folder/path to skip; node_modules is skipped by default",
1416
+ " --client-id, --clientid <id>",
1417
+ " OAuth client id; may use RUNNETH_OAUTH_CLIENT_ID",
1194
1418
  " --client-name <name> Dynamic OAuth client name",
1419
+ " --client-secret, --secret <val>",
1420
+ " OAuth client secret; prefer RUNNETH_OAUTH_CLIENT_SECRET",
1195
1421
  " --no-open Print the authorization URL without opening a browser",
1196
1422
  " --scope <scope> OAuth scopes",
1197
1423
  " --timeout-ms <ms> Copy command timeout",
@@ -1212,20 +1438,35 @@ const run = async (argv) => {
1212
1438
  destination: destinationTarget,
1213
1439
  source: sourceTarget
1214
1440
  });
1441
+ writeCopyTargetSummary({
1442
+ destination: destinationTarget,
1443
+ destinationPath,
1444
+ source: sourceTarget,
1445
+ sourcePath
1446
+ });
1215
1447
  const oauthToken = await resolveSshOAuthToken(parsed, sourceTarget.resourceUrl);
1216
- writeJson({
1217
- ok: true,
1218
- op: "copy",
1219
- ...await runRunnethVmCopy({
1220
- destination: destinationTarget,
1221
- destinationPath,
1222
- ignoredPaths: parsed.ignoredPaths,
1223
- oauthToken,
1224
- source: sourceTarget,
1225
- sourcePath,
1226
- timeoutMs: parsed.timeoutMs
1227
- })
1448
+ const copyProgressReporter = createCopyProgressReporter({
1449
+ interactive: process$1.stderr.isTTY === true,
1450
+ output: process$1.stderr
1228
1451
  });
1452
+ try {
1453
+ writeJson({
1454
+ ok: true,
1455
+ op: "copy",
1456
+ ...await runRunnethVmCopy({
1457
+ destination: destinationTarget,
1458
+ destinationPath,
1459
+ ignoredPaths: parsed.ignoredPaths,
1460
+ oauthToken,
1461
+ onProgress: copyProgressReporter.report,
1462
+ source: sourceTarget,
1463
+ sourcePath,
1464
+ timeoutMs: parsed.timeoutMs
1465
+ })
1466
+ });
1467
+ } catch (error) {
1468
+ await copyProgressReporter.finishFailure(error);
1469
+ }
1229
1470
  return;
1230
1471
  }
1231
1472
  if (command === "skills") {
@@ -1270,7 +1511,11 @@ const run = async (argv) => {
1270
1511
  " target Manage named Runneth SSH VM targets",
1271
1512
  "",
1272
1513
  "Options:",
1514
+ " --client-id, --clientid <id>",
1515
+ " OAuth client id; may use RUNNETH_OAUTH_CLIENT_ID",
1273
1516
  " --client-name <name> Dynamic OAuth client name",
1517
+ " --client-secret, --secret <val>",
1518
+ " OAuth client secret; prefer RUNNETH_OAUTH_CLIENT_SECRET",
1274
1519
  " --no-open Print the authorization URL without opening a browser",
1275
1520
  " --resource <url> OAuth-protected MCP resource URL, defaults to build MONDRIAN_API_URL/mcp when using --host or --ssh-url",
1276
1521
  " --scope <scope> OAuth scopes",
@@ -1448,7 +1693,7 @@ const run = async (argv) => {
1448
1693
  ...resolveSshKeyInstallOptions(parsed),
1449
1694
  ...target.targetName === void 0 ? {} : { targetName: target.targetName }
1450
1695
  });
1451
- process$1.stderr.write(`SSH target ${setup.hostAlias} configured with ${setup.configPath}\n`);
1696
+ writeSshSetupSummary(setup);
1452
1697
  await runRunnethSshStdio({
1453
1698
  defaultTimeoutMs: parsed.timeoutMs,
1454
1699
  setup
@@ -1468,7 +1713,7 @@ const run = async (argv) => {
1468
1713
  ...resolveSshKeyInstallOptions(parsed),
1469
1714
  ...target.targetName === void 0 ? {} : { targetName: target.targetName }
1470
1715
  });
1471
- process$1.stderr.write(`SSH target ${setup.hostAlias} configured with ${setup.configPath}\n`);
1716
+ writeSshSetupSummary(setup);
1472
1717
  process$1.exitCode = await runOpenSsh({
1473
1718
  extraArgs: parsed.values,
1474
1719
  setup