terminal-commands 0.1.3 → 0.2.1

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,88 +35647,443 @@ 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
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
+ ];
35490
35838
  var destructiveTools = /* @__PURE__ */ new Set([
35491
35839
  "start_process",
35492
35840
  "terminate_process",
35493
35841
  "write_text_file"
35494
35842
  ]);
35495
- var requestSchema = external_exports.object({
35843
+ var requestEnvelope = external_exports.object({
35496
35844
  type: external_exports.literal("request"),
35497
- requestId: external_exports.string().uuid(),
35498
- tool: external_exports.enum([
35499
- "get_system_info",
35500
- "list_directory",
35501
- "read_text_file",
35502
- "start_process",
35503
- "read_process_output",
35504
- "terminate_process",
35505
- "list_processes",
35506
- "write_text_file"
35507
- ]),
35508
- arguments: external_exports.record(external_exports.string(), external_exports.unknown())
35509
- });
35510
- 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, progress) {
35870
+ const notify = (phase) => {
35871
+ try {
35872
+ progress?.(phase);
35873
+ } catch {
35874
+ }
35875
+ };
35876
+ assertActive(signal);
35511
35877
  if (!destructiveTools.has(tool)) return;
35512
35878
  if (!allowDestructive) {
35879
+ notify("denied");
35513
35880
  throw new Error(
35514
35881
  "This action requires local approval. Commands and writes are disabled unless explicitly enabled."
35515
35882
  );
35516
35883
  }
35517
- if (!approve || !await approve(tool, args)) {
35884
+ if (!approve) {
35885
+ notify("denied");
35886
+ throw new Error("The action was not approved on the connected machine.");
35887
+ }
35888
+ notify("awaiting_approval");
35889
+ if (!await approve(tool, args, signal)) {
35890
+ assertActive(signal);
35891
+ notify("denied");
35518
35892
  throw new Error("The action was not approved on the connected machine.");
35519
35893
  }
35894
+ assertActive(signal);
35895
+ notify("approved");
35520
35896
  }
35521
- async function connectOnce(options) {
35897
+ async function connectOnce(options, acknowledged) {
35522
35898
  const { config: config2, signal } = options;
35899
+ if (!config2.MACHINE_TERMINAL_DEVICE_SECRET) {
35900
+ throw new Error(
35901
+ "DEVICE_PAIRING_REQUIRED: Connect using the CLI or set MACHINE_TERMINAL_DEVICE_SECRET."
35902
+ );
35903
+ }
35523
35904
  const client = new Client({
35524
35905
  name: "terminal-commands-cli",
35525
- version: "0.1.0"
35526
- });
35527
- await client.connect(
35528
- new StreamableHTTPClientTransport(
35529
- new URL(config2.MACHINE_TERMINAL_LOCAL_MCP_URL)
35530
- )
35531
- );
35532
- const token = options.getToken ? await options.getToken() : config2.MACHINE_TERMINAL_DEVICE_TOKEN;
35533
- const socket = new wrapper_default(config2.MACHINE_TERMINAL_GATEWAY_URL, {
35534
- headers: { Authorization: `Bearer ${token}` }
35906
+ version: "0.2.1"
35535
35907
  });
35536
- const stop = () => socket.close(1e3, "Client stopping.");
35537
- signal?.addEventListener("abort", stop, { once: true });
35538
- socket.on("open", () => {
35539
- socket.send(
35540
- JSON.stringify({
35541
- type: "hello",
35542
- deviceId: config2.MACHINE_TERMINAL_DEVICE_ID,
35543
- name: config2.MACHINE_TERMINAL_DEVICE_NAME,
35544
- platform: `${os.platform()}-${os.arch()}`
35545
- })
35908
+ let disposeSocket;
35909
+ try {
35910
+ await client.connect(
35911
+ new StreamableHTTPClientTransport(
35912
+ new URL(config2.MACHINE_TERMINAL_LOCAL_MCP_URL),
35913
+ {
35914
+ requestInit: {
35915
+ headers: config2.MACHINE_TERMINAL_LOCAL_AGENT_TOKEN ? {
35916
+ "X-Terminal-Commands-Agent-Token": config2.MACHINE_TERMINAL_LOCAL_AGENT_TOKEN
35917
+ } : {}
35918
+ }
35919
+ }
35920
+ )
35546
35921
  );
35547
- });
35548
- socket.on("message", (raw) => {
35549
- void handleMessage(options, client, socket, raw);
35550
- });
35551
- await new Promise((resolve, reject) => {
35552
- socket.once("close", resolve);
35553
- socket.once("error", reject);
35554
- }).finally(() => signal?.removeEventListener("abort", stop));
35555
- await client.close();
35922
+ const token = options.getToken ? await options.getToken() : config2.MACHINE_TERMINAL_DEVICE_TOKEN;
35923
+ const socket = new wrapper_default(config2.MACHINE_TERMINAL_GATEWAY_URL, {
35924
+ headers: { Authorization: "Bearer " + token }
35925
+ });
35926
+ disposeSocket = () => socket.terminate();
35927
+ const pending = /* @__PURE__ */ new Map();
35928
+ let requestProgressEnabled = false;
35929
+ const forwarder = new ProcessEventForwarder(
35930
+ async (name, args) => client.callTool({ name, arguments: args }),
35931
+ (message) => {
35932
+ if (socket.readyState !== wrapper_default.OPEN) return;
35933
+ if (socket.bufferedAmount > 512e3) {
35934
+ socket.close(1013, "Process event backpressure");
35935
+ throw new Error(
35936
+ "Process event WebSocket backpressure limit reached."
35937
+ );
35938
+ }
35939
+ socket.send(JSON.stringify(message));
35940
+ },
35941
+ options.log,
35942
+ acknowledged,
35943
+ 3e3,
35944
+ () => {
35945
+ if (socket.readyState === wrapper_default.OPEN) {
35946
+ socket.close(1013, "Process event delivery stalled");
35947
+ }
35948
+ }
35949
+ );
35950
+ const stop = () => socket.close(1e3, "Client stopping.");
35951
+ signal?.addEventListener("abort", stop, { once: true });
35952
+ socket.on("open", () => {
35953
+ socket.send(
35954
+ JSON.stringify({
35955
+ type: "hello",
35956
+ deviceId: config2.MACHINE_TERMINAL_DEVICE_ID,
35957
+ deviceSecret: config2.MACHINE_TERMINAL_DEVICE_SECRET,
35958
+ name: config2.MACHINE_TERMINAL_DEVICE_NAME,
35959
+ platform: os.platform() + "-" + os.arch(),
35960
+ protocolVersion: 2,
35961
+ capabilities: supportedAgentTools
35962
+ })
35963
+ );
35964
+ });
35965
+ socket.on("message", (raw) => {
35966
+ try {
35967
+ const message = JSON.parse(raw.toString());
35968
+ const cancelled = cancelSchema.safeParse(message);
35969
+ if (cancelled.success) {
35970
+ pending.get(cancelled.data.requestId)?.abort();
35971
+ return;
35972
+ }
35973
+ const ack = ackSchema.safeParse(message);
35974
+ if (ack.success) {
35975
+ forwarder.acknowledge(ack.data.sessionId, ack.data.sequence);
35976
+ return;
35977
+ }
35978
+ if (message && typeof message === "object" && "type" in message && message.type === "ready") {
35979
+ requestProgressEnabled = message.requestProgressVersion === 1;
35980
+ void forwarder.recover().catch(
35981
+ (error62) => options.log?.(
35982
+ "Could not recover local process streams: " + (error62 instanceof Error ? error62.message : "unknown error")
35983
+ )
35984
+ );
35985
+ return;
35986
+ }
35987
+ } catch {
35988
+ options.log?.("Ignoring a malformed gateway message.");
35989
+ return;
35990
+ }
35991
+ void handleMessage(
35992
+ options,
35993
+ client,
35994
+ socket,
35995
+ raw,
35996
+ pending,
35997
+ forwarder,
35998
+ requestProgressEnabled
35999
+ );
36000
+ });
36001
+ socket.on("close", () => {
36002
+ forwarder.close();
36003
+ for (const controller of pending.values()) controller.abort();
36004
+ });
36005
+ await new Promise((resolve, reject) => {
36006
+ socket.once("close", resolve);
36007
+ socket.once("error", reject);
36008
+ }).finally(() => signal?.removeEventListener("abort", stop));
36009
+ } finally {
36010
+ disposeSocket?.();
36011
+ await client.close().catch(() => void 0);
36012
+ }
35556
36013
  }
35557
- async function handleMessage(options, client, socket, raw) {
36014
+ async function handleMessage(options, client, socket, raw, pending, forwarder, requestProgressEnabled) {
35558
36015
  let requestId;
36016
+ let controller;
36017
+ let deadlineTimer;
35559
36018
  try {
35560
- const parsed = requestSchema.parse(JSON.parse(raw.toString()));
35561
- requestId = parsed.requestId;
36019
+ const payload = JSON.parse(raw.toString());
36020
+ const envelope = requestEnvelope.safeParse(payload);
36021
+ if (!envelope.success) {
36022
+ options.log?.("Ignoring a request with an invalid envelope.");
36023
+ return;
36024
+ }
36025
+ requestId = envelope.data.requestId;
36026
+ const parsed = requestSchema.safeParse(payload);
36027
+ if (!parsed.success) {
36028
+ const tool = payload && typeof payload === "object" && "tool" in payload ? payload.tool : void 0;
36029
+ if (typeof tool === "string" && !supportedAgentTools.includes(tool)) {
36030
+ throw new Error(
36031
+ "UNSUPPORTED_TOOL: " + tool + ". Update the local companion."
36032
+ );
36033
+ }
36034
+ throw new Error("INVALID_REQUEST: Invalid tool arguments or deadline.");
36035
+ }
36036
+ if (pending.has(requestId)) {
36037
+ throw new Error(
36038
+ "DUPLICATE_REQUEST: This request is already in progress."
36039
+ );
36040
+ }
36041
+ const deadlineAt = parsed.data.deadlineAt ?? Date.now() + DEFAULT_REQUEST_LIFETIME_MS;
36042
+ if (deadlineAt <= Date.now()) {
36043
+ throw new Error("REQUEST_EXPIRED: The request deadline has passed.");
36044
+ }
36045
+ controller = new AbortController();
36046
+ pending.set(requestId, controller);
36047
+ deadlineTimer = setTimeout(
36048
+ () => controller?.abort(),
36049
+ Math.max(0, deadlineAt - Date.now())
36050
+ );
36051
+ let progressSequence = 0;
36052
+ const progress = (phase) => {
36053
+ if (!requestProgressEnabled || parsed.data.tool !== "start_process" || socket.readyState !== wrapper_default.OPEN)
36054
+ return;
36055
+ if (socket.bufferedAmount > 512e3) {
36056
+ socket.close(1013, "Request progress backpressure");
36057
+ return;
36058
+ }
36059
+ try {
36060
+ socket.send(
36061
+ JSON.stringify({
36062
+ type: "request_progress",
36063
+ requestId,
36064
+ sequence: ++progressSequence,
36065
+ phase,
36066
+ ...phase === "approved" ? {
36067
+ approvalMode: options.config.MACHINE_TERMINAL_AUTO_APPROVE === "1" ? "automatic" : "manual"
36068
+ } : {}
36069
+ })
36070
+ );
36071
+ } catch {
36072
+ }
36073
+ };
36074
+ progress("received");
35562
36075
  await authorizeToolCall(
35563
- parsed.tool,
35564
- parsed.arguments,
36076
+ parsed.data.tool,
36077
+ parsed.data.arguments,
35565
36078
  options.config.MACHINE_TERMINAL_AGENT_ALLOW_DESTRUCTIVE === "1",
35566
- options.approve
36079
+ options.approve,
36080
+ controller.signal,
36081
+ progress
35567
36082
  );
36083
+ assertActive(controller.signal);
35568
36084
  const result = await client.callTool({
35569
- name: parsed.tool,
35570
- arguments: parsed.arguments
36085
+ name: parsed.data.tool,
36086
+ arguments: parsed.data.arguments
35571
36087
  });
35572
36088
  if (result.isError) {
35573
36089
  const content = Array.isArray(result.content) ? result.content : [];
@@ -35584,6 +36100,12 @@ async function handleMessage(options, client, socket, raw) {
35584
36100
  result: result.structuredContent ?? { content: result.content }
35585
36101
  })
35586
36102
  );
36103
+ if (parsed.data.tool === "start_process") {
36104
+ const snapshot = result.structuredContent;
36105
+ if (snapshot && typeof snapshot === "object" && "sessionId" in snapshot && typeof snapshot.sessionId === "string" && "taskId" in snapshot && typeof snapshot.taskId === "string") {
36106
+ forwarder.watch(snapshot.sessionId);
36107
+ }
36108
+ }
35587
36109
  }
35588
36110
  } catch (error62) {
35589
36111
  if (!requestId || socket.readyState !== wrapper_default.OPEN) return;
@@ -35594,18 +36116,24 @@ async function handleMessage(options, client, socket, raw) {
35594
36116
  error: error62 instanceof Error ? error62.message : "Local request failed."
35595
36117
  })
35596
36118
  );
36119
+ } finally {
36120
+ if (deadlineTimer) clearTimeout(deadlineTimer);
36121
+ if (requestId && controller && pending.get(requestId) === controller) {
36122
+ pending.delete(requestId);
36123
+ }
35597
36124
  }
35598
36125
  }
35599
36126
  async function runAgent(options) {
35600
36127
  let reconnectDelayMs = 1e3;
36128
+ const acknowledged = /* @__PURE__ */ new Map();
35601
36129
  while (!options.signal?.aborted) {
35602
36130
  try {
35603
- await connectOnce(options);
36131
+ await connectOnce(options, acknowledged);
35604
36132
  reconnectDelayMs = 1e3;
35605
36133
  } catch (error62) {
35606
36134
  if (options.signal?.aborted) break;
35607
36135
  options.log?.(
35608
- `Connection failed: ${error62 instanceof Error ? error62.message : "unknown error"}`
36136
+ "Connection failed: " + (error62 instanceof Error ? error62.message : "unknown error")
35609
36137
  );
35610
36138
  }
35611
36139
  if (options.signal?.aborted) break;
@@ -35624,12 +36152,120 @@ async function runAgent(options) {
35624
36152
  }
35625
36153
  }
35626
36154
 
36155
+ // src/local-agent-auth.ts
36156
+ import { createHmac, timingSafeEqual } from "node:crypto";
36157
+ function localHealthProof(secret, challenge) {
36158
+ if (!secret || !/^[a-f0-9]{64}$/i.test(secret)) return null;
36159
+ if (!challenge || !/^[a-f0-9]{64}$/i.test(challenge)) return null;
36160
+ return createHmac("sha256", Buffer.from(secret, "hex")).update(challenge, "ascii").digest("hex");
36161
+ }
36162
+
36163
+ // src/device-management.ts
36164
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
36165
+ function validDeviceId(id) {
36166
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id);
36167
+ }
36168
+ function managementUrl(connectionUrl) {
36169
+ const url2 = new URL(connectionUrl);
36170
+ if (url2.username || url2.password || url2.pathname !== "/device/connect") {
36171
+ throw new Error(
36172
+ "Expected a credential-free Terminal Commands gateway URL."
36173
+ );
36174
+ }
36175
+ if (url2.protocol === "wss:") {
36176
+ url2.protocol = "https:";
36177
+ } else if (url2.protocol === "ws:" && LOOPBACK_HOSTS.has(url2.hostname)) {
36178
+ url2.protocol = "http:";
36179
+ } else {
36180
+ throw new Error("Device revocation requires HTTPS, or loopback test HTTP.");
36181
+ }
36182
+ url2.search = "";
36183
+ url2.hash = "";
36184
+ return url2;
36185
+ }
36186
+ function deviceRevocationUrl(connectionUrl, deviceId) {
36187
+ if (!validDeviceId(deviceId)) throw new Error("Invalid device identifier.");
36188
+ const url2 = managementUrl(connectionUrl);
36189
+ url2.pathname = "/devices/" + encodeURIComponent(deviceId) + "/revoke";
36190
+ return url2.toString();
36191
+ }
36192
+ function pairedDevicesUrl(connectionUrl, cursor) {
36193
+ if (cursor !== void 0 && !validDeviceId(cursor)) {
36194
+ throw new Error("Invalid pairing-list cursor.");
36195
+ }
36196
+ const url2 = managementUrl(connectionUrl);
36197
+ url2.pathname = "/devices";
36198
+ if (cursor !== void 0) url2.searchParams.set("after", cursor);
36199
+ return url2.toString();
36200
+ }
36201
+ async function fetchPairedDevices(connectionUrl, managementToken2, fetchImpl = fetch) {
36202
+ if (!managementToken2 || /\s/.test(managementToken2)) {
36203
+ throw new Error("Invalid device-management access token.");
36204
+ }
36205
+ const devices = [];
36206
+ const seen = /* @__PURE__ */ new Set();
36207
+ let cursor;
36208
+ for (let page = 0; page < 10; page++) {
36209
+ const response = await fetchImpl(pairedDevicesUrl(connectionUrl, cursor), {
36210
+ headers: { Authorization: "Bearer " + managementToken2 },
36211
+ redirect: "error",
36212
+ signal: AbortSignal.timeout(15e3)
36213
+ });
36214
+ if (!response.ok) {
36215
+ throw new Error(
36216
+ "Could not list paired devices (" + response.status + ")."
36217
+ );
36218
+ }
36219
+ const payload = await response.json();
36220
+ if (!Array.isArray(payload.devices) || payload.devices.length > 100) {
36221
+ throw new Error("Invalid paired-device response.");
36222
+ }
36223
+ for (const entry of payload.devices) {
36224
+ const item = entry;
36225
+ 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)) {
36226
+ throw new Error("Invalid or repeated device in pairing inventory.");
36227
+ }
36228
+ seen.add(item.deviceId);
36229
+ devices.push(item);
36230
+ }
36231
+ if (payload.nextCursor === null) return { devices, truncated: false };
36232
+ if (typeof payload.nextCursor !== "string" || !validDeviceId(payload.nextCursor) || cursor === payload.nextCursor) {
36233
+ throw new Error("Invalid device-list continuation cursor.");
36234
+ }
36235
+ cursor = payload.nextCursor;
36236
+ }
36237
+ return { devices, truncated: true };
36238
+ }
36239
+ async function requestDeviceRevocation(connectionUrl, deviceId, managementToken2, fetchImpl = fetch) {
36240
+ if (!managementToken2 || /\s/.test(managementToken2)) {
36241
+ throw new Error("Invalid device-management access token.");
36242
+ }
36243
+ const endpoint2 = deviceRevocationUrl(connectionUrl, deviceId);
36244
+ const response = await fetchImpl(endpoint2, {
36245
+ method: "POST",
36246
+ headers: { Authorization: "Bearer " + managementToken2 },
36247
+ redirect: "error",
36248
+ signal: AbortSignal.timeout(15e3)
36249
+ });
36250
+ if (!response.ok) {
36251
+ throw new Error(
36252
+ "Device revocation was not confirmed (" + response.status + ")."
36253
+ );
36254
+ }
36255
+ const result = await response.json();
36256
+ if (result.deviceId !== deviceId || result.revoked !== true) {
36257
+ throw new Error("Gateway did not confirm this device's revocation.");
36258
+ }
36259
+ }
36260
+
35627
36261
  // src/agent-config.ts
35628
36262
  import os2 from "node:os";
35629
36263
  var schema = external_exports.object({
35630
36264
  MACHINE_TERMINAL_GATEWAY_URL: external_exports.string().url(),
35631
36265
  MACHINE_TERMINAL_DEVICE_TOKEN: external_exports.string().min(1),
36266
+ MACHINE_TERMINAL_LOCAL_AGENT_TOKEN: external_exports.string().regex(/^[a-f0-9]{64}$/i).optional(),
35632
36267
  MACHINE_TERMINAL_DEVICE_ID: external_exports.string().min(1).max(128),
36268
+ MACHINE_TERMINAL_DEVICE_SECRET: external_exports.string().regex(/^[a-f0-9]{64}$/i).optional(),
35633
36269
  MACHINE_TERMINAL_DEVICE_NAME: external_exports.string().min(1).max(128).default(os2.hostname()),
35634
36270
  MACHINE_TERMINAL_LOCAL_MCP_URL: external_exports.string().url().default("http://127.0.0.1:3333/mcp"),
35635
36271
  MACHINE_TERMINAL_AGENT_ALLOW_DESTRUCTIVE: external_exports.enum(["0", "1"]).default("0"),
@@ -35693,55 +36329,78 @@ function keyValueBlock(rows) {
35693
36329
  }
35694
36330
 
35695
36331
  // src/approval.ts
36332
+ var DEFAULT_APPROVAL_TIMEOUT_MS = 3e4;
36333
+ function safeLabel(value) {
36334
+ return JSON.stringify(String(value));
36335
+ }
35696
36336
  function approvalSummary(tool, args) {
35697
36337
  if (tool === "write_text_file") {
35698
36338
  const bytes = Buffer.byteLength(String(args.contents ?? ""));
35699
- return `Write ${bytes} bytes to ${String(args.path ?? "an unknown path")}${args.overwrite ? " (overwrite)" : ""}`;
36339
+ return "Write " + bytes + " bytes to " + safeLabel(args.path ?? "an unknown path") + (args.overwrite ? " (overwrite)" : "");
35700
36340
  }
35701
36341
  if (tool === "start_process") {
35702
36342
  const command = [
35703
- String(args.program ?? ""),
35704
- ...Array.isArray(args.args) ? args.args.map(String) : []
36343
+ safeLabel(args.program ?? ""),
36344
+ ...Array.isArray(args.args) ? args.args.map(safeLabel) : []
35705
36345
  ];
35706
- return `Start: ${command.join(" ")}`;
36346
+ return "Start: " + command.join(" ") + " (cwd: " + safeLabel(args.cwd ?? "configured root") + ")";
35707
36347
  }
35708
36348
  if (tool === "terminate_process") {
35709
- return `Terminate process session ${String(args.sessionId ?? "")}`;
36349
+ return "Terminate process session " + safeLabel(args.sessionId ?? "");
35710
36350
  }
35711
- return `Run destructive tool: ${tool}`;
36351
+ return "Run destructive tool: " + safeLabel(tool);
35712
36352
  }
35713
36353
  function createAutoApprover() {
35714
- return (tool, args) => {
36354
+ return (tool, args, signal) => {
36355
+ if (signal?.aborted) return Promise.resolve(false);
35715
36356
  console.log(
35716
- `
35717
- ${warn("Approval requested:")} ${approvalSummary(tool, args)} ${dim("(auto-approved)")}`
36357
+ "\n" + warn("Approval requested:") + " " + approvalSummary(tool, args) + " " + dim("(auto-approved)")
35718
36358
  );
35719
36359
  return Promise.resolve(true);
35720
36360
  };
35721
36361
  }
35722
- function createTerminalApprover() {
35723
- let queue = Promise.resolve(false);
35724
- return (tool, args) => {
35725
- queue = queue.catch(() => false).then(async () => {
35726
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
36362
+ function createTerminalApprover(timeoutMs = DEFAULT_APPROVAL_TIMEOUT_MS) {
36363
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
36364
+ throw new Error("Approval timeout must be a positive integer.");
36365
+ }
36366
+ let queue = Promise.resolve();
36367
+ return (tool, args, signal) => {
36368
+ const next = queue.then(async () => {
36369
+ if (!process.stdin.isTTY || !process.stdout.isTTY || signal?.aborted) {
36370
+ return false;
36371
+ }
36372
+ const controller = new AbortController();
36373
+ const abort = () => controller.abort();
36374
+ signal?.addEventListener("abort", abort, { once: true });
36375
+ const timer = setTimeout(abort, timeoutMs);
35727
36376
  const prompt = createInterface({
35728
36377
  input: process.stdin,
35729
36378
  output: process.stdout
35730
36379
  });
35731
36380
  try {
36381
+ if (signal?.aborted) return false;
35732
36382
  console.log(
35733
- `
35734
- ${warn("Approval requested:")} ${approvalSummary(tool, args)}`
36383
+ "\n" + warn("Approval requested:") + " " + approvalSummary(tool, args)
35735
36384
  );
35736
36385
  const answer = await prompt.question(
35737
- `${bold("Approve this action?")} [y/N] `
36386
+ bold("Approve this action?") + " [y/N] ",
36387
+ { signal: controller.signal }
35738
36388
  );
35739
- return (/* @__PURE__ */ new Set(["y", "yes"])).has(answer.trim().toLowerCase());
36389
+ return !controller.signal.aborted && (/* @__PURE__ */ new Set(["y", "yes"])).has(answer.trim().toLowerCase());
36390
+ } catch (error62) {
36391
+ if (controller.signal.aborted) return false;
36392
+ throw error62;
35740
36393
  } finally {
36394
+ clearTimeout(timer);
36395
+ signal?.removeEventListener("abort", abort);
35741
36396
  prompt.close();
35742
36397
  }
35743
36398
  });
35744
- return queue;
36399
+ queue = next.then(
36400
+ () => void 0,
36401
+ () => void 0
36402
+ );
36403
+ return next;
35745
36404
  };
35746
36405
  }
35747
36406
 
@@ -35754,15 +36413,26 @@ var COMMANDS = [
35754
36413
  "connect",
35755
36414
  "status",
35756
36415
  "logout",
36416
+ "revoke-device",
36417
+ "paired-devices",
35757
36418
  "help"
35758
36419
  ];
35759
36420
  var COMMAND_SUMMARIES = {
35760
36421
  login: "Sign in using a URL and one-time code",
35761
36422
  connect: "Connect this machine until stopped",
35762
36423
  status: "Show sign-in state and the settings connect would use",
35763
- logout: "Remove locally saved credentials"
36424
+ logout: "Remove locally saved credentials",
36425
+ "revoke-device": "Revoke one paired device using fresh owner authentication",
36426
+ "paired-devices": "List offline, connected and revoked device identities"
35764
36427
  };
35765
- var AUTH_COMMANDS = ["login", "connect", "status", "logout"];
36428
+ var AUTH_COMMANDS = [
36429
+ "login",
36430
+ "connect",
36431
+ "status",
36432
+ "logout",
36433
+ "revoke-device",
36434
+ "paired-devices"
36435
+ ];
35766
36436
  var OPTIONS = [
35767
36437
  {
35768
36438
  name: "root",
@@ -35787,8 +36457,8 @@ var OPTIONS = [
35787
36457
  kind: "string",
35788
36458
  env: "MACHINE_TERMINAL_DEVICE_ID",
35789
36459
  placeholder: "<id>",
35790
- commands: ["connect", "status"],
35791
- description: "Device identifier to report. Defaults to the saved local device ID."
36460
+ commands: ["connect", "status", "revoke-device"],
36461
+ description: "Device identifier. Required explicitly for revoke-device; defaults to saved ID for connect/status."
35792
36462
  },
35793
36463
  {
35794
36464
  name: "port",
@@ -35811,7 +36481,7 @@ var OPTIONS = [
35811
36481
  kind: "string",
35812
36482
  env: "MACHINE_TERMINAL_GATEWAY_URL",
35813
36483
  placeholder: "<url>",
35814
- commands: ["connect", "status"],
36484
+ commands: ["connect", "status", "revoke-device", "paired-devices"],
35815
36485
  description: "Gateway WebSocket URL. Defaults to wss://mcp.terminalcommands.fr/device/connect."
35816
36486
  },
35817
36487
  {
@@ -36119,6 +36789,8 @@ function renderHelp(command = "help") {
36119
36789
  " terminal-commands connect --root ~/Documents/GitHub --root ~/work",
36120
36790
  " terminal-commands connect --root . --dry-run",
36121
36791
  " terminal-commands connect --root ~/code --name laptop --allow-destructive",
36792
+ " terminal-commands paired-devices",
36793
+ " terminal-commands revoke-device --device-id <paired-device-id>",
36122
36794
  " npm run cli -- connect --root ~/Documents/GitHub --allowed-program git,node"
36123
36795
  );
36124
36796
  return lines.join("\n");
@@ -36126,7 +36798,7 @@ function renderHelp(command = "help") {
36126
36798
 
36127
36799
  // src/credential-store.ts
36128
36800
  import { promises as fs2 } from "node:fs";
36129
- import { randomUUID } from "node:crypto";
36801
+ import { randomBytes, randomUUID } from "node:crypto";
36130
36802
  import os4 from "node:os";
36131
36803
  import path2 from "node:path";
36132
36804
  var storedSchema = external_exports.object({
@@ -36199,6 +36871,23 @@ async function loadOrCreateDeviceId(environment = process.env) {
36199
36871
  });
36200
36872
  return (await fs2.readFile(target, "utf8")).trim();
36201
36873
  }
36874
+ async function loadOrCreateDeviceSecret(environment = process.env) {
36875
+ const directory = configDirectory(environment);
36876
+ const target = path2.join(directory, "device-secret");
36877
+ await fs2.mkdir(directory, { recursive: true, mode: 448 });
36878
+ const generated = randomBytes(32).toString("hex");
36879
+ try {
36880
+ await fs2.writeFile(target, generated + "\n", { mode: 384, flag: "wx" });
36881
+ } catch (error62) {
36882
+ if (error62.code !== "EEXIST") throw error62;
36883
+ }
36884
+ const value = (await fs2.readFile(target, "utf8")).trim();
36885
+ if (!/^[a-f0-9]{64}$/i.test(value)) {
36886
+ throw new Error("DEVICE_SECRET_INVALID: Re-pair this machine securely.");
36887
+ }
36888
+ if (process.platform !== "win32") await fs2.chmod(target, 384);
36889
+ return value;
36890
+ }
36202
36891
 
36203
36892
  // src/device-auth.ts
36204
36893
  var deviceCodeSchema = external_exports.object({
@@ -36449,14 +37138,22 @@ async function validCredentials(auth2) {
36449
37138
  await saveCredentials(refreshed);
36450
37139
  return refreshed;
36451
37140
  }
36452
- async function waitForHealth(url2, child) {
37141
+ async function waitForHealth(url2, child, agentToken) {
37142
+ const challenge = randomBytes2(32).toString("hex");
37143
+ const expectedProof = localHealthProof(agentToken, challenge);
37144
+ if (!expectedProof)
37145
+ throw new Error("Invalid local agent startup capability.");
36453
37146
  const deadline = Date.now() + 1e4;
36454
37147
  while (Date.now() < deadline) {
36455
- if (child.exitCode !== null)
37148
+ if (child.exitCode !== null || child.signalCode !== null)
36456
37149
  throw new Error("The local terminal service could not start.");
36457
37150
  try {
36458
- const response = await fetch(url2);
36459
- if (response.ok) return;
37151
+ const response = await fetch(url2, {
37152
+ headers: { "X-Terminal-Commands-Health-Challenge": challenge },
37153
+ signal: AbortSignal.timeout(1e3)
37154
+ });
37155
+ const health = await response.json();
37156
+ if (response.ok && health?.agentProof === expectedProof) return;
36460
37157
  } catch {
36461
37158
  }
36462
37159
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -36475,6 +37172,7 @@ async function connect(environment, parsed) {
36475
37172
  );
36476
37173
  return;
36477
37174
  }
37175
+ const deviceSecret = environment.MACHINE_TERMINAL_DEVICE_SECRET ?? await loadOrCreateDeviceSecret();
36478
37176
  const auth2 = resolveAuth(environment);
36479
37177
  const firstCredentials = await validCredentials(auth2);
36480
37178
  console.log(
@@ -36487,11 +37185,14 @@ async function connect(environment, parsed) {
36487
37185
  )
36488
37186
  );
36489
37187
  }
37188
+ const localAgentToken = randomBytes2(32).toString("hex");
36490
37189
  const childEnvironment = {
36491
37190
  ...environment,
37191
+ MACHINE_TERMINAL_LOCAL_AGENT_TOKEN: localAgentToken,
36492
37192
  MACHINE_TERMINAL_HOST: settings.host,
36493
37193
  MACHINE_TERMINAL_PORT: String(settings.port),
36494
- MACHINE_TERMINAL_ROOTS: settings.roots.join(path3.delimiter)
37194
+ MACHINE_TERMINAL_ROOTS: settings.roots.join(path3.delimiter),
37195
+ MACHINE_TERMINAL_TASK_RECEIPTS_FILE: environment.MACHINE_TERMINAL_TASK_RECEIPTS_FILE ?? path3.join(configDirectory(environment), "task-receipts.json")
36495
37196
  };
36496
37197
  const serverEntry = fileURLToPath(new URL("./index.js", import.meta.url));
36497
37198
  const local = spawn(process.execPath, [serverEntry], {
@@ -36503,12 +37204,14 @@ async function connect(environment, parsed) {
36503
37204
  process.once("SIGINT", stop);
36504
37205
  process.once("SIGTERM", stop);
36505
37206
  try {
36506
- await waitForHealth(`${settings.localUrl}/health`, local);
37207
+ await waitForHealth(`${settings.localUrl}/health`, local, localAgentToken);
36507
37208
  const config2 = loadAgentConfig({
36508
37209
  ...childEnvironment,
36509
37210
  MACHINE_TERMINAL_GATEWAY_URL: settings.gatewayUrl,
37211
+ MACHINE_TERMINAL_LOCAL_AGENT_TOKEN: localAgentToken,
36510
37212
  MACHINE_TERMINAL_DEVICE_TOKEN: firstCredentials.accessToken,
36511
37213
  MACHINE_TERMINAL_DEVICE_ID: deviceId,
37214
+ MACHINE_TERMINAL_DEVICE_SECRET: deviceSecret,
36512
37215
  MACHINE_TERMINAL_DEVICE_NAME: settings.deviceName,
36513
37216
  MACHINE_TERMINAL_LOCAL_MCP_URL: `${settings.localUrl}/mcp`,
36514
37217
  MACHINE_TERMINAL_AGENT_ALLOW_DESTRUCTIVE: settings.allowDestructive ? "1" : "0"
@@ -36583,6 +37286,81 @@ async function status(environment, parsed) {
36583
37286
  ].join("\n")
36584
37287
  );
36585
37288
  }
37289
+ async function managementToken(environment, purpose) {
37290
+ const management = {
37291
+ ...resolveAuth(environment),
37292
+ scope: "openid profile email terminal:devices:revoke"
37293
+ };
37294
+ const code = await requestDeviceCode(management);
37295
+ console.log("\n" + bold("Authorize " + purpose + " in your browser:"));
37296
+ console.log(info(code.verification_uri_complete ?? code.verification_uri));
37297
+ console.log("\n" + bold("One-time code: ") + info(code.user_code));
37298
+ const credentials = await pollForDeviceToken(
37299
+ management,
37300
+ code.device_code,
37301
+ code.expires_in,
37302
+ code.interval
37303
+ );
37304
+ return credentials.accessToken;
37305
+ }
37306
+ async function pairedDevices(environment) {
37307
+ const gateway = environment.MACHINE_TERMINAL_GATEWAY_URL ?? DEFAULTS.gatewayUrl;
37308
+ pairedDevicesUrl(gateway);
37309
+ const token = await managementToken(environment, "paired-device inventory");
37310
+ const result = await fetchPairedDevices(gateway, token);
37311
+ if (result.devices.length === 0) {
37312
+ console.log(info("No paired devices found for this account."));
37313
+ return;
37314
+ }
37315
+ for (const device of result.devices) {
37316
+ const status2 = device.revokedAt ? "revoked" : device.connected ? "connected" : "offline";
37317
+ console.log(
37318
+ device.deviceId + " \u2014 " + status2 + " (paired " + device.pairedAt + ")"
37319
+ );
37320
+ }
37321
+ if (result.truncated) {
37322
+ console.log(warn("More than 1,000 device records: list was truncated."));
37323
+ }
37324
+ }
37325
+ async function revokeDevice(environment, parsed) {
37326
+ const supplied = parsed.options.get("device-id");
37327
+ const deviceId = Array.isArray(supplied) ? supplied[0] : void 0;
37328
+ if (!deviceId) {
37329
+ throw new Error("Specify --device-id of the pairing to revoke.");
37330
+ }
37331
+ const gateway = environment.MACHINE_TERMINAL_GATEWAY_URL ?? DEFAULTS.gatewayUrl;
37332
+ deviceRevocationUrl(gateway, deviceId);
37333
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
37334
+ throw new Error("Revocation needs an interactive confirmation terminal.");
37335
+ }
37336
+ console.log(
37337
+ warn(
37338
+ "Device revocation is permanent for this device ID. It disconnects the companion, but cannot stop already launched OS processes or invalidate Auth0 tokens."
37339
+ )
37340
+ );
37341
+ const prompt = createInterface2({
37342
+ input: process.stdin,
37343
+ output: process.stdout
37344
+ });
37345
+ let answer;
37346
+ try {
37347
+ answer = await prompt.question(
37348
+ "Type REVOKE " + deviceId + " to continue: "
37349
+ );
37350
+ } finally {
37351
+ prompt.close();
37352
+ }
37353
+ if (answer.trim() !== "REVOKE " + deviceId) {
37354
+ throw new Error("Device revocation cancelled.");
37355
+ }
37356
+ const token = await managementToken(environment, "device revocation");
37357
+ await requestDeviceRevocation(gateway, deviceId, token);
37358
+ console.log(
37359
+ ok(
37360
+ "Device " + deviceId + " was revoked and its existing pairing disabled."
37361
+ )
37362
+ );
37363
+ }
36586
37364
  async function main() {
36587
37365
  const parsed = parseCommandLine(process.argv.slice(2));
36588
37366
  if (parsed.help) {
@@ -36596,6 +37374,12 @@ async function main() {
36596
37374
  if (parsed.command === "login") return login(environment);
36597
37375
  if (parsed.command === "connect") return connect(environment, parsed);
36598
37376
  if (parsed.command === "status") return status(environment, parsed);
37377
+ if (parsed.command === "revoke-device") {
37378
+ return revokeDevice(environment, parsed);
37379
+ }
37380
+ if (parsed.command === "paired-devices") {
37381
+ return pairedDevices(environment);
37382
+ }
36599
37383
  if (parsed.command === "logout") {
36600
37384
  await clearCredentials();
36601
37385
  console.log(ok("Signed out and removed locally saved credentials."));