terminal-commands 0.1.2 → 0.2.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/cli.js CHANGED
@@ -4155,13 +4155,14 @@ var require_fast_uri = __commonJS({
4155
4155
  if (!malformedIPLiteral) {
4156
4156
  malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4157
4157
  }
4158
- if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4159
- if (uri.indexOf("%") !== -1) {
4160
- if (parsed.host !== void 0 && !malformedIPLiteral) {
4161
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4162
- parsed.host = reescapeHostDelimiters(host, isIP);
4163
- }
4158
+ if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
4159
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4160
+ if (!isIP) {
4161
+ host = normalizePercentEncoding(host.toLowerCase());
4164
4162
  }
4163
+ parsed.host = reescapeHostDelimiters(host, isIP);
4164
+ }
4165
+ if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4165
4166
  if (parsed.path) {
4166
4167
  parsed.path = normalizePathEncoding(parsed.path);
4167
4168
  }
@@ -9548,7 +9549,7 @@ var require_websocket = __commonJS({
9548
9549
  var http = __require("http");
9549
9550
  var net = __require("net");
9550
9551
  var tls = __require("tls");
9551
- var { randomBytes, createHash } = __require("crypto");
9552
+ var { randomBytes: randomBytes3, createHash } = __require("crypto");
9552
9553
  var { Duplex, Readable } = __require("stream");
9553
9554
  var { URL: URL2 } = __require("url");
9554
9555
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -10086,7 +10087,7 @@ var require_websocket = __commonJS({
10086
10087
  }
10087
10088
  }
10088
10089
  const defaultPort = isSecure ? 443 : 80;
10089
- const key = randomBytes(16).toString("base64");
10090
+ const key = randomBytes3(16).toString("base64");
10090
10091
  const request = isSecure ? https.request : http.request;
10091
10092
  const protocolSet = /* @__PURE__ */ new Set();
10092
10093
  let perMessageDeflate;
@@ -10981,7 +10982,9 @@ var require_websocket_server = __commonJS({
10981
10982
 
10982
10983
  // src/cli.ts
10983
10984
  import { spawn } from "node:child_process";
10985
+ import { randomBytes as randomBytes2 } from "node:crypto";
10984
10986
  import { fileURLToPath } from "node:url";
10987
+ import { createInterface as createInterface2 } from "node:readline/promises";
10985
10988
  import os5 from "node:os";
10986
10989
  import path3 from "node:path";
10987
10990
 
@@ -32309,6 +32312,9 @@ var Protocol = class {
32309
32312
  this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => {
32310
32313
  const handleTaskResult = async () => {
32311
32314
  const taskId = request.params.taskId;
32315
+ if (!await this._taskStore.getTask(taskId, extra.sessionId)) {
32316
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
32317
+ }
32312
32318
  if (this._taskMessageQueue) {
32313
32319
  let queuedMessage;
32314
32320
  while (queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId)) {
@@ -32339,12 +32345,12 @@ var Protocol = class {
32339
32345
  throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
32340
32346
  }
32341
32347
  if (!isTerminal(task.status)) {
32342
- await this._waitForTaskUpdate(taskId, extra.signal);
32348
+ await this._waitForTaskUpdate(taskId, extra.signal, extra.sessionId);
32343
32349
  return await handleTaskResult();
32344
32350
  }
32345
32351
  if (isTerminal(task.status)) {
32346
32352
  const result = await this._taskStore.getTaskResult(taskId, extra.sessionId);
32347
- this._clearTaskQueue(taskId);
32353
+ this._clearTaskQueue(taskId, extra.sessionId);
32348
32354
  return {
32349
32355
  ...result,
32350
32356
  _meta: {
@@ -32381,7 +32387,7 @@ var Protocol = class {
32381
32387
  throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
32382
32388
  }
32383
32389
  await this._taskStore.updateTaskStatus(request.params.taskId, "cancelled", "Client cancelled task execution.", extra.sessionId);
32384
- this._clearTaskQueue(request.params.taskId);
32390
+ this._clearTaskQueue(request.params.taskId, extra.sessionId);
32385
32391
  const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId);
32386
32392
  if (!cancelledTask) {
32387
32393
  throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
@@ -32509,6 +32515,19 @@ var Protocol = class {
32509
32515
  const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
32510
32516
  const capturedTransport = this._transport;
32511
32517
  const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
32518
+ const sessionId = capturedTransport?.sessionId;
32519
+ const store = this._taskStore;
32520
+ let relatedTaskFound = true;
32521
+ let relatedTaskLookup;
32522
+ if (relatedTaskId && store && this._taskMessageQueue && sessionId !== void 0) {
32523
+ relatedTaskFound = false;
32524
+ relatedTaskLookup = (async () => {
32525
+ if (!await store.getTask(relatedTaskId, sessionId)) {
32526
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${relatedTaskId}`);
32527
+ }
32528
+ relatedTaskFound = true;
32529
+ })();
32530
+ }
32512
32531
  if (handler === void 0) {
32513
32532
  const errorResponse = {
32514
32533
  jsonrpc: "2.0",
@@ -32518,7 +32537,10 @@ var Protocol = class {
32518
32537
  message: "Method not found"
32519
32538
  }
32520
32539
  };
32521
- if (relatedTaskId && this._taskMessageQueue) {
32540
+ if (relatedTaskId && relatedTaskLookup) {
32541
+ const queuedError = { type: "error", message: errorResponse, timestamp: Date.now() };
32542
+ relatedTaskLookup.then(() => this._enqueueTaskMessage(relatedTaskId, queuedError, sessionId), () => capturedTransport?.send(errorResponse)).catch((error62) => this._onerror(new Error(`Failed to send an error response: ${error62}`)));
32543
+ } else if (relatedTaskId && this._taskMessageQueue) {
32522
32544
  this._enqueueTaskMessage(relatedTaskId, {
32523
32545
  type: "error",
32524
32546
  message: errorResponse,
@@ -32569,7 +32591,10 @@ var Protocol = class {
32569
32591
  closeSSEStream: extra?.closeSSEStream,
32570
32592
  closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
32571
32593
  };
32572
- Promise.resolve().then(() => {
32594
+ (relatedTaskLookup ?? Promise.resolve()).then(() => {
32595
+ if (relatedTaskLookup && abortController.signal.aborted) {
32596
+ throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
32597
+ }
32573
32598
  if (taskCreationParams) {
32574
32599
  this.assertTaskHandlerCapability(request.method);
32575
32600
  }
@@ -32604,7 +32629,7 @@ var Protocol = class {
32604
32629
  ...error62["data"] !== void 0 && { data: error62["data"] }
32605
32630
  }
32606
32631
  };
32607
- if (relatedTaskId && this._taskMessageQueue) {
32632
+ if (relatedTaskId && this._taskMessageQueue && relatedTaskFound) {
32608
32633
  await this._enqueueTaskMessage(relatedTaskId, {
32609
32634
  type: "error",
32610
32635
  message: errorResponse,
@@ -33084,7 +33109,7 @@ var Protocol = class {
33084
33109
  throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured");
33085
33110
  }
33086
33111
  const maxQueueSize = this._options?.maxTaskQueueSize;
33087
- await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize);
33112
+ await this._taskMessageQueue.enqueue(taskId, message, sessionId ?? this._transport?.sessionId, maxQueueSize);
33088
33113
  }
33089
33114
  /**
33090
33115
  * Clears the message queue for a task and rejects any pending request resolvers.
@@ -33113,12 +33138,13 @@ var Protocol = class {
33113
33138
  * Uses polling to check for updates at the task's configured poll interval.
33114
33139
  * @param taskId The task ID to wait for
33115
33140
  * @param signal Abort signal to cancel the wait
33141
+ * @param sessionId Session of the request that waits, passed to the task store
33116
33142
  * @returns Promise that resolves when an update occurs or rejects if aborted
33117
33143
  */
33118
- async _waitForTaskUpdate(taskId, signal) {
33144
+ async _waitForTaskUpdate(taskId, signal, sessionId) {
33119
33145
  let interval = this._options?.defaultTaskPollInterval ?? 1e3;
33120
33146
  try {
33121
- const task = await this._taskStore?.getTask(taskId);
33147
+ const task = await this._taskStore?.getTask(taskId, sessionId);
33122
33148
  if (task?.pollInterval) {
33123
33149
  interval = task.pollInterval;
33124
33150
  }
@@ -34033,6 +34059,68 @@ function createFetchWithInit(baseFetch = fetch, baseInit) {
34033
34059
  return baseFetch(url2, mergedInit);
34034
34060
  };
34035
34061
  }
34062
+ var MAX_REDIRECTS = 5;
34063
+ var REDIRECT_STATUSES = [301, 302, 303, 307, 308];
34064
+ function isWithinOrigin(from, to) {
34065
+ if (from.protocol === to.protocol && from.hostname === to.hostname && from.port === to.port) {
34066
+ return true;
34067
+ }
34068
+ return from.hostname === to.hostname && from.protocol === "http:" && from.port === "" && to.protocol === "https:" && to.port === "";
34069
+ }
34070
+ function redirectTarget(response, requestUrl) {
34071
+ const location = REDIRECT_STATUSES.includes(response.status) ? response.headers.get("location") : null;
34072
+ try {
34073
+ return location ? new URL(location, requestUrl) : void 0;
34074
+ } catch {
34075
+ return void 0;
34076
+ }
34077
+ }
34078
+ function followWithinOrigin(baseFetch, url2, init, response, followed = 0) {
34079
+ const target = redirectTarget(response, url2);
34080
+ const keepsMethod = response.status === 307 || response.status === 308 || (init?.method ?? "GET").toUpperCase() === "GET";
34081
+ if (!target || followed === MAX_REDIRECTS || !keepsMethod) {
34082
+ return void 0;
34083
+ }
34084
+ const from = new URL(url2);
34085
+ const addsUserinfo = (target.username || target.password) && (target.username !== from.username || target.password !== from.password);
34086
+ if (addsUserinfo || !isWithinOrigin(from, target)) {
34087
+ return void 0;
34088
+ }
34089
+ return Promise.resolve(response.body?.cancel()).then(() => baseFetch(target, { ...init, redirect: "manual" })).then((next) => followWithinOrigin(baseFetch, target, init, next, followed + 1) ?? next);
34090
+ }
34091
+ var followingFetches = /* @__PURE__ */ new WeakSet();
34092
+ function fetchLeavingRedirects(baseFetch) {
34093
+ const following = (url2, init) => (baseFetch ?? fetch)(url2, init);
34094
+ followingFetches.add(following);
34095
+ return following;
34096
+ }
34097
+ function fetchWithinOrigin(baseFetch = fetch) {
34098
+ if (followingFetches.has(baseFetch)) {
34099
+ return baseFetch;
34100
+ }
34101
+ return (url2, init) => {
34102
+ if (init?.redirect === "error" || init?.redirect === "manual") {
34103
+ return baseFetch(url2, init);
34104
+ }
34105
+ return Promise.resolve(baseFetch(url2, { ...init, redirect: "manual" })).then((response) => followWithinOrigin(baseFetch, url2, init, response) ?? response);
34106
+ };
34107
+ }
34108
+ function unfollowedRedirect(response, requestUrl) {
34109
+ if (response.type === "opaqueredirect") {
34110
+ return "Redirect not followed: this runtime does not expose the redirect target";
34111
+ }
34112
+ const from = response.url || requestUrl;
34113
+ const target = redirectTarget(response, from);
34114
+ if (!target) {
34115
+ return void 0;
34116
+ }
34117
+ target.username = target.password = target.search = target.hash = "";
34118
+ if (target.protocol === "http:" && new URL(from).protocol === "https:") {
34119
+ target.protocol = "https:";
34120
+ return `Redirect to plain http not followed; try ${target.href} instead`;
34121
+ }
34122
+ return `Redirect to ${target.href} not followed`;
34123
+ }
34036
34124
 
34037
34125
  // node_modules/pkce-challenge/dist/index.node.js
34038
34126
  var crypto;
@@ -34047,8 +34135,8 @@ async function random(size) {
34047
34135
  const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
34048
34136
  let result = "";
34049
34137
  while (result.length < size) {
34050
- const randomBytes = await getRandomValues(size - result.length);
34051
- for (const randomByte of randomBytes) {
34138
+ const randomBytes3 = await getRandomValues(size - result.length);
34139
+ for (const randomByte of randomBytes3) {
34052
34140
  if (randomByte < evenDistCutoff) {
34053
34141
  result += mask[randomByte % mask.length];
34054
34142
  }
@@ -34179,7 +34267,12 @@ var OAuthTokensSchema = object2({
34179
34267
  token_type: string2(),
34180
34268
  expires_in: coerce_exports.number().optional(),
34181
34269
  scope: string2().optional(),
34182
- refresh_token: string2().optional()
34270
+ refresh_token: string2().optional(),
34271
+ /**
34272
+ * Not part of the wire format: the authorization server this value was obtained from, added by
34273
+ * the client's `auth()` before it is stored and compared when it is read back.
34274
+ */
34275
+ issuer: string2().optional().catch(void 0)
34183
34276
  }).strip();
34184
34277
  var OAuthErrorResponseSchema = object2({
34185
34278
  error: string2(),
@@ -34209,7 +34302,12 @@ var OAuthClientInformationSchema = object2({
34209
34302
  client_id: string2(),
34210
34303
  client_secret: string2().optional(),
34211
34304
  client_id_issued_at: number2().optional(),
34212
- client_secret_expires_at: number2().optional()
34305
+ client_secret_expires_at: number2().optional(),
34306
+ /**
34307
+ * Not part of the wire format: the authorization server this value was obtained from, added by
34308
+ * the client's `auth()` before it is stored and compared when it is read back.
34309
+ */
34310
+ issuer: string2().optional().catch(void 0)
34213
34311
  }).strip();
34214
34312
  var OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema);
34215
34313
  var OAuthClientRegistrationErrorSchema = object2({
@@ -34407,10 +34505,31 @@ async function parseErrorResponse(input2) {
34407
34505
  const errorClass = OAUTH_ERRORS[error62] || ServerError;
34408
34506
  return new errorClass(error_description || "", error_uri);
34409
34507
  } catch (error62) {
34410
- const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error62}. Raw body: ${body}`;
34508
+ const redirect = input2 instanceof Response ? unfollowedRedirect(input2, input2.url) : void 0;
34509
+ const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ""}${redirect ?? `Invalid OAuth error response: ${error62}. Raw body: ${body}`}`;
34411
34510
  return new ServerError(errorMessage);
34412
34511
  }
34413
34512
  }
34513
+ function issuersMatch(a, b) {
34514
+ let [x, y] = [a, b];
34515
+ try {
34516
+ [x, y] = [new URL(a).href, new URL(b).href];
34517
+ } catch {
34518
+ }
34519
+ return x === y || x.endsWith("/") && x.slice(0, -1) === y || y.endsWith("/") && y.slice(0, -1) === x;
34520
+ }
34521
+ function discardIfIssuerMismatch(stored, issuer) {
34522
+ if (!stored)
34523
+ return void 0;
34524
+ if (typeof stored.issuer !== "string")
34525
+ return stored.issuer == null ? stored : { ...stored, issuer: void 0 };
34526
+ return issuersMatch(stored.issuer, issuer) ? stored : void 0;
34527
+ }
34528
+ var TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true });
34529
+ var RegistrationResponseSchema = OAuthClientInformationFullSchema.omit({ issuer: true });
34530
+ function boundElsewhereError(stored, issuer) {
34531
+ return new Error(`OAuth client information is bound to authorization server ${stored.issuer} and is not presented to ${issuer}. Clear the stored client information, or correct \`expectedIssuer\`, if the authorization server has moved.`);
34532
+ }
34414
34533
  async function auth(provider, options) {
34415
34534
  try {
34416
34535
  return await authInternal(provider, options);
@@ -34464,9 +34583,24 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
34464
34583
  authorizationServerMetadata: metadata
34465
34584
  });
34466
34585
  }
34467
- const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
34586
+ const issuer = String(authorizationServerUrl);
34587
+ const selectedResource = await selectResourceURL(serverUrl, provider, resourceMetadata);
34588
+ const resource = selectedResource && resourceMetadata && !provider.validateResourceURL ? resourceMetadata.resource : selectedResource;
34468
34589
  const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(" ") || provider.clientMetadata.scope;
34469
- let clientInformation = await Promise.resolve(provider.clientInformation());
34590
+ const storedClientInformation = await Promise.resolve(provider.clientInformation());
34591
+ let clientInformation = discardIfIssuerMismatch(storedClientInformation, issuer);
34592
+ const canRegisterAgain = provider.saveClientInformation !== void 0 && !!provider.redirectUrl;
34593
+ if (storedClientInformation && !clientInformation && !canRegisterAgain) {
34594
+ throw boundElsewhereError(storedClientInformation, issuer);
34595
+ }
34596
+ const unstampedClientInformation = clientInformation?.issuer == null ? clientInformation : void 0;
34597
+ const bindClientInformation = async () => {
34598
+ try {
34599
+ if (unstampedClientInformation)
34600
+ await provider.saveClientInformation?.({ ...unstampedClientInformation, issuer });
34601
+ } catch {
34602
+ }
34603
+ };
34470
34604
  if (!clientInformation) {
34471
34605
  if (authorizationCode !== void 0) {
34472
34606
  throw new Error("Existing OAuth client information is required when exchanging an authorization code");
@@ -34478,9 +34612,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
34478
34612
  }
34479
34613
  const shouldUseUrlBasedClientId = supportsUrlBasedClientId && clientMetadataUrl;
34480
34614
  if (shouldUseUrlBasedClientId) {
34481
- clientInformation = {
34482
- client_id: clientMetadataUrl
34483
- };
34615
+ clientInformation = { client_id: clientMetadataUrl, issuer };
34484
34616
  await provider.saveClientInformation?.(clientInformation);
34485
34617
  } else {
34486
34618
  if (!provider.saveClientInformation) {
@@ -34492,8 +34624,8 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
34492
34624
  scope: resolvedScope,
34493
34625
  fetchFn
34494
34626
  });
34495
- await provider.saveClientInformation(fullInformation);
34496
- clientInformation = fullInformation;
34627
+ clientInformation = { ...fullInformation, issuer };
34628
+ await provider.saveClientInformation(clientInformation);
34497
34629
  }
34498
34630
  }
34499
34631
  const nonInteractiveFlow = !provider.redirectUrl;
@@ -34504,10 +34636,14 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
34504
34636
  authorizationCode,
34505
34637
  fetchFn
34506
34638
  });
34507
- await provider.saveTokens(tokens2);
34639
+ await bindClientInformation();
34640
+ await provider.saveTokens({ ...tokens2, issuer });
34508
34641
  return "AUTHORIZED";
34509
34642
  }
34510
- const tokens = await provider.tokens();
34643
+ const tokens = discardIfIssuerMismatch(await provider.tokens(), issuer);
34644
+ if (tokens?.refresh_token && tokens.issuer == null) {
34645
+ console.warn("[mcp-sdk] stored OAuth tokens have no 'issuer' property (saved by an earlier version, or by a provider that does not keep it) and are used as-is; make sure your OAuthClientProvider stores what saveTokens() and saveClientInformation() receive unchanged.");
34646
+ }
34511
34647
  if (tokens?.refresh_token) {
34512
34648
  try {
34513
34649
  const newTokens = await refreshAuthorization(authorizationServerUrl, {
@@ -34518,7 +34654,8 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
34518
34654
  addClientAuthentication: provider.addClientAuthentication,
34519
34655
  fetchFn
34520
34656
  });
34521
- await provider.saveTokens(newTokens);
34657
+ await bindClientInformation();
34658
+ await provider.saveTokens({ ...newTokens, issuer });
34522
34659
  return "AUTHORIZED";
34523
34660
  } catch (error62) {
34524
34661
  if (!(error62 instanceof OAuthError) || error62 instanceof ServerError) {
@@ -34617,7 +34754,7 @@ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn =
34617
34754
  }
34618
34755
  async function fetchWithCorsRetry(url2, headers, fetchFn = fetch) {
34619
34756
  try {
34620
- return await fetchFn(url2, { headers });
34757
+ return await fetchWithinOrigin(fetchFn)(url2, { headers });
34621
34758
  } catch (error62) {
34622
34759
  if (error62 instanceof TypeError) {
34623
34760
  if (headers) {
@@ -34642,7 +34779,7 @@ async function tryMetadataDiscovery(url2, protocolVersion, fetchFn = fetch) {
34642
34779
  return await fetchWithCorsRetry(url2, headers, fetchFn);
34643
34780
  }
34644
34781
  function shouldAttemptFallback(response, pathname) {
34645
- return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
34782
+ return !response || !response.ok && response.status < 500 && pathname !== "/";
34646
34783
  }
34647
34784
  async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
34648
34785
  const issuer = new URL(serverUrl);
@@ -34708,7 +34845,7 @@ async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fet
34708
34845
  }
34709
34846
  if (!response.ok) {
34710
34847
  await response.body?.cancel();
34711
- if (response.status >= 400 && response.status < 500) {
34848
+ if (response.status < 500) {
34712
34849
  continue;
34713
34850
  }
34714
34851
  throw new Error(`HTTP ${response.status} trying to load ${type === "oauth" ? "OAuth" : "OpenID provider"} metadata from ${endpointUrl}`);
@@ -34741,6 +34878,9 @@ async function discoverOAuthServerInfo(serverUrl, opts) {
34741
34878
  resourceMetadata
34742
34879
  };
34743
34880
  }
34881
+ function resourceIndicatorToString(resource) {
34882
+ return typeof resource === "string" ? resource : resource.href;
34883
+ }
34744
34884
  async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) {
34745
34885
  let authorizationUrl;
34746
34886
  if (metadata) {
@@ -34772,7 +34912,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
34772
34912
  authorizationUrl.searchParams.append("prompt", "consent");
34773
34913
  }
34774
34914
  if (resource) {
34775
- authorizationUrl.searchParams.set("resource", resource.href);
34915
+ authorizationUrl.searchParams.set("resource", resourceIndicatorToString(resource));
34776
34916
  }
34777
34917
  return { authorizationUrl, codeVerifier };
34778
34918
  }
@@ -34791,7 +34931,7 @@ async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequ
34791
34931
  Accept: "application/json"
34792
34932
  });
34793
34933
  if (resource) {
34794
- tokenRequestParams.set("resource", resource.href);
34934
+ tokenRequestParams.set("resource", resourceIndicatorToString(resource));
34795
34935
  }
34796
34936
  if (addClientAuthentication) {
34797
34937
  await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
@@ -34800,7 +34940,7 @@ async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequ
34800
34940
  const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
34801
34941
  applyClientAuthentication(authMethod, clientInformation, headers, tokenRequestParams);
34802
34942
  }
34803
- const response = await (fetchFn ?? fetch)(tokenUrl, {
34943
+ const response = await fetchWithinOrigin(fetchFn ?? fetch)(tokenUrl, {
34804
34944
  method: "POST",
34805
34945
  headers,
34806
34946
  body: tokenRequestParams
@@ -34808,7 +34948,7 @@ async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequ
34808
34948
  if (!response.ok) {
34809
34949
  throw await parseErrorResponse(response);
34810
34950
  }
34811
- return OAuthTokensSchema.parse(await response.json());
34951
+ return TokenResponseSchema.parse(await response.json());
34812
34952
  }
34813
34953
  async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
34814
34954
  const tokenRequestParams = new URLSearchParams({
@@ -34826,6 +34966,15 @@ async function refreshAuthorization(authorizationServerUrl, { metadata, clientIn
34826
34966
  return { refresh_token: refreshToken, ...tokens };
34827
34967
  }
34828
34968
  async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, fetchFn } = {}) {
34969
+ const readClientInformation = async () => {
34970
+ const storedClientInformation = await provider.clientInformation();
34971
+ const checked = discardIfIssuerMismatch(storedClientInformation, String(authorizationServerUrl));
34972
+ if (storedClientInformation && !checked) {
34973
+ throw boundElsewhereError(storedClientInformation, String(authorizationServerUrl));
34974
+ }
34975
+ return checked;
34976
+ };
34977
+ let clientInformation = await readClientInformation();
34829
34978
  const scope = provider.clientMetadata.scope;
34830
34979
  let tokenRequestParams;
34831
34980
  if (provider.prepareTokenRequest) {
@@ -34841,11 +34990,11 @@ async function fetchToken(provider, authorizationServerUrl, { metadata, resource
34841
34990
  const codeVerifier = await provider.codeVerifier();
34842
34991
  tokenRequestParams = prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, provider.redirectUrl);
34843
34992
  }
34844
- const clientInformation = await provider.clientInformation();
34993
+ clientInformation ?? (clientInformation = await readClientInformation());
34845
34994
  return executeTokenRequest(authorizationServerUrl, {
34846
34995
  metadata,
34847
34996
  tokenRequestParams,
34848
- clientInformation: clientInformation ?? void 0,
34997
+ clientInformation,
34849
34998
  addClientAuthentication: provider.addClientAuthentication,
34850
34999
  resource,
34851
35000
  fetchFn
@@ -34861,7 +35010,7 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
34861
35010
  } else {
34862
35011
  registrationUrl = new URL("/register", authorizationServerUrl);
34863
35012
  }
34864
- const response = await (fetchFn ?? fetch)(registrationUrl, {
35013
+ const response = await fetchWithinOrigin(fetchFn ?? fetch)(registrationUrl, {
34865
35014
  method: "POST",
34866
35015
  headers: {
34867
35016
  "Content-Type": "application/json"
@@ -34874,7 +35023,7 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
34874
35023
  if (!response.ok) {
34875
35024
  throw await parseErrorResponse(response);
34876
35025
  }
34877
- return OAuthClientInformationFullSchema.parse(await response.json());
35026
+ return RegistrationResponseSchema.parse(await response.json());
34878
35027
  }
34879
35028
 
34880
35029
  // node_modules/eventsource-parser/dist/index.js
@@ -35101,9 +35250,19 @@ var StreamableHTTPClientTransport = class {
35101
35250
  this._authProvider = opts?.authProvider;
35102
35251
  this._fetch = opts?.fetch;
35103
35252
  this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
35253
+ this._followRedirects = opts?.redirectPolicy === "follow";
35254
+ if (this._followRedirects) {
35255
+ this._fetch = fetchLeavingRedirects(this._fetch);
35256
+ this._fetchWithInit = fetchLeavingRedirects(this._fetchWithInit);
35257
+ }
35104
35258
  this._sessionId = opts?.sessionId;
35105
35259
  this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
35106
35260
  }
35261
+ /** Error text for a redirect `response` that was not followed, or `undefined` for any other response. */
35262
+ _unfollowedRedirect(response, url2) {
35263
+ const text = unfollowedRedirect(response, url2);
35264
+ return text && !this._followRedirects ? `${text} (redirectPolicy: 'same-origin')` : text;
35265
+ }
35107
35266
  async _authThenStart() {
35108
35267
  if (!this._authProvider) {
35109
35268
  throw new UnauthorizedError("No auth provider");
@@ -35153,11 +35312,13 @@ var StreamableHTTPClientTransport = class {
35153
35312
  if (resumptionToken) {
35154
35313
  headers.set("last-event-id", resumptionToken);
35155
35314
  }
35156
- const response = await (this._fetch ?? fetch)(this._url, {
35157
- method: "GET",
35158
- headers,
35159
- signal: this._abortController?.signal
35160
- });
35315
+ const fetchFn = this._fetch ?? fetch;
35316
+ const init = { method: "GET", headers, signal: this._abortController?.signal };
35317
+ let response = await fetchFn(this._url, this._followRedirects ? init : { ...init, redirect: "manual" });
35318
+ const followed = this._followRedirects ? void 0 : followWithinOrigin(fetchFn, this._url, init, response);
35319
+ if (followed) {
35320
+ response = await followed;
35321
+ }
35161
35322
  if (!response.ok) {
35162
35323
  await response.body?.cancel();
35163
35324
  if (response.status === 401 && this._authProvider) {
@@ -35166,7 +35327,7 @@ var StreamableHTTPClientTransport = class {
35166
35327
  if (response.status === 405) {
35167
35328
  return;
35168
35329
  }
35169
- throw new StreamableHTTPError(response.status, `Failed to open SSE stream: ${response.statusText}`);
35330
+ throw new StreamableHTTPError(response.status, `Failed to open SSE stream: ${this._unfollowedRedirect(response, this._url) ?? response.statusText}`);
35170
35331
  }
35171
35332
  this._handleSseStream(response.body, options, true);
35172
35333
  } catch (error62) {
@@ -35329,7 +35490,7 @@ var StreamableHTTPClientTransport = class {
35329
35490
  body: JSON.stringify(message),
35330
35491
  signal: this._abortController?.signal
35331
35492
  };
35332
- const response = await (this._fetch ?? fetch)(this._url, init);
35493
+ const response = await fetchWithinOrigin(this._fetch ?? fetch)(this._url, init);
35333
35494
  const sessionId = response.headers.get("mcp-session-id");
35334
35495
  if (sessionId) {
35335
35496
  this._sessionId = sessionId;
@@ -35381,7 +35542,7 @@ var StreamableHTTPClientTransport = class {
35381
35542
  return this.send(message);
35382
35543
  }
35383
35544
  }
35384
- throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
35545
+ throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${this._unfollowedRedirect(response, this._url) ?? text}`);
35385
35546
  }
35386
35547
  this._hasCompletedAuthFlow = false;
35387
35548
  this._lastUpscopingHeader = void 0;
@@ -35443,10 +35604,10 @@ var StreamableHTTPClientTransport = class {
35443
35604
  headers,
35444
35605
  signal: this._abortController?.signal
35445
35606
  };
35446
- const response = await (this._fetch ?? fetch)(this._url, init);
35607
+ const response = await fetchWithinOrigin(this._fetch ?? fetch)(this._url, init);
35447
35608
  await response.body?.cancel();
35448
35609
  if (!response.ok && response.status !== 405) {
35449
- throw new StreamableHTTPError(response.status, `Failed to terminate session: ${response.statusText}`);
35610
+ throw new StreamableHTTPError(response.status, `Failed to terminate session: ${this._unfollowedRedirect(response, this._url) ?? response.statusText}`);
35450
35611
  }
35451
35612
  this._sessionId = void 0;
35452
35613
  } catch (error62) {
@@ -35486,81 +35647,394 @@ var import_websocket = __toESM(require_websocket(), 1);
35486
35647
  var import_websocket_server = __toESM(require_websocket_server(), 1);
35487
35648
  var wrapper_default = import_websocket.default;
35488
35649
 
35650
+ // src/process-event-forwarder.ts
35651
+ var eventSchema = external_exports.object({
35652
+ sessionId: external_exports.string().uuid(),
35653
+ taskId: external_exports.string().uuid().optional(),
35654
+ sequence: external_exports.number().int().positive(),
35655
+ kind: external_exports.enum(["started", "stdout", "stderr", "finished"]),
35656
+ offset: external_exports.number().int().nonnegative().optional(),
35657
+ data: external_exports.string().max(4096).optional(),
35658
+ exitCode: external_exports.number().int().nullable().optional(),
35659
+ signal: external_exports.string().nullable().optional(),
35660
+ timedOut: external_exports.boolean().optional()
35661
+ });
35662
+ var batchSchema = external_exports.object({
35663
+ sessionId: external_exports.string().uuid(),
35664
+ events: external_exports.array(eventSchema).max(32),
35665
+ nextSequence: external_exports.number().int().nonnegative(),
35666
+ missedEvents: external_exports.boolean(),
35667
+ finished: external_exports.boolean()
35668
+ });
35669
+ var processListSchema = external_exports.object({
35670
+ processes: external_exports.array(
35671
+ external_exports.object({
35672
+ sessionId: external_exports.string().uuid(),
35673
+ taskId: external_exports.string().uuid().optional()
35674
+ })
35675
+ )
35676
+ });
35677
+ function structuredContent(response) {
35678
+ if (!response || typeof response !== "object") {
35679
+ throw new Error("Local process tool returned an invalid response.");
35680
+ }
35681
+ const result = response;
35682
+ if (result.isError || result.structuredContent === void 0) {
35683
+ throw new Error("Local process event tool failed or returned no content.");
35684
+ }
35685
+ return result.structuredContent;
35686
+ }
35687
+ var ProcessEventForwarder = class {
35688
+ constructor(callLocalTool, send, log, acknowledged = /* @__PURE__ */ new Map(), ackTimeoutMs = 3e3, onFatal) {
35689
+ this.callLocalTool = callLocalTool;
35690
+ this.send = send;
35691
+ this.log = log;
35692
+ this.acknowledged = acknowledged;
35693
+ this.ackTimeoutMs = ackTimeoutMs;
35694
+ this.onFatal = onFatal;
35695
+ }
35696
+ callLocalTool;
35697
+ send;
35698
+ log;
35699
+ acknowledged;
35700
+ ackTimeoutMs;
35701
+ onFatal;
35702
+ #watchers = /* @__PURE__ */ new Map();
35703
+ #ackWaiters = /* @__PURE__ */ new Map();
35704
+ #lastSent = /* @__PURE__ */ new Map();
35705
+ #closed = false;
35706
+ acknowledge(sessionId, sequence) {
35707
+ if (!external_exports.string().uuid().safeParse(sessionId).success || !Number.isSafeInteger(sequence) || sequence < 1)
35708
+ return;
35709
+ const maximum = this.#lastSent.get(sessionId) ?? 0;
35710
+ if (sequence > maximum) return;
35711
+ const current = this.acknowledged.get(sessionId) ?? 0;
35712
+ if (sequence <= current) return;
35713
+ this.acknowledged.set(sessionId, sequence);
35714
+ for (const wake of this.#ackWaiters.get(sessionId) ?? []) wake();
35715
+ }
35716
+ async #waitForAck(sessionId, target, signal) {
35717
+ if ((this.acknowledged.get(sessionId) ?? 0) >= target) return true;
35718
+ if (signal.aborted) return false;
35719
+ return new Promise((resolve) => {
35720
+ const listeners = this.#ackWaiters.get(sessionId) ?? /* @__PURE__ */ new Set();
35721
+ this.#ackWaiters.set(sessionId, listeners);
35722
+ let timer;
35723
+ const finish = () => {
35724
+ clearTimeout(timer);
35725
+ signal.removeEventListener("abort", finish);
35726
+ listeners.delete(wake);
35727
+ if (listeners.size === 0) this.#ackWaiters.delete(sessionId);
35728
+ resolve((this.acknowledged.get(sessionId) ?? 0) >= target);
35729
+ };
35730
+ const wake = () => {
35731
+ if ((this.acknowledged.get(sessionId) ?? 0) >= target) finish();
35732
+ };
35733
+ listeners.add(wake);
35734
+ signal.addEventListener("abort", finish, { once: true });
35735
+ timer = setTimeout(finish, this.ackTimeoutMs);
35736
+ wake();
35737
+ });
35738
+ }
35739
+ async recover() {
35740
+ if (this.#closed) return;
35741
+ const result = await this.callLocalTool("list_processes", {});
35742
+ const parsed = processListSchema.parse(structuredContent(result));
35743
+ for (const process2 of parsed.processes) {
35744
+ if (process2.taskId) this.watch(process2.sessionId);
35745
+ }
35746
+ }
35747
+ watch(sessionId) {
35748
+ if (this.#closed || this.#watchers.has(sessionId)) return;
35749
+ if (!external_exports.string().uuid().safeParse(sessionId).success) return;
35750
+ const controller = new AbortController();
35751
+ this.#watchers.set(sessionId, controller);
35752
+ void this.#pump(sessionId, controller.signal).catch((error62) => {
35753
+ const failure2 = error62 instanceof Error ? error62 : new Error("Unknown process event failure.");
35754
+ this.log?.("Process event watch failed: " + failure2.message);
35755
+ if (!controller.signal.aborted) this.onFatal?.(failure2);
35756
+ }).finally(() => {
35757
+ if (this.#watchers.get(sessionId) === controller) {
35758
+ this.#watchers.delete(sessionId);
35759
+ }
35760
+ });
35761
+ }
35762
+ async #pump(sessionId, signal) {
35763
+ let afterSequence = this.acknowledged.get(sessionId) ?? 0;
35764
+ while (!signal.aborted) {
35765
+ const raw = await this.callLocalTool("watch_process_events", {
35766
+ sessionId,
35767
+ afterSequence,
35768
+ timeoutMs: 1e4,
35769
+ maxEvents: 32
35770
+ });
35771
+ if (signal.aborted) break;
35772
+ const batch = batchSchema.parse(structuredContent(raw));
35773
+ if (batch.sessionId !== sessionId) {
35774
+ throw new Error("Process event watch returned a different session ID.");
35775
+ }
35776
+ if (batch.nextSequence < afterSequence) {
35777
+ throw new Error("Process event cursor moved backwards.");
35778
+ }
35779
+ if (batch.events.length === 0 && batch.finished) break;
35780
+ if (batch.events.length === 0) continue;
35781
+ let confirmed = false;
35782
+ for (let attempt = 0; attempt < 5 && !signal.aborted; attempt++) {
35783
+ for (const [index, event] of batch.events.entries()) {
35784
+ if (signal.aborted) break;
35785
+ if (event.sessionId !== sessionId) {
35786
+ throw new Error("Process event belongs to another session.");
35787
+ }
35788
+ this.#lastSent.set(
35789
+ sessionId,
35790
+ Math.max(this.#lastSent.get(sessionId) ?? 0, event.sequence)
35791
+ );
35792
+ this.send({
35793
+ type: "process_event",
35794
+ event,
35795
+ missedEvents: index === 0 && batch.missedEvents
35796
+ });
35797
+ }
35798
+ confirmed = await this.#waitForAck(
35799
+ sessionId,
35800
+ batch.nextSequence,
35801
+ signal
35802
+ );
35803
+ if (confirmed) break;
35804
+ }
35805
+ if (!confirmed) {
35806
+ if (signal.aborted) break;
35807
+ throw new Error(
35808
+ "PROCESS_EVENT_ACK_TIMEOUT: Gateway did not acknowledge event batch."
35809
+ );
35810
+ }
35811
+ afterSequence = batch.nextSequence;
35812
+ if (batch.finished && batch.events.length < 32) break;
35813
+ }
35814
+ }
35815
+ close() {
35816
+ this.#closed = true;
35817
+ for (const controller of this.#watchers.values()) controller.abort();
35818
+ this.#watchers.clear();
35819
+ for (const wake of this.#ackWaiters.values()) {
35820
+ for (const notify of wake) notify();
35821
+ }
35822
+ this.#ackWaiters.clear();
35823
+ }
35824
+ };
35825
+
35489
35826
  // src/agent-core.ts
35490
- var destructiveTools = /* @__PURE__ */ new Set(["run_program", "write_text_file"]);
35491
- var requestSchema = external_exports.object({
35827
+ var supportedAgentTools = [
35828
+ "get_system_info",
35829
+ "list_directory",
35830
+ "read_text_file",
35831
+ "start_process",
35832
+ "read_process_output",
35833
+ "watch_process_events",
35834
+ "terminate_process",
35835
+ "list_processes",
35836
+ "write_text_file"
35837
+ ];
35838
+ var destructiveTools = /* @__PURE__ */ new Set([
35839
+ "start_process",
35840
+ "terminate_process",
35841
+ "write_text_file"
35842
+ ]);
35843
+ var requestEnvelope = external_exports.object({
35492
35844
  type: external_exports.literal("request"),
35493
- requestId: external_exports.string().uuid(),
35494
- tool: external_exports.enum([
35495
- "get_system_info",
35496
- "list_directory",
35497
- "read_text_file",
35498
- "run_program",
35499
- "write_text_file"
35500
- ]),
35501
- arguments: external_exports.record(external_exports.string(), external_exports.unknown())
35502
- });
35503
- async function authorizeToolCall(tool, args, allowDestructive, approve) {
35845
+ requestId: external_exports.string().uuid()
35846
+ });
35847
+ var requestSchema = requestEnvelope.extend({
35848
+ tool: external_exports.enum(supportedAgentTools),
35849
+ arguments: external_exports.record(external_exports.string(), external_exports.unknown()),
35850
+ deadlineAt: external_exports.number().int().positive().optional()
35851
+ });
35852
+ var cancelSchema = external_exports.object({
35853
+ type: external_exports.literal("cancel"),
35854
+ requestId: external_exports.string().uuid()
35855
+ });
35856
+ var ackSchema = external_exports.object({
35857
+ type: external_exports.literal("process_event_ack"),
35858
+ sessionId: external_exports.string().uuid(),
35859
+ sequence: external_exports.number().int().positive()
35860
+ });
35861
+ var DEFAULT_REQUEST_LIFETIME_MS = 6e4;
35862
+ function assertActive(signal) {
35863
+ if (signal?.aborted) {
35864
+ throw new Error(
35865
+ "REQUEST_EXPIRED: The operation was cancelled or its approval deadline expired."
35866
+ );
35867
+ }
35868
+ }
35869
+ async function authorizeToolCall(tool, args, allowDestructive, approve, signal) {
35870
+ assertActive(signal);
35504
35871
  if (!destructiveTools.has(tool)) return;
35505
35872
  if (!allowDestructive) {
35506
35873
  throw new Error(
35507
35874
  "This action requires local approval. Commands and writes are disabled unless explicitly enabled."
35508
35875
  );
35509
35876
  }
35510
- if (!approve || !await approve(tool, args)) {
35877
+ if (!approve || !await approve(tool, args, signal)) {
35878
+ assertActive(signal);
35511
35879
  throw new Error("The action was not approved on the connected machine.");
35512
35880
  }
35881
+ assertActive(signal);
35513
35882
  }
35514
- async function connectOnce(options) {
35883
+ async function connectOnce(options, acknowledged) {
35515
35884
  const { config: config2, signal } = options;
35885
+ if (!config2.MACHINE_TERMINAL_DEVICE_SECRET) {
35886
+ throw new Error(
35887
+ "DEVICE_PAIRING_REQUIRED: Connect using the CLI or set MACHINE_TERMINAL_DEVICE_SECRET."
35888
+ );
35889
+ }
35516
35890
  const client = new Client({
35517
35891
  name: "terminal-commands-cli",
35518
- version: "0.1.0"
35519
- });
35520
- await client.connect(
35521
- new StreamableHTTPClientTransport(
35522
- new URL(config2.MACHINE_TERMINAL_LOCAL_MCP_URL)
35523
- )
35524
- );
35525
- const token = options.getToken ? await options.getToken() : config2.MACHINE_TERMINAL_DEVICE_TOKEN;
35526
- const socket = new wrapper_default(config2.MACHINE_TERMINAL_GATEWAY_URL, {
35527
- headers: { Authorization: `Bearer ${token}` }
35892
+ version: "0.2.0"
35528
35893
  });
35529
- const stop = () => socket.close(1e3, "Client stopping.");
35530
- signal?.addEventListener("abort", stop, { once: true });
35531
- socket.on("open", () => {
35532
- socket.send(
35533
- JSON.stringify({
35534
- type: "hello",
35535
- deviceId: config2.MACHINE_TERMINAL_DEVICE_ID,
35536
- name: config2.MACHINE_TERMINAL_DEVICE_NAME,
35537
- platform: `${os.platform()}-${os.arch()}`
35538
- })
35894
+ let disposeSocket;
35895
+ try {
35896
+ await client.connect(
35897
+ new StreamableHTTPClientTransport(
35898
+ new URL(config2.MACHINE_TERMINAL_LOCAL_MCP_URL),
35899
+ {
35900
+ requestInit: {
35901
+ headers: config2.MACHINE_TERMINAL_LOCAL_AGENT_TOKEN ? {
35902
+ "X-Terminal-Commands-Agent-Token": config2.MACHINE_TERMINAL_LOCAL_AGENT_TOKEN
35903
+ } : {}
35904
+ }
35905
+ }
35906
+ )
35539
35907
  );
35540
- });
35541
- socket.on("message", (raw) => {
35542
- void handleMessage(options, client, socket, raw);
35543
- });
35544
- await new Promise((resolve, reject) => {
35545
- socket.once("close", resolve);
35546
- socket.once("error", reject);
35547
- }).finally(() => signal?.removeEventListener("abort", stop));
35548
- await client.close();
35908
+ const token = options.getToken ? await options.getToken() : config2.MACHINE_TERMINAL_DEVICE_TOKEN;
35909
+ const socket = new wrapper_default(config2.MACHINE_TERMINAL_GATEWAY_URL, {
35910
+ headers: { Authorization: "Bearer " + token }
35911
+ });
35912
+ disposeSocket = () => socket.terminate();
35913
+ const pending = /* @__PURE__ */ new Map();
35914
+ const forwarder = new ProcessEventForwarder(
35915
+ async (name, args) => client.callTool({ name, arguments: args }),
35916
+ (message) => {
35917
+ if (socket.readyState !== wrapper_default.OPEN) return;
35918
+ if (socket.bufferedAmount > 512e3) {
35919
+ socket.close(1013, "Process event backpressure");
35920
+ throw new Error(
35921
+ "Process event WebSocket backpressure limit reached."
35922
+ );
35923
+ }
35924
+ socket.send(JSON.stringify(message));
35925
+ },
35926
+ options.log,
35927
+ acknowledged,
35928
+ 3e3,
35929
+ () => {
35930
+ if (socket.readyState === wrapper_default.OPEN) {
35931
+ socket.close(1013, "Process event delivery stalled");
35932
+ }
35933
+ }
35934
+ );
35935
+ const stop = () => socket.close(1e3, "Client stopping.");
35936
+ signal?.addEventListener("abort", stop, { once: true });
35937
+ socket.on("open", () => {
35938
+ socket.send(
35939
+ JSON.stringify({
35940
+ type: "hello",
35941
+ deviceId: config2.MACHINE_TERMINAL_DEVICE_ID,
35942
+ deviceSecret: config2.MACHINE_TERMINAL_DEVICE_SECRET,
35943
+ name: config2.MACHINE_TERMINAL_DEVICE_NAME,
35944
+ platform: os.platform() + "-" + os.arch(),
35945
+ protocolVersion: 2,
35946
+ capabilities: supportedAgentTools
35947
+ })
35948
+ );
35949
+ });
35950
+ socket.on("message", (raw) => {
35951
+ try {
35952
+ const message = JSON.parse(raw.toString());
35953
+ const cancelled = cancelSchema.safeParse(message);
35954
+ if (cancelled.success) {
35955
+ pending.get(cancelled.data.requestId)?.abort();
35956
+ return;
35957
+ }
35958
+ const ack = ackSchema.safeParse(message);
35959
+ if (ack.success) {
35960
+ forwarder.acknowledge(ack.data.sessionId, ack.data.sequence);
35961
+ return;
35962
+ }
35963
+ if (message && typeof message === "object" && "type" in message && message.type === "ready") {
35964
+ void forwarder.recover().catch(
35965
+ (error62) => options.log?.(
35966
+ "Could not recover local process streams: " + (error62 instanceof Error ? error62.message : "unknown error")
35967
+ )
35968
+ );
35969
+ return;
35970
+ }
35971
+ } catch {
35972
+ options.log?.("Ignoring a malformed gateway message.");
35973
+ return;
35974
+ }
35975
+ void handleMessage(options, client, socket, raw, pending, forwarder);
35976
+ });
35977
+ socket.on("close", () => {
35978
+ forwarder.close();
35979
+ for (const controller of pending.values()) controller.abort();
35980
+ });
35981
+ await new Promise((resolve, reject) => {
35982
+ socket.once("close", resolve);
35983
+ socket.once("error", reject);
35984
+ }).finally(() => signal?.removeEventListener("abort", stop));
35985
+ } finally {
35986
+ disposeSocket?.();
35987
+ await client.close().catch(() => void 0);
35988
+ }
35549
35989
  }
35550
- async function handleMessage(options, client, socket, raw) {
35990
+ async function handleMessage(options, client, socket, raw, pending, forwarder) {
35551
35991
  let requestId;
35992
+ let controller;
35993
+ let deadlineTimer;
35552
35994
  try {
35553
- const parsed = requestSchema.parse(JSON.parse(raw.toString()));
35554
- requestId = parsed.requestId;
35995
+ const payload = JSON.parse(raw.toString());
35996
+ const envelope = requestEnvelope.safeParse(payload);
35997
+ if (!envelope.success) {
35998
+ options.log?.("Ignoring a request with an invalid envelope.");
35999
+ return;
36000
+ }
36001
+ requestId = envelope.data.requestId;
36002
+ const parsed = requestSchema.safeParse(payload);
36003
+ if (!parsed.success) {
36004
+ const tool = payload && typeof payload === "object" && "tool" in payload ? payload.tool : void 0;
36005
+ if (typeof tool === "string" && !supportedAgentTools.includes(tool)) {
36006
+ throw new Error(
36007
+ "UNSUPPORTED_TOOL: " + tool + ". Update the local companion."
36008
+ );
36009
+ }
36010
+ throw new Error("INVALID_REQUEST: Invalid tool arguments or deadline.");
36011
+ }
36012
+ if (pending.has(requestId)) {
36013
+ throw new Error(
36014
+ "DUPLICATE_REQUEST: This request is already in progress."
36015
+ );
36016
+ }
36017
+ const deadlineAt = parsed.data.deadlineAt ?? Date.now() + DEFAULT_REQUEST_LIFETIME_MS;
36018
+ if (deadlineAt <= Date.now()) {
36019
+ throw new Error("REQUEST_EXPIRED: The request deadline has passed.");
36020
+ }
36021
+ controller = new AbortController();
36022
+ pending.set(requestId, controller);
36023
+ deadlineTimer = setTimeout(
36024
+ () => controller?.abort(),
36025
+ Math.max(0, deadlineAt - Date.now())
36026
+ );
35555
36027
  await authorizeToolCall(
35556
- parsed.tool,
35557
- parsed.arguments,
36028
+ parsed.data.tool,
36029
+ parsed.data.arguments,
35558
36030
  options.config.MACHINE_TERMINAL_AGENT_ALLOW_DESTRUCTIVE === "1",
35559
- options.approve
36031
+ options.approve,
36032
+ controller.signal
35560
36033
  );
36034
+ assertActive(controller.signal);
35561
36035
  const result = await client.callTool({
35562
- name: parsed.tool,
35563
- arguments: parsed.arguments
36036
+ name: parsed.data.tool,
36037
+ arguments: parsed.data.arguments
35564
36038
  });
35565
36039
  if (result.isError) {
35566
36040
  const content = Array.isArray(result.content) ? result.content : [];
@@ -35577,6 +36051,12 @@ async function handleMessage(options, client, socket, raw) {
35577
36051
  result: result.structuredContent ?? { content: result.content }
35578
36052
  })
35579
36053
  );
36054
+ if (parsed.data.tool === "start_process") {
36055
+ const snapshot = result.structuredContent;
36056
+ if (snapshot && typeof snapshot === "object" && "sessionId" in snapshot && typeof snapshot.sessionId === "string" && "taskId" in snapshot && typeof snapshot.taskId === "string") {
36057
+ forwarder.watch(snapshot.sessionId);
36058
+ }
36059
+ }
35580
36060
  }
35581
36061
  } catch (error62) {
35582
36062
  if (!requestId || socket.readyState !== wrapper_default.OPEN) return;
@@ -35587,18 +36067,24 @@ async function handleMessage(options, client, socket, raw) {
35587
36067
  error: error62 instanceof Error ? error62.message : "Local request failed."
35588
36068
  })
35589
36069
  );
36070
+ } finally {
36071
+ if (deadlineTimer) clearTimeout(deadlineTimer);
36072
+ if (requestId && controller && pending.get(requestId) === controller) {
36073
+ pending.delete(requestId);
36074
+ }
35590
36075
  }
35591
36076
  }
35592
36077
  async function runAgent(options) {
35593
36078
  let reconnectDelayMs = 1e3;
36079
+ const acknowledged = /* @__PURE__ */ new Map();
35594
36080
  while (!options.signal?.aborted) {
35595
36081
  try {
35596
- await connectOnce(options);
36082
+ await connectOnce(options, acknowledged);
35597
36083
  reconnectDelayMs = 1e3;
35598
36084
  } catch (error62) {
35599
36085
  if (options.signal?.aborted) break;
35600
36086
  options.log?.(
35601
- `Connection failed: ${error62 instanceof Error ? error62.message : "unknown error"}`
36087
+ "Connection failed: " + (error62 instanceof Error ? error62.message : "unknown error")
35602
36088
  );
35603
36089
  }
35604
36090
  if (options.signal?.aborted) break;
@@ -35617,12 +36103,120 @@ async function runAgent(options) {
35617
36103
  }
35618
36104
  }
35619
36105
 
36106
+ // src/local-agent-auth.ts
36107
+ import { createHmac, timingSafeEqual } from "node:crypto";
36108
+ function localHealthProof(secret, challenge) {
36109
+ if (!secret || !/^[a-f0-9]{64}$/i.test(secret)) return null;
36110
+ if (!challenge || !/^[a-f0-9]{64}$/i.test(challenge)) return null;
36111
+ return createHmac("sha256", Buffer.from(secret, "hex")).update(challenge, "ascii").digest("hex");
36112
+ }
36113
+
36114
+ // src/device-management.ts
36115
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
36116
+ function validDeviceId(id) {
36117
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id);
36118
+ }
36119
+ function managementUrl(connectionUrl) {
36120
+ const url2 = new URL(connectionUrl);
36121
+ if (url2.username || url2.password || url2.pathname !== "/device/connect") {
36122
+ throw new Error(
36123
+ "Expected a credential-free Terminal Commands gateway URL."
36124
+ );
36125
+ }
36126
+ if (url2.protocol === "wss:") {
36127
+ url2.protocol = "https:";
36128
+ } else if (url2.protocol === "ws:" && LOOPBACK_HOSTS.has(url2.hostname)) {
36129
+ url2.protocol = "http:";
36130
+ } else {
36131
+ throw new Error("Device revocation requires HTTPS, or loopback test HTTP.");
36132
+ }
36133
+ url2.search = "";
36134
+ url2.hash = "";
36135
+ return url2;
36136
+ }
36137
+ function deviceRevocationUrl(connectionUrl, deviceId) {
36138
+ if (!validDeviceId(deviceId)) throw new Error("Invalid device identifier.");
36139
+ const url2 = managementUrl(connectionUrl);
36140
+ url2.pathname = "/devices/" + encodeURIComponent(deviceId) + "/revoke";
36141
+ return url2.toString();
36142
+ }
36143
+ function pairedDevicesUrl(connectionUrl, cursor) {
36144
+ if (cursor !== void 0 && !validDeviceId(cursor)) {
36145
+ throw new Error("Invalid pairing-list cursor.");
36146
+ }
36147
+ const url2 = managementUrl(connectionUrl);
36148
+ url2.pathname = "/devices";
36149
+ if (cursor !== void 0) url2.searchParams.set("after", cursor);
36150
+ return url2.toString();
36151
+ }
36152
+ async function fetchPairedDevices(connectionUrl, managementToken2, fetchImpl = fetch) {
36153
+ if (!managementToken2 || /\s/.test(managementToken2)) {
36154
+ throw new Error("Invalid device-management access token.");
36155
+ }
36156
+ const devices = [];
36157
+ const seen = /* @__PURE__ */ new Set();
36158
+ let cursor;
36159
+ for (let page = 0; page < 10; page++) {
36160
+ const response = await fetchImpl(pairedDevicesUrl(connectionUrl, cursor), {
36161
+ headers: { Authorization: "Bearer " + managementToken2 },
36162
+ redirect: "error",
36163
+ signal: AbortSignal.timeout(15e3)
36164
+ });
36165
+ if (!response.ok) {
36166
+ throw new Error(
36167
+ "Could not list paired devices (" + response.status + ")."
36168
+ );
36169
+ }
36170
+ const payload = await response.json();
36171
+ if (!Array.isArray(payload.devices) || payload.devices.length > 100) {
36172
+ throw new Error("Invalid paired-device response.");
36173
+ }
36174
+ for (const entry of payload.devices) {
36175
+ const item = entry;
36176
+ if (typeof item.deviceId !== "string" || !validDeviceId(item.deviceId) || typeof item.pairedAt !== "string" || !(typeof item.revokedAt === "string" || item.revokedAt === null) || typeof item.connected !== "boolean" || seen.has(item.deviceId)) {
36177
+ throw new Error("Invalid or repeated device in pairing inventory.");
36178
+ }
36179
+ seen.add(item.deviceId);
36180
+ devices.push(item);
36181
+ }
36182
+ if (payload.nextCursor === null) return { devices, truncated: false };
36183
+ if (typeof payload.nextCursor !== "string" || !validDeviceId(payload.nextCursor) || cursor === payload.nextCursor) {
36184
+ throw new Error("Invalid device-list continuation cursor.");
36185
+ }
36186
+ cursor = payload.nextCursor;
36187
+ }
36188
+ return { devices, truncated: true };
36189
+ }
36190
+ async function requestDeviceRevocation(connectionUrl, deviceId, managementToken2, fetchImpl = fetch) {
36191
+ if (!managementToken2 || /\s/.test(managementToken2)) {
36192
+ throw new Error("Invalid device-management access token.");
36193
+ }
36194
+ const endpoint2 = deviceRevocationUrl(connectionUrl, deviceId);
36195
+ const response = await fetchImpl(endpoint2, {
36196
+ method: "POST",
36197
+ headers: { Authorization: "Bearer " + managementToken2 },
36198
+ redirect: "error",
36199
+ signal: AbortSignal.timeout(15e3)
36200
+ });
36201
+ if (!response.ok) {
36202
+ throw new Error(
36203
+ "Device revocation was not confirmed (" + response.status + ")."
36204
+ );
36205
+ }
36206
+ const result = await response.json();
36207
+ if (result.deviceId !== deviceId || result.revoked !== true) {
36208
+ throw new Error("Gateway did not confirm this device's revocation.");
36209
+ }
36210
+ }
36211
+
35620
36212
  // src/agent-config.ts
35621
36213
  import os2 from "node:os";
35622
36214
  var schema = external_exports.object({
35623
36215
  MACHINE_TERMINAL_GATEWAY_URL: external_exports.string().url(),
35624
36216
  MACHINE_TERMINAL_DEVICE_TOKEN: external_exports.string().min(1),
36217
+ MACHINE_TERMINAL_LOCAL_AGENT_TOKEN: external_exports.string().regex(/^[a-f0-9]{64}$/i).optional(),
35625
36218
  MACHINE_TERMINAL_DEVICE_ID: external_exports.string().min(1).max(128),
36219
+ MACHINE_TERMINAL_DEVICE_SECRET: external_exports.string().regex(/^[a-f0-9]{64}$/i).optional(),
35626
36220
  MACHINE_TERMINAL_DEVICE_NAME: external_exports.string().min(1).max(128).default(os2.hostname()),
35627
36221
  MACHINE_TERMINAL_LOCAL_MCP_URL: external_exports.string().url().default("http://127.0.0.1:3333/mcp"),
35628
36222
  MACHINE_TERMINAL_AGENT_ALLOW_DESTRUCTIVE: external_exports.enum(["0", "1"]).default("0"),
@@ -35686,52 +36280,78 @@ function keyValueBlock(rows) {
35686
36280
  }
35687
36281
 
35688
36282
  // src/approval.ts
36283
+ var DEFAULT_APPROVAL_TIMEOUT_MS = 3e4;
36284
+ function safeLabel(value) {
36285
+ return JSON.stringify(String(value));
36286
+ }
35689
36287
  function approvalSummary(tool, args) {
35690
36288
  if (tool === "write_text_file") {
35691
36289
  const bytes = Buffer.byteLength(String(args.contents ?? ""));
35692
- return `Write ${bytes} bytes to ${String(args.path ?? "an unknown path")}${args.overwrite ? " (overwrite)" : ""}`;
36290
+ return "Write " + bytes + " bytes to " + safeLabel(args.path ?? "an unknown path") + (args.overwrite ? " (overwrite)" : "");
35693
36291
  }
35694
- if (tool === "run_program") {
36292
+ if (tool === "start_process") {
35695
36293
  const command = [
35696
- String(args.program ?? ""),
35697
- ...Array.isArray(args.args) ? args.args.map(String) : []
36294
+ safeLabel(args.program ?? ""),
36295
+ ...Array.isArray(args.args) ? args.args.map(safeLabel) : []
35698
36296
  ];
35699
- return `Run: ${command.join(" ")}`;
36297
+ return "Start: " + command.join(" ") + " (cwd: " + safeLabel(args.cwd ?? "configured root") + ")";
36298
+ }
36299
+ if (tool === "terminate_process") {
36300
+ return "Terminate process session " + safeLabel(args.sessionId ?? "");
35700
36301
  }
35701
- return `Run destructive tool: ${tool}`;
36302
+ return "Run destructive tool: " + safeLabel(tool);
35702
36303
  }
35703
36304
  function createAutoApprover() {
35704
- return (tool, args) => {
36305
+ return (tool, args, signal) => {
36306
+ if (signal?.aborted) return Promise.resolve(false);
35705
36307
  console.log(
35706
- `
35707
- ${warn("Approval requested:")} ${approvalSummary(tool, args)} ${dim("(auto-approved)")}`
36308
+ "\n" + warn("Approval requested:") + " " + approvalSummary(tool, args) + " " + dim("(auto-approved)")
35708
36309
  );
35709
36310
  return Promise.resolve(true);
35710
36311
  };
35711
36312
  }
35712
- function createTerminalApprover() {
35713
- let queue = Promise.resolve(false);
35714
- return (tool, args) => {
35715
- queue = queue.catch(() => false).then(async () => {
35716
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
36313
+ function createTerminalApprover(timeoutMs = DEFAULT_APPROVAL_TIMEOUT_MS) {
36314
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
36315
+ throw new Error("Approval timeout must be a positive integer.");
36316
+ }
36317
+ let queue = Promise.resolve();
36318
+ return (tool, args, signal) => {
36319
+ const next = queue.then(async () => {
36320
+ if (!process.stdin.isTTY || !process.stdout.isTTY || signal?.aborted) {
36321
+ return false;
36322
+ }
36323
+ const controller = new AbortController();
36324
+ const abort = () => controller.abort();
36325
+ signal?.addEventListener("abort", abort, { once: true });
36326
+ const timer = setTimeout(abort, timeoutMs);
35717
36327
  const prompt = createInterface({
35718
36328
  input: process.stdin,
35719
36329
  output: process.stdout
35720
36330
  });
35721
36331
  try {
36332
+ if (signal?.aborted) return false;
35722
36333
  console.log(
35723
- `
35724
- ${warn("Approval requested:")} ${approvalSummary(tool, args)}`
36334
+ "\n" + warn("Approval requested:") + " " + approvalSummary(tool, args)
35725
36335
  );
35726
36336
  const answer = await prompt.question(
35727
- `${bold("Approve this action?")} [y/N] `
36337
+ bold("Approve this action?") + " [y/N] ",
36338
+ { signal: controller.signal }
35728
36339
  );
35729
- return (/* @__PURE__ */ new Set(["y", "yes"])).has(answer.trim().toLowerCase());
36340
+ return !controller.signal.aborted && (/* @__PURE__ */ new Set(["y", "yes"])).has(answer.trim().toLowerCase());
36341
+ } catch (error62) {
36342
+ if (controller.signal.aborted) return false;
36343
+ throw error62;
35730
36344
  } finally {
36345
+ clearTimeout(timer);
36346
+ signal?.removeEventListener("abort", abort);
35731
36347
  prompt.close();
35732
36348
  }
35733
36349
  });
35734
- return queue;
36350
+ queue = next.then(
36351
+ () => void 0,
36352
+ () => void 0
36353
+ );
36354
+ return next;
35735
36355
  };
35736
36356
  }
35737
36357
 
@@ -35744,15 +36364,26 @@ var COMMANDS = [
35744
36364
  "connect",
35745
36365
  "status",
35746
36366
  "logout",
36367
+ "revoke-device",
36368
+ "paired-devices",
35747
36369
  "help"
35748
36370
  ];
35749
36371
  var COMMAND_SUMMARIES = {
35750
36372
  login: "Sign in using a URL and one-time code",
35751
36373
  connect: "Connect this machine until stopped",
35752
36374
  status: "Show sign-in state and the settings connect would use",
35753
- logout: "Remove locally saved credentials"
36375
+ logout: "Remove locally saved credentials",
36376
+ "revoke-device": "Revoke one paired device using fresh owner authentication",
36377
+ "paired-devices": "List offline, connected and revoked device identities"
35754
36378
  };
35755
- var AUTH_COMMANDS = ["login", "connect", "status", "logout"];
36379
+ var AUTH_COMMANDS = [
36380
+ "login",
36381
+ "connect",
36382
+ "status",
36383
+ "logout",
36384
+ "revoke-device",
36385
+ "paired-devices"
36386
+ ];
35756
36387
  var OPTIONS = [
35757
36388
  {
35758
36389
  name: "root",
@@ -35777,8 +36408,8 @@ var OPTIONS = [
35777
36408
  kind: "string",
35778
36409
  env: "MACHINE_TERMINAL_DEVICE_ID",
35779
36410
  placeholder: "<id>",
35780
- commands: ["connect", "status"],
35781
- description: "Device identifier to report. Defaults to the saved local device ID."
36411
+ commands: ["connect", "status", "revoke-device"],
36412
+ description: "Device identifier. Required explicitly for revoke-device; defaults to saved ID for connect/status."
35782
36413
  },
35783
36414
  {
35784
36415
  name: "port",
@@ -35801,7 +36432,7 @@ var OPTIONS = [
35801
36432
  kind: "string",
35802
36433
  env: "MACHINE_TERMINAL_GATEWAY_URL",
35803
36434
  placeholder: "<url>",
35804
- commands: ["connect", "status"],
36435
+ commands: ["connect", "status", "revoke-device", "paired-devices"],
35805
36436
  description: "Gateway WebSocket URL. Defaults to wss://mcp.terminalcommands.fr/device/connect."
35806
36437
  },
35807
36438
  {
@@ -36109,6 +36740,8 @@ function renderHelp(command = "help") {
36109
36740
  " terminal-commands connect --root ~/Documents/GitHub --root ~/work",
36110
36741
  " terminal-commands connect --root . --dry-run",
36111
36742
  " terminal-commands connect --root ~/code --name laptop --allow-destructive",
36743
+ " terminal-commands paired-devices",
36744
+ " terminal-commands revoke-device --device-id <paired-device-id>",
36112
36745
  " npm run cli -- connect --root ~/Documents/GitHub --allowed-program git,node"
36113
36746
  );
36114
36747
  return lines.join("\n");
@@ -36116,7 +36749,7 @@ function renderHelp(command = "help") {
36116
36749
 
36117
36750
  // src/credential-store.ts
36118
36751
  import { promises as fs2 } from "node:fs";
36119
- import { randomUUID } from "node:crypto";
36752
+ import { randomBytes, randomUUID } from "node:crypto";
36120
36753
  import os4 from "node:os";
36121
36754
  import path2 from "node:path";
36122
36755
  var storedSchema = external_exports.object({
@@ -36189,6 +36822,23 @@ async function loadOrCreateDeviceId(environment = process.env) {
36189
36822
  });
36190
36823
  return (await fs2.readFile(target, "utf8")).trim();
36191
36824
  }
36825
+ async function loadOrCreateDeviceSecret(environment = process.env) {
36826
+ const directory = configDirectory(environment);
36827
+ const target = path2.join(directory, "device-secret");
36828
+ await fs2.mkdir(directory, { recursive: true, mode: 448 });
36829
+ const generated = randomBytes(32).toString("hex");
36830
+ try {
36831
+ await fs2.writeFile(target, generated + "\n", { mode: 384, flag: "wx" });
36832
+ } catch (error62) {
36833
+ if (error62.code !== "EEXIST") throw error62;
36834
+ }
36835
+ const value = (await fs2.readFile(target, "utf8")).trim();
36836
+ if (!/^[a-f0-9]{64}$/i.test(value)) {
36837
+ throw new Error("DEVICE_SECRET_INVALID: Re-pair this machine securely.");
36838
+ }
36839
+ if (process.platform !== "win32") await fs2.chmod(target, 384);
36840
+ return value;
36841
+ }
36192
36842
 
36193
36843
  // src/device-auth.ts
36194
36844
  var deviceCodeSchema = external_exports.object({
@@ -36439,14 +37089,22 @@ async function validCredentials(auth2) {
36439
37089
  await saveCredentials(refreshed);
36440
37090
  return refreshed;
36441
37091
  }
36442
- async function waitForHealth(url2, child) {
37092
+ async function waitForHealth(url2, child, agentToken) {
37093
+ const challenge = randomBytes2(32).toString("hex");
37094
+ const expectedProof = localHealthProof(agentToken, challenge);
37095
+ if (!expectedProof)
37096
+ throw new Error("Invalid local agent startup capability.");
36443
37097
  const deadline = Date.now() + 1e4;
36444
37098
  while (Date.now() < deadline) {
36445
- if (child.exitCode !== null)
37099
+ if (child.exitCode !== null || child.signalCode !== null)
36446
37100
  throw new Error("The local terminal service could not start.");
36447
37101
  try {
36448
- const response = await fetch(url2);
36449
- if (response.ok) return;
37102
+ const response = await fetch(url2, {
37103
+ headers: { "X-Terminal-Commands-Health-Challenge": challenge },
37104
+ signal: AbortSignal.timeout(1e3)
37105
+ });
37106
+ const health = await response.json();
37107
+ if (response.ok && health?.agentProof === expectedProof) return;
36450
37108
  } catch {
36451
37109
  }
36452
37110
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -36465,6 +37123,7 @@ async function connect(environment, parsed) {
36465
37123
  );
36466
37124
  return;
36467
37125
  }
37126
+ const deviceSecret = environment.MACHINE_TERMINAL_DEVICE_SECRET ?? await loadOrCreateDeviceSecret();
36468
37127
  const auth2 = resolveAuth(environment);
36469
37128
  const firstCredentials = await validCredentials(auth2);
36470
37129
  console.log(
@@ -36477,11 +37136,14 @@ async function connect(environment, parsed) {
36477
37136
  )
36478
37137
  );
36479
37138
  }
37139
+ const localAgentToken = randomBytes2(32).toString("hex");
36480
37140
  const childEnvironment = {
36481
37141
  ...environment,
37142
+ MACHINE_TERMINAL_LOCAL_AGENT_TOKEN: localAgentToken,
36482
37143
  MACHINE_TERMINAL_HOST: settings.host,
36483
37144
  MACHINE_TERMINAL_PORT: String(settings.port),
36484
- MACHINE_TERMINAL_ROOTS: settings.roots.join(path3.delimiter)
37145
+ MACHINE_TERMINAL_ROOTS: settings.roots.join(path3.delimiter),
37146
+ MACHINE_TERMINAL_TASK_RECEIPTS_FILE: environment.MACHINE_TERMINAL_TASK_RECEIPTS_FILE ?? path3.join(configDirectory(environment), "task-receipts.json")
36485
37147
  };
36486
37148
  const serverEntry = fileURLToPath(new URL("./index.js", import.meta.url));
36487
37149
  const local = spawn(process.execPath, [serverEntry], {
@@ -36493,12 +37155,14 @@ async function connect(environment, parsed) {
36493
37155
  process.once("SIGINT", stop);
36494
37156
  process.once("SIGTERM", stop);
36495
37157
  try {
36496
- await waitForHealth(`${settings.localUrl}/health`, local);
37158
+ await waitForHealth(`${settings.localUrl}/health`, local, localAgentToken);
36497
37159
  const config2 = loadAgentConfig({
36498
37160
  ...childEnvironment,
36499
37161
  MACHINE_TERMINAL_GATEWAY_URL: settings.gatewayUrl,
37162
+ MACHINE_TERMINAL_LOCAL_AGENT_TOKEN: localAgentToken,
36500
37163
  MACHINE_TERMINAL_DEVICE_TOKEN: firstCredentials.accessToken,
36501
37164
  MACHINE_TERMINAL_DEVICE_ID: deviceId,
37165
+ MACHINE_TERMINAL_DEVICE_SECRET: deviceSecret,
36502
37166
  MACHINE_TERMINAL_DEVICE_NAME: settings.deviceName,
36503
37167
  MACHINE_TERMINAL_LOCAL_MCP_URL: `${settings.localUrl}/mcp`,
36504
37168
  MACHINE_TERMINAL_AGENT_ALLOW_DESTRUCTIVE: settings.allowDestructive ? "1" : "0"
@@ -36573,6 +37237,81 @@ async function status(environment, parsed) {
36573
37237
  ].join("\n")
36574
37238
  );
36575
37239
  }
37240
+ async function managementToken(environment, purpose) {
37241
+ const management = {
37242
+ ...resolveAuth(environment),
37243
+ scope: "openid profile email terminal:devices:revoke"
37244
+ };
37245
+ const code = await requestDeviceCode(management);
37246
+ console.log("\n" + bold("Authorize " + purpose + " in your browser:"));
37247
+ console.log(info(code.verification_uri_complete ?? code.verification_uri));
37248
+ console.log("\n" + bold("One-time code: ") + info(code.user_code));
37249
+ const credentials = await pollForDeviceToken(
37250
+ management,
37251
+ code.device_code,
37252
+ code.expires_in,
37253
+ code.interval
37254
+ );
37255
+ return credentials.accessToken;
37256
+ }
37257
+ async function pairedDevices(environment) {
37258
+ const gateway = environment.MACHINE_TERMINAL_GATEWAY_URL ?? DEFAULTS.gatewayUrl;
37259
+ pairedDevicesUrl(gateway);
37260
+ const token = await managementToken(environment, "paired-device inventory");
37261
+ const result = await fetchPairedDevices(gateway, token);
37262
+ if (result.devices.length === 0) {
37263
+ console.log(info("No paired devices found for this account."));
37264
+ return;
37265
+ }
37266
+ for (const device of result.devices) {
37267
+ const status2 = device.revokedAt ? "revoked" : device.connected ? "connected" : "offline";
37268
+ console.log(
37269
+ device.deviceId + " \u2014 " + status2 + " (paired " + device.pairedAt + ")"
37270
+ );
37271
+ }
37272
+ if (result.truncated) {
37273
+ console.log(warn("More than 1,000 device records: list was truncated."));
37274
+ }
37275
+ }
37276
+ async function revokeDevice(environment, parsed) {
37277
+ const supplied = parsed.options.get("device-id");
37278
+ const deviceId = Array.isArray(supplied) ? supplied[0] : void 0;
37279
+ if (!deviceId) {
37280
+ throw new Error("Specify --device-id of the pairing to revoke.");
37281
+ }
37282
+ const gateway = environment.MACHINE_TERMINAL_GATEWAY_URL ?? DEFAULTS.gatewayUrl;
37283
+ deviceRevocationUrl(gateway, deviceId);
37284
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
37285
+ throw new Error("Revocation needs an interactive confirmation terminal.");
37286
+ }
37287
+ console.log(
37288
+ warn(
37289
+ "Device revocation is permanent for this device ID. It disconnects the companion, but cannot stop already launched OS processes or invalidate Auth0 tokens."
37290
+ )
37291
+ );
37292
+ const prompt = createInterface2({
37293
+ input: process.stdin,
37294
+ output: process.stdout
37295
+ });
37296
+ let answer;
37297
+ try {
37298
+ answer = await prompt.question(
37299
+ "Type REVOKE " + deviceId + " to continue: "
37300
+ );
37301
+ } finally {
37302
+ prompt.close();
37303
+ }
37304
+ if (answer.trim() !== "REVOKE " + deviceId) {
37305
+ throw new Error("Device revocation cancelled.");
37306
+ }
37307
+ const token = await managementToken(environment, "device revocation");
37308
+ await requestDeviceRevocation(gateway, deviceId, token);
37309
+ console.log(
37310
+ ok(
37311
+ "Device " + deviceId + " was revoked and its existing pairing disabled."
37312
+ )
37313
+ );
37314
+ }
36576
37315
  async function main() {
36577
37316
  const parsed = parseCommandLine(process.argv.slice(2));
36578
37317
  if (parsed.help) {
@@ -36586,6 +37325,12 @@ async function main() {
36586
37325
  if (parsed.command === "login") return login(environment);
36587
37326
  if (parsed.command === "connect") return connect(environment, parsed);
36588
37327
  if (parsed.command === "status") return status(environment, parsed);
37328
+ if (parsed.command === "revoke-device") {
37329
+ return revokeDevice(environment, parsed);
37330
+ }
37331
+ if (parsed.command === "paired-devices") {
37332
+ return pairedDevices(environment);
37333
+ }
36589
37334
  if (parsed.command === "logout") {
36590
37335
  await clearCredentials();
36591
37336
  console.log(ok("Signed out and removed locally saved credentials."));